Commit c25135cc4127f29b2cf1a3b3ee3cb57a8ddd6414
1 parent
0001eec7
feat(usr): 用户单据页面常量与提交映射纯函数 REQ-USR-001 REQ-USR-002
Showing
2 changed files
with
252 additions
and
0 deletions
frontend/src/pages/usr/UserDetail/constants.ts
0 → 100644
| 1 | +// REQ-USR-001 / REQ-USR-002: 用户单据页合同级常量(枚举/默认/正则/错误码/文案 + 提交映射纯函数) | ||
| 2 | +import type { | ||
| 3 | + UserCreateReq, | ||
| 4 | + UserUpdateReq, | ||
| 5 | + UserVO, | ||
| 6 | + UserDetailMode, | ||
| 7 | +} from '../../../api/types'; | ||
| 8 | + | ||
| 9 | +// === mode 常量(由路由 :id 判定) === | ||
| 10 | +export const MODE_CREATE: UserDetailMode = 'create'; | ||
| 11 | +export const MODE_EDIT: UserDetailMode = 'edit'; | ||
| 12 | + | ||
| 13 | +// === 枚举(逐字一致,原样作为提交值,前端不映射,由后端裁决) === | ||
| 14 | +/** 用户类型枚举,create 默认「普通用户」(BR6) */ | ||
| 15 | +export const USER_TYPE_OPTIONS = ['普通用户', '超级管理员'] as const; | ||
| 16 | +/** 语言枚举(BR7,无默认强制选,create 必选) */ | ||
| 17 | +export const LANGUAGE_OPTIONS = ['中文', '英文', '繁体'] as const; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * 受控表单值(spec § 6;`tCreateDate`/`sCreator` 只读展示态另存,不在提交值内)。 | ||
| 21 | + */ | ||
| 22 | +export interface UserFormValues { | ||
| 23 | + sUserName: string; | ||
| 24 | + sUserNo: string; | ||
| 25 | + iEmployeeId: number | null; | ||
| 26 | + sUserType: string; | ||
| 27 | + sLanguage: string | undefined; | ||
| 28 | + iCanModifyBill: 0 | 1; | ||
| 29 | + iIsVoid?: 0 | 1; | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +/** create 默认表单值(BR1/BR2/BR6/BR8;sLanguage 未选触发必填校验 BR7) */ | ||
| 33 | +export const CREATE_DEFAULTS: UserFormValues = { | ||
| 34 | + sUserName: '', | ||
| 35 | + sUserNo: '', | ||
| 36 | + iEmployeeId: null, | ||
| 37 | + sUserType: '普通用户', | ||
| 38 | + sLanguage: undefined, | ||
| 39 | + iCanModifyBill: 0, | ||
| 40 | + iIsVoid: 0, | ||
| 41 | +}; | ||
| 42 | + | ||
| 43 | +/** 用户名前置校验正则(3-20 位字母数字下划线,BR3,对齐 docs/05 § REQ-USR-001) */ | ||
| 44 | +export const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,20}$/; | ||
| 45 | + | ||
| 46 | +// === 错误码常量(对齐 docs/05 § REQ-USR-001 / § REQ-USR-002 / spec § 4) === | ||
| 47 | +/** 参数校验失败 */ | ||
| 48 | +export const ERR_VALIDATION = 40001; | ||
| 49 | +/** 用户名已存在(仅 create) */ | ||
| 50 | +export const ERR_USERNAME_EXISTS = 40901; | ||
| 51 | +/** 用户不存在(仅 edit) */ | ||
| 52 | +export const ERR_USER_NOT_FOUND = 40401; | ||
| 53 | +/** 无权限 */ | ||
| 54 | +export const ERR_NO_PERMISSION = 40301; | ||
| 55 | + | ||
| 56 | +// === 静态文案(逐字一致,复刻原型 / spec) === | ||
| 57 | +export const TEXT_SAVE = '保存'; | ||
| 58 | +export const TEXT_CANCEL = '取消'; | ||
| 59 | +export const TEXT_NEW = '新增'; | ||
| 60 | +export const TEXT_DELETE = '删除'; | ||
| 61 | +export const TEXT_VOID = '作废'; | ||
| 62 | +export const TEXT_RESET_PWD = '重置密码'; | ||
| 63 | +export const TEXT_UNVOID = '取消作废'; | ||
| 64 | +export const TEXT_FUNC = '功能'; | ||
| 65 | + | ||
| 66 | +export const TEXT_CREATOR_PLACEHOLDER = '保存后自动生成'; | ||
| 67 | +export const LABEL_CREATE_TIME = '创建时间'; | ||
| 68 | +export const LABEL_CREATOR = '制单人'; | ||
| 69 | +export const LABEL_EMPLOYEE = '员工名'; | ||
| 70 | +export const LABEL_USERNAME = '用户名'; | ||
| 71 | +export const LABEL_USER_TYPE = '类型'; | ||
| 72 | +export const LABEL_LANGUAGE = '语言'; | ||
| 73 | +export const LABEL_USER_NO = '用户号'; | ||
| 74 | +export const LABEL_CAN_MODIFY_BILL = '单据修改权限'; | ||
| 75 | + | ||
| 76 | +export const TAB_PERM_GROUP = '权限组'; | ||
| 77 | +/** 5 个占位查看权限页签(D9) */ | ||
| 78 | +export const PLACEHOLDER_TABS = [ | ||
| 79 | + '客户查看权限', | ||
| 80 | + '供应商查看权限', | ||
| 81 | + '人员查看权限', | ||
| 82 | + '工序查看权限', | ||
| 83 | + '司机查看权限', | ||
| 84 | +] as const; | ||
| 85 | +export const PERM_LIST_HEADER = '权限分类'; | ||
| 86 | + | ||
| 87 | +// 校验提示 | ||
| 88 | +export const MSG_USERNAME_FORMAT = '用户名须为 3-20 位字母数字下划线'; | ||
| 89 | +export const MSG_USERNAME_REQUIRED = '请输入用户名'; | ||
| 90 | +export const MSG_USERNO_REQUIRED = '请输入用户号'; | ||
| 91 | +export const MSG_USERTYPE_REQUIRED = '请选择类型'; | ||
| 92 | +export const MSG_LANGUAGE_REQUIRED = '请选择语言'; | ||
| 93 | + | ||
| 94 | +// 成功 / 错误反馈 | ||
| 95 | +export const MSG_CREATE_SUCCESS = '用户创建成功'; | ||
| 96 | +export const MSG_EDIT_SUCCESS = '保存成功'; | ||
| 97 | +export const MSG_ERR_VALIDATION = '提交信息有误,请检查后重试'; | ||
| 98 | +export const MSG_ERR_USERNAME_EXISTS = '用户名已存在,请更换'; | ||
| 99 | +export const MSG_ERR_USER_NOT_FOUND = '该用户不存在或已被删除'; | ||
| 100 | +export const MSG_ERR_NO_PERMISSION = '无权限执行此操作'; | ||
| 101 | +export const MSG_ERR_NETWORK = '保存失败,请稍后重试'; | ||
| 102 | +export const MSG_ERR_LOAD_EMPLOYEES = '员工列表加载失败'; | ||
| 103 | +export const MSG_ERR_LOAD_PERMISSIONS = '权限列表加载失败'; | ||
| 104 | +export const MSG_LOAD_DETAIL_FAIL = '加载失败,点击重试'; | ||
| 105 | +export const MSG_CANCEL_CONFIRM = '放弃未保存的修改?'; | ||
| 106 | +export const MSG_FUNC_PLACEHOLDER = '功能开发中'; | ||
| 107 | +export const TEXT_BACK_TO_LIST = '返回列表'; | ||
| 108 | + | ||
| 109 | +// 路由 path(FE-02 已注册占位) | ||
| 110 | +export const PATH_USER_LIST = '/usr/users'; | ||
| 111 | +export const PATH_USER_NEW = '/usr/users/new'; | ||
| 112 | + | ||
| 113 | +// === 提交映射纯函数(跨 task 一致,便于单测) === | ||
| 114 | + | ||
| 115 | +/** 表单值 + 勾选权限 → UserCreateReq(无密码,BR9) */ | ||
| 116 | +export function toCreateReq(values: UserFormValues, permissionIds: number[]): UserCreateReq { | ||
| 117 | + return { | ||
| 118 | + sUserName: values.sUserName, | ||
| 119 | + sUserNo: values.sUserNo, | ||
| 120 | + iEmployeeId: values.iEmployeeId, | ||
| 121 | + sUserType: values.sUserType, | ||
| 122 | + sLanguage: values.sLanguage ?? '', | ||
| 123 | + iCanModifyBill: values.iCanModifyBill, | ||
| 124 | + permissionIds, | ||
| 125 | + }; | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +/** 表单值 + 勾选权限 → UserUpdateReq(不含 sUserName,BR3;permissionIds 全量覆盖,BR11) */ | ||
| 129 | +export function toUpdateReq(values: UserFormValues, permissionIds: number[]): UserUpdateReq { | ||
| 130 | + return { | ||
| 131 | + sUserNo: values.sUserNo, | ||
| 132 | + iEmployeeId: values.iEmployeeId, | ||
| 133 | + sUserType: values.sUserType, | ||
| 134 | + sLanguage: values.sLanguage ?? '', | ||
| 135 | + iCanModifyBill: values.iCanModifyBill, | ||
| 136 | + iIsVoid: values.iIsVoid ?? 0, | ||
| 137 | + permissionIds, | ||
| 138 | + }; | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +/** | ||
| 142 | + * edit 回填(BR17)。`UserVO`(FE-03 列表 VO)不暴露 `iCanModifyBill`, | ||
| 143 | + * 故该字段默认 0;其余基本字段按原值回填。 | ||
| 144 | + */ | ||
| 145 | +export function userVoToFormValues(vo: UserVO): UserFormValues { | ||
| 146 | + return { | ||
| 147 | + sUserName: vo.sUserName, | ||
| 148 | + sUserNo: vo.sUserNo ?? '', | ||
| 149 | + iEmployeeId: null, | ||
| 150 | + sUserType: vo.sUserType, | ||
| 151 | + sLanguage: vo.sLanguage, | ||
| 152 | + iCanModifyBill: 0, | ||
| 153 | + iIsVoid: (vo.iIsVoid === 1 ? 1 : 0) as 0 | 1, | ||
| 154 | + }; | ||
| 155 | +} |
frontend/tests/unit/userDetailMappers.test.ts
0 → 100644
| 1 | +// REQ-USR-001 / REQ-USR-002: 用户单据常量与提交映射纯函数单测(枚举/默认/正则/错误码 + toCreateReq/toUpdateReq/userVoToFormValues) | ||
| 2 | +import { describe, it, expect } from 'vitest'; | ||
| 3 | +import { | ||
| 4 | + USER_TYPE_OPTIONS, | ||
| 5 | + LANGUAGE_OPTIONS, | ||
| 6 | + CREATE_DEFAULTS, | ||
| 7 | + USERNAME_PATTERN, | ||
| 8 | + ERR_VALIDATION, | ||
| 9 | + ERR_USERNAME_EXISTS, | ||
| 10 | + ERR_USER_NOT_FOUND, | ||
| 11 | + ERR_NO_PERMISSION, | ||
| 12 | + MODE_CREATE, | ||
| 13 | + MODE_EDIT, | ||
| 14 | + toCreateReq, | ||
| 15 | + toUpdateReq, | ||
| 16 | + userVoToFormValues, | ||
| 17 | + type UserFormValues, | ||
| 18 | +} from '../../src/pages/usr/UserDetail/constants'; | ||
| 19 | +import type { UserVO } from '../../src/api/types'; | ||
| 20 | + | ||
| 21 | +function makeFormValues(over: Partial<UserFormValues> = {}): UserFormValues { | ||
| 22 | + return { | ||
| 23 | + sUserName: 'zhangsan', | ||
| 24 | + sUserNo: 'zs', | ||
| 25 | + iEmployeeId: 3, | ||
| 26 | + sUserType: '普通用户', | ||
| 27 | + sLanguage: '中文', | ||
| 28 | + iCanModifyBill: 0, | ||
| 29 | + iIsVoid: 0, | ||
| 30 | + ...over, | ||
| 31 | + }; | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +describe('用户单据常量与映射', () => { | ||
| 35 | + it('constants enums and defaults', () => { | ||
| 36 | + expect(USER_TYPE_OPTIONS).toEqual(['普通用户', '超级管理员']); | ||
| 37 | + expect(LANGUAGE_OPTIONS).toEqual(['中文', '英文', '繁体']); | ||
| 38 | + expect(CREATE_DEFAULTS.sUserType).toBe('普通用户'); | ||
| 39 | + expect(CREATE_DEFAULTS.iCanModifyBill).toBe(0); | ||
| 40 | + expect(CREATE_DEFAULTS.sLanguage).toBeUndefined(); | ||
| 41 | + expect(USERNAME_PATTERN.test('ab_12')).toBe(true); | ||
| 42 | + expect(USERNAME_PATTERN.test('ab')).toBe(false); | ||
| 43 | + expect(USERNAME_PATTERN.test('有中文')).toBe(false); | ||
| 44 | + expect(ERR_VALIDATION).toBe(40001); | ||
| 45 | + expect(ERR_USERNAME_EXISTS).toBe(40901); | ||
| 46 | + expect(ERR_USER_NOT_FOUND).toBe(40401); | ||
| 47 | + expect(ERR_NO_PERMISSION).toBe(40301); | ||
| 48 | + expect(MODE_CREATE).toBe('create'); | ||
| 49 | + expect(MODE_EDIT).toBe('edit'); | ||
| 50 | + }); | ||
| 51 | + | ||
| 52 | + it('toCreateReq maps form values + permissionIds (no password)', () => { | ||
| 53 | + const req = toCreateReq(makeFormValues({ iCanModifyBill: 1 }), [1, 2]); | ||
| 54 | + expect(req.sUserName).toBe('zhangsan'); | ||
| 55 | + expect(req.sUserNo).toBe('zs'); | ||
| 56 | + expect(req.iEmployeeId).toBe(3); | ||
| 57 | + expect(req.sUserType).toBe('普通用户'); | ||
| 58 | + expect(req.sLanguage).toBe('中文'); | ||
| 59 | + expect(req.iCanModifyBill).toBe(1); | ||
| 60 | + expect(req.permissionIds).toEqual([1, 2]); | ||
| 61 | + expect(req).not.toHaveProperty('initialPassword'); | ||
| 62 | + expect(req).not.toHaveProperty('iIsVoid'); | ||
| 63 | + }); | ||
| 64 | + | ||
| 65 | + it('toUpdateReq maps without sUserName + includes iIsVoid + full permissionIds', () => { | ||
| 66 | + const req = toUpdateReq(makeFormValues({ iIsVoid: 1 }), [2, 3]); | ||
| 67 | + expect(req).not.toHaveProperty('sUserName'); | ||
| 68 | + expect(req.iIsVoid).toBe(1); | ||
| 69 | + expect(req.permissionIds).toEqual([2, 3]); | ||
| 70 | + expect(req.sUserType).toBe('普通用户'); | ||
| 71 | + expect(req.sLanguage).toBe('中文'); | ||
| 72 | + expect(req.iCanModifyBill).toBe(0); | ||
| 73 | + }); | ||
| 74 | + | ||
| 75 | + it('userVoToFormValues fills from UserVO', () => { | ||
| 76 | + const vo: UserVO = { | ||
| 77 | + id: 7, | ||
| 78 | + sUserName: 'zhangsan', | ||
| 79 | + employeeName: '张三', | ||
| 80 | + sUserNo: 'zs', | ||
| 81 | + departmentName: null, | ||
| 82 | + sUserType: '超级管理员', | ||
| 83 | + sLanguage: '英文', | ||
| 84 | + iIsVoid: 1, | ||
| 85 | + tLastLoginDate: null, | ||
| 86 | + sCreator: 'admin', | ||
| 87 | + tCreateDate: '2026-01-01T00:00:00', | ||
| 88 | + }; | ||
| 89 | + const fv = userVoToFormValues(vo); | ||
| 90 | + expect(fv.sUserName).toBe('zhangsan'); | ||
| 91 | + expect(fv.sUserNo).toBe('zs'); | ||
| 92 | + expect(fv.sUserType).toBe('超级管理员'); | ||
| 93 | + expect(fv.sLanguage).toBe('英文'); | ||
| 94 | + expect(fv.iCanModifyBill).toBe(0); | ||
| 95 | + expect(fv.iIsVoid).toBe(1); | ||
| 96 | + }); | ||
| 97 | +}); |