kuaifan 20b5daba50 feat(manage): 团队管理支持管理员创建/批量导入员工账号(含部门、职位)
单个创建:邮箱/昵称/初始密码 + 可选首登改密、职位、部门(多选,选子部门自动补选上级,并加入对应部门群)。

批量导入:上传 Excel/CSV → 预览逐行校验 → 确认后导入。职位为模板第4列(选填,逐行解析校验),部门在预览表按行勾选后由底部设置部门到选中写入;导入按行返回结果(全成功关弹窗+成功提示;含失败留弹窗显示失败明细;仅 success>0 才刷新列表)。

后端:User::createByAdmin 选项数组化 + 校验助手 assertValidProfession/assertValidDepartments;importUsers 逐行 department/profession;UsersController createuser/import;UserImport/UserImportTemplate(含职位列)。

测试:tests/Feature/AdminCreateUserTest、tests/Unit/UserImportParseTest。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:26:34 +00:00

144 lines
5.6 KiB
Vue

<template>
<Modal
v-model="show"
:title="$L('创建用户')"
:mask-closable="false"
@on-cancel="onCancel">
<Form ref="form" :model="formData" :label-width="80" @submit.native.prevent>
<FormItem :label="$L('邮箱')" required>
<Input v-model="formData.email" :placeholder="$L('请输入邮箱')" clearable/>
</FormItem>
<FormItem :label="$L('昵称')" required>
<Input v-model="formData.nickname" :placeholder="$L('请输入昵称')" clearable/>
</FormItem>
<FormItem :label="$L('初始密码')" required>
<Input v-model="formData.password" type="password" password :placeholder="$L('请输入初始密码')" clearable/>
</FormItem>
<FormItem :label="$L('职位')">
<Input v-model="formData.profession" :maxlength="20" :placeholder="$L('请输入职位/职称')" clearable/>
</FormItem>
<FormItem :label="$L('所属部门')">
<Select
v-model="formData.department"
multiple
:multiple-max="10"
:multiple-max-before="onMultipleMaxBefore"
:placeholder="$L('留空为默认部门')">
<Option
v-for="(item, index) in departmentList"
:value="item.id"
:key="index"
:label="item.chains.join(' - ')">
<div :class="`department-level-name level-${item.level - 1}`">{{ item.name }}</div>
</Option>
</Select>
</FormItem>
<FormItem>
<Checkbox v-model="formData.changepass">{{$L('员工首次登录需修改密码')}}</Checkbox>
</FormItem>
</Form>
<div slot="footer">
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loading" @click="onSubmit">{{$L('创建')}}</Button>
</div>
</Modal>
</template>
<script>
export default {
name: 'CreateUserModal',
props: {
value: {
type: Boolean,
default: false
},
departmentList: {
type: Array,
default: () => []
}
},
data() {
return {
show: false,
loading: false,
formData: {email: '', nickname: '', password: '', changepass: true, profession: '', department: []},
}
},
watch: {
value(val) {
this.show = val;
if (val) {
this.formData = {email: '', nickname: '', password: '', changepass: true, profession: '', department: []};
}
},
show(val) {
this.$emit('input', val);
},
// 选择子部门时自动补选其所有上级部门(与 UserEditModal 行为一致)
'formData.department': {
handler(value, oldValue = []) {
if (!Array.isArray(value) || value.length === 0 || this.departmentList.length === 0) {
return;
}
const previous = Array.isArray(oldValue) ? new Set(oldValue) : new Set();
const selected = new Set(value);
const hasNewSelection = Array.from(selected).some(id => !previous.has(id));
if (!hasNewSelection) {
return;
}
const departmentMap = this.departmentList.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
const needAdd = new Set();
value.forEach((id) => {
let cursor = departmentMap[id];
while (cursor && cursor.parent_id && cursor.parent_id > 0) {
if (!selected.has(cursor.parent_id)) {
needAdd.add(cursor.parent_id);
}
cursor = departmentMap[cursor.parent_id];
}
});
if (needAdd.size > 0) {
const merged = Array.from(new Set([...value, ...needAdd])).sort((a, b) => a - b);
if (merged.length !== value.length || merged.some((id, index) => id !== value[index])) {
this.$set(this.formData, 'department', merged);
}
}
},
deep: true
}
},
methods: {
onCancel() {
this.show = false;
},
onSubmit() {
const {email, nickname, password, changepass, profession, department} = this.formData;
if (!email || !nickname || !password) {
$A.messageWarning('邮箱、昵称、初始密码均为必填');
return;
}
this.loading = true;
this.$store.dispatch("call", {
url: 'users/createuser',
data: {email, nickname, password, changepass: changepass ? 1 : 0, profession, department},
}).then(() => {
this.loading = false;
$A.messageSuccess('创建成功');
this.show = false;
this.$emit('created');
}).catch(({msg}) => {
this.loading = false;
$A.modalError(msg);
});
},
onMultipleMaxBefore(num) {
$A.messageError(this.$L('最多选择(*)个部门', num));
return false;
},
}
}
</script>