Commit c25135cc4127f29b2cf1a3b3ee3cb57a8ddd6414

Authored by zichun
1 parent 0001eec7

feat(usr): 用户单据页面常量与提交映射纯函数 REQ-USR-001 REQ-USR-002

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 +});
... ...