diff --git a/frontend/src/pages/usr/UserDetail/constants.ts b/frontend/src/pages/usr/UserDetail/constants.ts new file mode 100644 index 0000000..26fb820 --- /dev/null +++ b/frontend/src/pages/usr/UserDetail/constants.ts @@ -0,0 +1,155 @@ +// REQ-USR-001 / REQ-USR-002: 用户单据页合同级常量(枚举/默认/正则/错误码/文案 + 提交映射纯函数) +import type { + UserCreateReq, + UserUpdateReq, + UserVO, + UserDetailMode, +} from '../../../api/types'; + +// === mode 常量(由路由 :id 判定) === +export const MODE_CREATE: UserDetailMode = 'create'; +export const MODE_EDIT: UserDetailMode = 'edit'; + +// === 枚举(逐字一致,原样作为提交值,前端不映射,由后端裁决) === +/** 用户类型枚举,create 默认「普通用户」(BR6) */ +export const USER_TYPE_OPTIONS = ['普通用户', '超级管理员'] as const; +/** 语言枚举(BR7,无默认强制选,create 必选) */ +export const LANGUAGE_OPTIONS = ['中文', '英文', '繁体'] as const; + +/** + * 受控表单值(spec § 6;`tCreateDate`/`sCreator` 只读展示态另存,不在提交值内)。 + */ +export interface UserFormValues { + sUserName: string; + sUserNo: string; + iEmployeeId: number | null; + sUserType: string; + sLanguage: string | undefined; + iCanModifyBill: 0 | 1; + iIsVoid?: 0 | 1; +} + +/** create 默认表单值(BR1/BR2/BR6/BR8;sLanguage 未选触发必填校验 BR7) */ +export const CREATE_DEFAULTS: UserFormValues = { + sUserName: '', + sUserNo: '', + iEmployeeId: null, + sUserType: '普通用户', + sLanguage: undefined, + iCanModifyBill: 0, + iIsVoid: 0, +}; + +/** 用户名前置校验正则(3-20 位字母数字下划线,BR3,对齐 docs/05 § REQ-USR-001) */ +export const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,20}$/; + +// === 错误码常量(对齐 docs/05 § REQ-USR-001 / § REQ-USR-002 / spec § 4) === +/** 参数校验失败 */ +export const ERR_VALIDATION = 40001; +/** 用户名已存在(仅 create) */ +export const ERR_USERNAME_EXISTS = 40901; +/** 用户不存在(仅 edit) */ +export const ERR_USER_NOT_FOUND = 40401; +/** 无权限 */ +export const ERR_NO_PERMISSION = 40301; + +// === 静态文案(逐字一致,复刻原型 / spec) === +export const TEXT_SAVE = '保存'; +export const TEXT_CANCEL = '取消'; +export const TEXT_NEW = '新增'; +export const TEXT_DELETE = '删除'; +export const TEXT_VOID = '作废'; +export const TEXT_RESET_PWD = '重置密码'; +export const TEXT_UNVOID = '取消作废'; +export const TEXT_FUNC = '功能'; + +export const TEXT_CREATOR_PLACEHOLDER = '保存后自动生成'; +export const LABEL_CREATE_TIME = '创建时间'; +export const LABEL_CREATOR = '制单人'; +export const LABEL_EMPLOYEE = '员工名'; +export const LABEL_USERNAME = '用户名'; +export const LABEL_USER_TYPE = '类型'; +export const LABEL_LANGUAGE = '语言'; +export const LABEL_USER_NO = '用户号'; +export const LABEL_CAN_MODIFY_BILL = '单据修改权限'; + +export const TAB_PERM_GROUP = '权限组'; +/** 5 个占位查看权限页签(D9) */ +export const PLACEHOLDER_TABS = [ + '客户查看权限', + '供应商查看权限', + '人员查看权限', + '工序查看权限', + '司机查看权限', +] as const; +export const PERM_LIST_HEADER = '权限分类'; + +// 校验提示 +export const MSG_USERNAME_FORMAT = '用户名须为 3-20 位字母数字下划线'; +export const MSG_USERNAME_REQUIRED = '请输入用户名'; +export const MSG_USERNO_REQUIRED = '请输入用户号'; +export const MSG_USERTYPE_REQUIRED = '请选择类型'; +export const MSG_LANGUAGE_REQUIRED = '请选择语言'; + +// 成功 / 错误反馈 +export const MSG_CREATE_SUCCESS = '用户创建成功'; +export const MSG_EDIT_SUCCESS = '保存成功'; +export const MSG_ERR_VALIDATION = '提交信息有误,请检查后重试'; +export const MSG_ERR_USERNAME_EXISTS = '用户名已存在,请更换'; +export const MSG_ERR_USER_NOT_FOUND = '该用户不存在或已被删除'; +export const MSG_ERR_NO_PERMISSION = '无权限执行此操作'; +export const MSG_ERR_NETWORK = '保存失败,请稍后重试'; +export const MSG_ERR_LOAD_EMPLOYEES = '员工列表加载失败'; +export const MSG_ERR_LOAD_PERMISSIONS = '权限列表加载失败'; +export const MSG_LOAD_DETAIL_FAIL = '加载失败,点击重试'; +export const MSG_CANCEL_CONFIRM = '放弃未保存的修改?'; +export const MSG_FUNC_PLACEHOLDER = '功能开发中'; +export const TEXT_BACK_TO_LIST = '返回列表'; + +// 路由 path(FE-02 已注册占位) +export const PATH_USER_LIST = '/usr/users'; +export const PATH_USER_NEW = '/usr/users/new'; + +// === 提交映射纯函数(跨 task 一致,便于单测) === + +/** 表单值 + 勾选权限 → UserCreateReq(无密码,BR9) */ +export function toCreateReq(values: UserFormValues, permissionIds: number[]): UserCreateReq { + return { + sUserName: values.sUserName, + sUserNo: values.sUserNo, + iEmployeeId: values.iEmployeeId, + sUserType: values.sUserType, + sLanguage: values.sLanguage ?? '', + iCanModifyBill: values.iCanModifyBill, + permissionIds, + }; +} + +/** 表单值 + 勾选权限 → UserUpdateReq(不含 sUserName,BR3;permissionIds 全量覆盖,BR11) */ +export function toUpdateReq(values: UserFormValues, permissionIds: number[]): UserUpdateReq { + return { + sUserNo: values.sUserNo, + iEmployeeId: values.iEmployeeId, + sUserType: values.sUserType, + sLanguage: values.sLanguage ?? '', + iCanModifyBill: values.iCanModifyBill, + iIsVoid: values.iIsVoid ?? 0, + permissionIds, + }; +} + +/** + * edit 回填(BR17)。`UserVO`(FE-03 列表 VO)不暴露 `iCanModifyBill`, + * 故该字段默认 0;其余基本字段按原值回填。 + */ +export function userVoToFormValues(vo: UserVO): UserFormValues { + return { + sUserName: vo.sUserName, + sUserNo: vo.sUserNo ?? '', + iEmployeeId: null, + sUserType: vo.sUserType, + sLanguage: vo.sLanguage, + iCanModifyBill: 0, + iIsVoid: (vo.iIsVoid === 1 ? 1 : 0) as 0 | 1, + }; +} diff --git a/frontend/tests/unit/userDetailMappers.test.ts b/frontend/tests/unit/userDetailMappers.test.ts new file mode 100644 index 0000000..285f432 --- /dev/null +++ b/frontend/tests/unit/userDetailMappers.test.ts @@ -0,0 +1,97 @@ +// REQ-USR-001 / REQ-USR-002: 用户单据常量与提交映射纯函数单测(枚举/默认/正则/错误码 + toCreateReq/toUpdateReq/userVoToFormValues) +import { describe, it, expect } from 'vitest'; +import { + USER_TYPE_OPTIONS, + LANGUAGE_OPTIONS, + CREATE_DEFAULTS, + USERNAME_PATTERN, + ERR_VALIDATION, + ERR_USERNAME_EXISTS, + ERR_USER_NOT_FOUND, + ERR_NO_PERMISSION, + MODE_CREATE, + MODE_EDIT, + toCreateReq, + toUpdateReq, + userVoToFormValues, + type UserFormValues, +} from '../../src/pages/usr/UserDetail/constants'; +import type { UserVO } from '../../src/api/types'; + +function makeFormValues(over: Partial = {}): UserFormValues { + return { + sUserName: 'zhangsan', + sUserNo: 'zs', + iEmployeeId: 3, + sUserType: '普通用户', + sLanguage: '中文', + iCanModifyBill: 0, + iIsVoid: 0, + ...over, + }; +} + +describe('用户单据常量与映射', () => { + it('constants enums and defaults', () => { + expect(USER_TYPE_OPTIONS).toEqual(['普通用户', '超级管理员']); + expect(LANGUAGE_OPTIONS).toEqual(['中文', '英文', '繁体']); + expect(CREATE_DEFAULTS.sUserType).toBe('普通用户'); + expect(CREATE_DEFAULTS.iCanModifyBill).toBe(0); + expect(CREATE_DEFAULTS.sLanguage).toBeUndefined(); + expect(USERNAME_PATTERN.test('ab_12')).toBe(true); + expect(USERNAME_PATTERN.test('ab')).toBe(false); + expect(USERNAME_PATTERN.test('有中文')).toBe(false); + expect(ERR_VALIDATION).toBe(40001); + expect(ERR_USERNAME_EXISTS).toBe(40901); + expect(ERR_USER_NOT_FOUND).toBe(40401); + expect(ERR_NO_PERMISSION).toBe(40301); + expect(MODE_CREATE).toBe('create'); + expect(MODE_EDIT).toBe('edit'); + }); + + it('toCreateReq maps form values + permissionIds (no password)', () => { + const req = toCreateReq(makeFormValues({ iCanModifyBill: 1 }), [1, 2]); + expect(req.sUserName).toBe('zhangsan'); + expect(req.sUserNo).toBe('zs'); + expect(req.iEmployeeId).toBe(3); + expect(req.sUserType).toBe('普通用户'); + expect(req.sLanguage).toBe('中文'); + expect(req.iCanModifyBill).toBe(1); + expect(req.permissionIds).toEqual([1, 2]); + expect(req).not.toHaveProperty('initialPassword'); + expect(req).not.toHaveProperty('iIsVoid'); + }); + + it('toUpdateReq maps without sUserName + includes iIsVoid + full permissionIds', () => { + const req = toUpdateReq(makeFormValues({ iIsVoid: 1 }), [2, 3]); + expect(req).not.toHaveProperty('sUserName'); + expect(req.iIsVoid).toBe(1); + expect(req.permissionIds).toEqual([2, 3]); + expect(req.sUserType).toBe('普通用户'); + expect(req.sLanguage).toBe('中文'); + expect(req.iCanModifyBill).toBe(0); + }); + + it('userVoToFormValues fills from UserVO', () => { + const vo: UserVO = { + id: 7, + sUserName: 'zhangsan', + employeeName: '张三', + sUserNo: 'zs', + departmentName: null, + sUserType: '超级管理员', + sLanguage: '英文', + iIsVoid: 1, + tLastLoginDate: null, + sCreator: 'admin', + tCreateDate: '2026-01-01T00:00:00', + }; + const fv = userVoToFormValues(vo); + expect(fv.sUserName).toBe('zhangsan'); + expect(fv.sUserNo).toBe('zs'); + expect(fv.sUserType).toBe('超级管理员'); + expect(fv.sLanguage).toBe('英文'); + expect(fv.iCanModifyBill).toBe(0); + expect(fv.iIsVoid).toBe(1); + }); +});