fix(user): 导入失败明细改用弹窗展示,用户页字段统一叫「用户账号」
- import-result.ts:导入失败明细从 notification(上百行长消息糊满屏幕)
改成弹窗展示,弹窗内列表可滚动;全部成功时仍用轻提示
- 用户管理:列表列 / 新增表单 / 搜索 / 分配角色的「用户名称」统一为
「用户账号」,与后端校验、导入模板的叫法保持一致
- 导入弹窗补充说明:模板列(用户账号、用户昵称、部门、岗位等)以及
「是否覆盖」不会修改已有用户的密码
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import { message, notification } from 'ant-design-vue'; |
| | | import { message, Modal } from 'ant-design-vue'; |
| | | |
| | | export interface ImportResultNotifyOptions { |
| | | /** 业务主体,用于拼标题与建议,如「设备」「客户」 */ |
| | |
| | | /** |
| | | * 统一展示 Excel 导入结果。 |
| | | * |
| | | * 只要后端返回了失败明细,就绝不能再提示「操作成功」——改为常驻通知逐行列出失败原因, |
| | | * 只要后端返回了失败明细,就绝不能再提示「操作成功」——改为弹窗逐行列出失败原因, |
| | | * 并按失败原因给出下一步动作;全部成功时才提示成功。 |
| | | * |
| | | * 失败明细可能上百行,所以用弹窗(弹窗内列表可滚动),避免长消息糊满屏幕。 |
| | | * |
| | | * @returns 实际写入的条数(新增 + 更新),供调用方决定是否需要刷新列表 |
| | | */ |
| | |
| | | return importedCount; |
| | | } |
| | | |
| | | const nodes: VNodeChild[] = [ |
| | | const hints = buildFailureHints( |
| | | failureEntries.map(([, reason]) => reason), |
| | | { subject, masterDataHint }, |
| | | ); |
| | | |
| | | const content: VNodeChild = h('div', [ |
| | | h( |
| | | 'div', |
| | | `新增成功 ${createCount} 条,更新成功 ${updateCount} 条,失败 ${failureEntries.length} 条:`, |
| | | `新增成功 ${createCount} 条,更新成功 ${updateCount} 条,失败 ${failureEntries.length} 条。`, |
| | | ), |
| | | ...failureEntries.map(([name, reason]) => h('div', `${name}:${reason}`)), |
| | | ...buildFailureHints(failureEntries.map(([, reason]) => reason), { |
| | | subject, |
| | | masterDataHint, |
| | | }).map((hint) => h('div', { class: 'mt-2 text-red-500' }, hint)), |
| | | ]; |
| | | h( |
| | | 'div', |
| | | { |
| | | class: |
| | | 'mt-2 max-h-72 overflow-y-auto rounded border border-solid border-gray-200 px-3 py-2 text-sm leading-6', |
| | | }, |
| | | failureEntries.map(([name, reason]) => |
| | | h('div', { class: 'py-0.5' }, `${name}:${reason}`), |
| | | ), |
| | | ), |
| | | ...hints.map((hint) => |
| | | h('div', { class: 'mt-2 text-red-500' }, hint), |
| | | ), |
| | | ]); |
| | | |
| | | const notify = importedCount === 0 ? notification.error : notification.warning; |
| | | notify({ |
| | | message: |
| | | const openModal = importedCount === 0 ? Modal.error : Modal.warning; |
| | | openModal({ |
| | | title: |
| | | importedCount === 0 |
| | | ? `${subject}导入失败:${failureEntries.length} 行全部未导入` |
| | | : `导入完成,部分${subject}导入失败(成功 ${importedCount} 条 / 失败 ${failureEntries.length} 条)`, |
| | | description: () => h('div', nodes), |
| | | duration: null, |
| | | : `导入完成:成功 ${importedCount} 条,失败 ${failureEntries.length} 条`, |
| | | content, |
| | | width: 680, |
| | | centered: true, |
| | | okText: '知道了', |
| | | }); |
| | | return importedCount; |
| | | } |
| | |
| | | /** 按失败原因归纳下一步动作,避免用户只看到「失败」却不知道改哪里 */ |
| | | function buildFailureHints( |
| | | reasons: string[], |
| | | { subject, masterDataHint }: Pick<ImportResultNotifyOptions, 'masterDataHint' | 'subject'>, |
| | | { |
| | | subject, |
| | | masterDataHint, |
| | | }: Pick<ImportResultNotifyOptions, 'masterDataHint' | 'subject'>, |
| | | ): string[] { |
| | | const hints = new Set<string>(); |
| | | for (const reason of reasons) { |
| | |
| | | }, |
| | | { |
| | | fieldName: 'username', |
| | | label: '用户名称', |
| | | label: '用户账号', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请输入用户名称', |
| | | placeholder: '请输入用户账号(4-30 位数字、字母)', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | |
| | | }, |
| | | { |
| | | fieldName: 'username', |
| | | label: '用户名称', |
| | | label: '用户账号', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | |
| | | label: '用户数据', |
| | | component: 'Upload', |
| | | rules: 'required', |
| | | help: '仅允许导入 xls、xlsx 格式文件', |
| | | help: '仅允许导入 xls、xlsx 格式文件;请先下载导入模板,按模板列(用户账号、用户昵称、部门、岗位等)填写', |
| | | }, |
| | | { |
| | | fieldName: 'updateSupport', |
| | |
| | | unCheckedChildren: '否', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: '是否更新已经存在的用户数据', |
| | | help: '勾选后,用户账号已存在的行会更新该用户信息(不会修改其密码)', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | return [ |
| | | { |
| | | fieldName: 'username', |
| | | label: '用户名称', |
| | | label: '用户账号', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请输入用户名称', |
| | | placeholder: '请输入用户账号', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'username', |
| | | title: '用户名称', |
| | | title: '用户账号', |
| | | minWidth: 120, |
| | | }, |
| | | { |