import Mock from 'mockjs'; /** 文件职责:员工排班页面 Mock 接口。 */ interface MockRequestOptions { body: null | string; type: string; url: string; } type StaffRoleType = 'cashier' | 'chef' | 'courier' | 'manager'; type StaffStatus = 'active' | 'leave' | 'resigned'; type ShiftType = 'evening' | 'full' | 'morning' | 'off'; interface StoreStaffMock { avatarColor: string; email: string; hiredAt: string; id: string; name: string; permissions: string[]; phone: string; roleType: StaffRoleType; status: StaffStatus; } interface ShiftTemplateItemMock { endTime: string; startTime: string; } interface StoreShiftTemplatesMock { evening: ShiftTemplateItemMock; full: ShiftTemplateItemMock; morning: ShiftTemplateItemMock; } interface StaffDayShiftMock { dayOfWeek: number; endTime: string; shiftType: ShiftType; startTime: string; } interface StaffScheduleMock { shifts: StaffDayShiftMock[]; staffId: string; } interface StoreStaffState { schedules: StaffScheduleMock[]; staffs: StoreStaffMock[]; templates: StoreShiftTemplatesMock; weekStartDate: string; } const ROLE_VALUES = new Set([ 'cashier', 'chef', 'courier', 'manager', ]); const STATUS_VALUES = new Set(['active', 'leave', 'resigned']); const SHIFT_VALUES = new Set(['evening', 'full', 'morning', 'off']); const AVATAR_COLORS = [ '#f56a00', '#7265e6', '#52c41a', '#fa8c16', '#1890ff', '#bfbfbf', '#13c2c2', '#eb2f96', ]; const DEFAULT_TEMPLATES: StoreShiftTemplatesMock = { morning: { startTime: '09:00', endTime: '14:00', }, evening: { startTime: '14:00', endTime: '21:00', }, full: { startTime: '09:00', endTime: '21:00', }, }; const DEFAULT_STAFFS: StoreStaffMock[] = [ { id: 'staff-001', name: '张伟', phone: '13800008001', email: 'zhangwei@example.com', roleType: 'manager', status: 'active', permissions: ['全部权限'], hiredAt: '2024-01-15', avatarColor: '#f56a00', }, { id: 'staff-002', name: '李娜', phone: '13800008002', email: 'lina@example.com', roleType: 'cashier', status: 'active', permissions: ['收银', '退款'], hiredAt: '2024-03-20', avatarColor: '#7265e6', }, { id: 'staff-003', name: '王磊', phone: '13800008003', email: '', roleType: 'courier', status: 'active', permissions: ['配送管理'], hiredAt: '2024-06-01', avatarColor: '#52c41a', }, { id: 'staff-004', name: '赵敏', phone: '13800008004', email: '', roleType: 'chef', status: 'active', permissions: ['订单查看'], hiredAt: '2024-08-10', avatarColor: '#fa8c16', }, { id: 'staff-005', name: '刘洋', phone: '13800008005', email: 'liuyang@example.com', roleType: 'courier', status: 'leave', permissions: ['配送管理'], hiredAt: '2025-01-05', avatarColor: '#1890ff', }, { id: 'staff-006', name: '陈静', phone: '13800008006', email: '', roleType: 'cashier', status: 'resigned', permissions: [], hiredAt: '2024-11-20', avatarColor: '#bfbfbf', }, ]; const storeStaffMap = new Map(); /** 解析 URL 查询参数。 */ function parseUrlParams(url: string) { const parsed = new URL(url, 'http://localhost'); const params: Record = {}; parsed.searchParams.forEach((value, key) => { params[key] = value; }); return params; } /** 解析请求体 JSON。 */ function parseBody(options: MockRequestOptions) { if (!options.body) return {}; try { return JSON.parse(options.body) as Record; } catch (error) { console.error('[mock-store-staff] parseBody error:', error); return {}; } } /** 获取当前周一日期。 */ function getCurrentWeekStartDate(baseDate = new Date()) { const date = new Date(baseDate); const weekDay = date.getDay(); const diff = weekDay === 0 ? -6 : 1 - weekDay; date.setDate(date.getDate() + diff); return toDateOnly(date); } /** 日期转 yyyy-MM-dd。 */ function toDateOnly(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** 归一化 HH:mm 时间。 */ function normalizeTime(value: unknown, fallback: string) { const input = typeof value === 'string' ? value : ''; const matched = /^(\d{2}):(\d{2})$/.exec(input); if (!matched) return fallback; const hour = Number(matched[1]); const minute = Number(matched[2]); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { return fallback; } return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; } /** 归一化角色。 */ function normalizeRoleType(value: unknown, fallback: StaffRoleType) { return ROLE_VALUES.has(value as StaffRoleType) ? (value as StaffRoleType) : fallback; } /** 归一化状态。 */ function normalizeStatus(value: unknown, fallback: StaffStatus) { return STATUS_VALUES.has(value as StaffStatus) ? (value as StaffStatus) : fallback; } /** 归一化班次类型。 */ function normalizeShiftType(value: unknown, fallback: ShiftType) { return SHIFT_VALUES.has(value as ShiftType) ? (value as ShiftType) : fallback; } /** 深拷贝员工列表。 */ function cloneStaffs(source: StoreStaffMock[]) { return source.map((item) => ({ ...item, permissions: [...item.permissions], })); } /** 深拷贝模板。 */ function cloneTemplates( source: StoreShiftTemplatesMock, ): StoreShiftTemplatesMock { return { morning: { ...source.morning }, evening: { ...source.evening }, full: { ...source.full }, }; } /** 深拷贝排班。 */ function cloneSchedules(source: StaffScheduleMock[]) { return source.map((item) => ({ staffId: item.staffId, shifts: item.shifts.map((shift) => ({ ...shift })), })); } /** 按入职时间稳定排序员工。 */ function sortStaffs(source: StoreStaffMock[]) { return cloneStaffs(source).toSorted((a, b) => { const dateDiff = a.hiredAt.localeCompare(b.hiredAt); if (dateDiff !== 0) return dateDiff; return a.name.localeCompare(b.name); }); } /** 通过班次类型生成单日排班。 */ function createDayShift( dayOfWeek: number, shiftType: ShiftType, templates: StoreShiftTemplatesMock, ): StaffDayShiftMock { if (shiftType === 'off') { return { dayOfWeek, shiftType, startTime: '', endTime: '', }; } const template = templates[shiftType]; return { dayOfWeek, shiftType, startTime: template.startTime, endTime: template.endTime, }; } /** 生成默认 7 天排班。 */ function createDefaultWeekByRole( roleType: StaffRoleType, templates: StoreShiftTemplatesMock, ): StaffDayShiftMock[] { const rolePatternMap: Record = { manager: ['full', 'full', 'full', 'full', 'full', 'morning', 'off'], cashier: [ 'morning', 'morning', 'off', 'morning', 'evening', 'full', 'full', ], courier: [ 'morning', 'evening', 'morning', 'evening', 'morning', 'evening', 'off', ], chef: ['full', 'full', 'evening', 'off', 'full', 'full', 'morning'], }; const pattern = rolePatternMap[roleType] ?? rolePatternMap.cashier; return Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, pattern[dayOfWeek] ?? 'off', templates), ); } /** 创建默认门店状态。 */ function createDefaultState(): StoreStaffState { const templates = cloneTemplates(DEFAULT_TEMPLATES); const staffs = sortStaffs(cloneStaffs(DEFAULT_STAFFS)); const schedules = staffs.map((staff) => ({ staffId: staff.id, shifts: staff.status === 'resigned' ? Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', templates), ) : createDefaultWeekByRole(staff.roleType, templates), })); return { staffs, templates, schedules, weekStartDate: getCurrentWeekStartDate(), }; } /** 确保门店状态存在。 */ function ensureStoreState(storeId = '') { const key = storeId || 'default'; let state = storeStaffMap.get(key); if (!state) { state = createDefaultState(); storeStaffMap.set(key, state); } return state; } /** 构建员工排班索引。 */ function createScheduleMap(schedules: StaffScheduleMock[]) { const scheduleMap = new Map(); for (const schedule of schedules) { scheduleMap.set( schedule.staffId, schedule.shifts.map((shift) => ({ ...shift })), ); } return scheduleMap; } /** 同步模板后刷新已有排班的时间段。 */ function syncScheduleTimesWithTemplates( schedules: StaffScheduleMock[], templates: StoreShiftTemplatesMock, ) { for (const schedule of schedules) { schedule.shifts = schedule.shifts .map((shift) => { const normalizedType = normalizeShiftType(shift.shiftType, 'off'); if (normalizedType === 'off') { return { dayOfWeek: shift.dayOfWeek, shiftType: 'off' as const, startTime: '', endTime: '', }; } return { dayOfWeek: shift.dayOfWeek, shiftType: normalizedType, startTime: templates[normalizedType].startTime, endTime: templates[normalizedType].endTime, }; }) .toSorted((a, b) => a.dayOfWeek - b.dayOfWeek); } } /** 归一化模板。 */ function normalizeTemplates( input: unknown, fallback: StoreShiftTemplatesMock, ): StoreShiftTemplatesMock { const record = typeof input === 'object' && input ? input : {}; const morning = typeof (record as { morning?: unknown }).morning === 'object' && (record as { morning?: unknown }).morning ? ((record as { morning: Record }).morning as Record< string, unknown >) : {}; const evening = typeof (record as { evening?: unknown }).evening === 'object' && (record as { evening?: unknown }).evening ? ((record as { evening: Record }).evening as Record< string, unknown >) : {}; const full = typeof (record as { full?: unknown }).full === 'object' && (record as { full?: unknown }).full ? ((record as { full: Record }).full as Record< string, unknown >) : {}; return { morning: { startTime: normalizeTime(morning.startTime, fallback.morning.startTime), endTime: normalizeTime(morning.endTime, fallback.morning.endTime), }, evening: { startTime: normalizeTime(evening.startTime, fallback.evening.startTime), endTime: normalizeTime(evening.endTime, fallback.evening.endTime), }, full: { startTime: normalizeTime(full.startTime, fallback.full.startTime), endTime: normalizeTime(full.endTime, fallback.full.endTime), }, }; } /** 归一化单员工 7 天排班。 */ function normalizeShifts( input: unknown, templates: StoreShiftTemplatesMock, fallback: StaffDayShiftMock[], ): StaffDayShiftMock[] { const byDay = new Map(); if (Array.isArray(input)) { for (const rawShift of input) { const shiftRecord = typeof rawShift === 'object' && rawShift ? (rawShift as Record) : {}; const dayOfWeek = Number(shiftRecord.dayOfWeek); if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) { continue; } const shiftType = normalizeShiftType(shiftRecord.shiftType, 'off'); if (shiftType === 'off') { byDay.set(dayOfWeek, { dayOfWeek, shiftType, startTime: '', endTime: '', }); continue; } byDay.set(dayOfWeek, { dayOfWeek, shiftType, startTime: normalizeTime( shiftRecord.startTime, templates[shiftType].startTime, ), endTime: normalizeTime( shiftRecord.endTime, templates[shiftType].endTime, ), }); } } return Array.from({ length: 7 }).map((_, dayOfWeek) => { const normalized = byDay.get(dayOfWeek); if (normalized) { return normalized; } const fallbackShift = fallback.find((item) => item.dayOfWeek === dayOfWeek); if (fallbackShift) { return { ...fallbackShift }; } return createDayShift(dayOfWeek, 'off', templates); }); } /** 规范手机号格式。 */ function normalizePhone(value: unknown) { return String(value || '') .replaceAll(/\D/g, '') .slice(0, 11); } /** 规范权限列表。 */ function normalizePermissions(value: unknown, roleType: StaffRoleType) { if (!Array.isArray(value)) { return roleType === 'manager' ? ['全部权限'] : []; } const unique = [ ...new Set(value.map((item) => String(item || '').trim()).filter(Boolean)), ]; if (roleType === 'manager' && unique.length === 0) { return ['全部权限']; } return unique; } /** 生成员工 ID。 */ function createStaffId() { return `staff-${Date.now()}-${Math.floor(Math.random() * 1000)}`; } /** 生成头像颜色。 */ function resolveAvatarColor(seed: string) { let hash = 0; for (const char of seed) { hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0; } return AVATAR_COLORS[hash % AVATAR_COLORS.length] ?? '#1677ff'; } /** 获取员工列表。 */ Mock.mock(/\/store\/staff(?:\?|$)/, 'get', (options: MockRequestOptions) => { const params = parseUrlParams(options.url); const storeId = String(params.storeId || ''); const state = ensureStoreState(storeId); const keyword = String(params.keyword || '') .trim() .toLowerCase(); const roleType = params.roleType ? normalizeRoleType(params.roleType, 'cashier') : ''; const status = params.status ? normalizeStatus(params.status, 'active') : ''; const page = Math.max(1, Number(params.page) || 1); const pageSize = Math.max(1, Math.min(200, Number(params.pageSize) || 10)); const filtered = state.staffs.filter((staff) => { if (keyword) { const hitKeyword = staff.name.toLowerCase().includes(keyword) || staff.phone.includes(keyword) || staff.email.toLowerCase().includes(keyword); if (!hitKeyword) return false; } if (roleType && staff.roleType !== roleType) { return false; } if (status && staff.status !== status) { return false; } return true; }); const start = (page - 1) * pageSize; const items = filtered.slice(start, start + pageSize); return { code: 200, data: { items: cloneStaffs(items), total: filtered.length, page, pageSize, }, }; }); /** 新增 / 编辑员工。 */ Mock.mock(/\/store\/staff\/save/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String(body.storeId || ''); if (!storeId) { return { code: 200, data: null }; } const state = ensureStoreState(storeId); const id = String(body.id || '').trim(); const existingIndex = state.staffs.findIndex((item) => item.id === id); const roleType = normalizeRoleType(body.roleType, 'cashier'); const status = normalizeStatus(body.status, 'active'); const nextStaff: StoreStaffMock = { id: id || createStaffId(), name: String(body.name || '').trim(), phone: normalizePhone(body.phone), email: String(body.email || '').trim(), roleType, status, permissions: normalizePermissions(body.permissions, roleType), hiredAt: existingIndex === -1 ? toDateOnly(new Date()) : state.staffs[existingIndex]?.hiredAt || toDateOnly(new Date()), avatarColor: existingIndex === -1 ? resolveAvatarColor(String(body.name || Date.now())) : state.staffs[existingIndex]?.avatarColor || '#1677ff', }; if (!nextStaff.name) { return { code: 400, data: null, message: '员工姓名不能为空', }; } if (!nextStaff.phone || nextStaff.phone.length < 6) { return { code: 400, data: null, message: '手机号格式不正确', }; } if (existingIndex === -1) { state.staffs.push(nextStaff); state.staffs = sortStaffs(state.staffs); state.schedules.push({ staffId: nextStaff.id, shifts: status === 'resigned' ? Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', state.templates), ) : createDefaultWeekByRole(roleType, state.templates), }); } else { state.staffs[existingIndex] = nextStaff; state.staffs = sortStaffs(state.staffs); const schedule = state.schedules.find( (item) => item.staffId === nextStaff.id, ); if (schedule && nextStaff.status === 'resigned') { schedule.shifts = Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', state.templates), ); } if (!schedule) { state.schedules.push({ staffId: nextStaff.id, shifts: createDefaultWeekByRole(nextStaff.roleType, state.templates), }); } } return { code: 200, data: { ...nextStaff, permissions: [...nextStaff.permissions], }, }; }); /** 删除员工。 */ Mock.mock(/\/store\/staff\/delete/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String(body.storeId || ''); const staffId = String(body.staffId || ''); if (!storeId || !staffId) { return { code: 200, data: null }; } const state = ensureStoreState(storeId); state.staffs = state.staffs.filter((item) => item.id !== staffId); state.schedules = state.schedules.filter((item) => item.staffId !== staffId); return { code: 200, data: null, }; }); /** 获取门店排班配置。 */ Mock.mock( /\/store\/staff\/schedule(?:\?|$)/, 'get', (options: MockRequestOptions) => { const params = parseUrlParams(options.url); const storeId = String(params.storeId || ''); const state = ensureStoreState(storeId); return { code: 200, data: { storeId, templates: cloneTemplates(state.templates), schedules: cloneSchedules(state.schedules), weekStartDate: String(params.weekStartDate || state.weekStartDate), }, }; }, ); /** 保存班次模板。 */ Mock.mock( /\/store\/staff\/template\/save/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String(body.storeId || ''); if (!storeId) { return { code: 200, data: cloneTemplates(DEFAULT_TEMPLATES) }; } const state = ensureStoreState(storeId); state.templates = normalizeTemplates(body.templates, state.templates); syncScheduleTimesWithTemplates(state.schedules, state.templates); return { code: 200, data: cloneTemplates(state.templates), }; }, ); /** 保存员工个人排班。 */ Mock.mock( /\/store\/staff\/schedule\/personal\/save/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String(body.storeId || ''); const staffId = String(body.staffId || ''); if (!storeId || !staffId) { return { code: 200, data: null }; } const state = ensureStoreState(storeId); const staff = state.staffs.find((item) => item.id === staffId); if (!staff) { return { code: 400, data: null, message: '员工不存在', }; } const schedule = state.schedules.find((item) => item.staffId === staffId); const fallbackShifts = schedule?.shifts ?? createDefaultWeekByRole(staff.roleType, state.templates); const nextShifts = staff.status === 'resigned' ? Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', state.templates), ) : normalizeShifts(body.shifts, state.templates, fallbackShifts); if (schedule) { schedule.shifts = nextShifts; } else { state.schedules.push({ staffId, shifts: nextShifts, }); } return { code: 200, data: { staffId, shifts: nextShifts.map((item) => ({ ...item })), }, }; }, ); /** 保存周排班。 */ Mock.mock( /\/store\/staff\/schedule\/weekly\/save/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String(body.storeId || ''); if (!storeId) { return { code: 200, data: null }; } const state = ensureStoreState(storeId); const incomingList = Array.isArray(body.schedules) ? body.schedules : []; const scheduleMap = createScheduleMap(state.schedules); for (const incoming of incomingList) { const record = typeof incoming === 'object' && incoming ? (incoming as Record) : {}; const staffId = String(record.staffId || ''); if (!staffId) continue; const staff = state.staffs.find((item) => item.id === staffId); if (!staff || staff.status === 'resigned') continue; const fallbackShifts = scheduleMap.get(staffId) ?? createDefaultWeekByRole(staff.roleType, state.templates); const nextShifts = normalizeShifts( record.shifts, state.templates, fallbackShifts, ); scheduleMap.set(staffId, nextShifts); } state.schedules = state.staffs.map((staff) => { const baseShifts = scheduleMap.get(staff.id) ?? createDefaultWeekByRole(staff.roleType, state.templates); return { staffId: staff.id, shifts: staff.status === 'resigned' ? Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', state.templates), ) : baseShifts.map((shift) => ({ ...shift })), }; }); return { code: 200, data: { storeId, templates: cloneTemplates(state.templates), schedules: cloneSchedules(state.schedules), weekStartDate: state.weekStartDate, }, }; }, ); /** 复制门店模板与排班。 */ Mock.mock(/\/store\/staff\/copy/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const sourceStoreId = String(body.sourceStoreId || ''); const copyScope = String(body.copyScope || ''); const targetStoreIds = Array.isArray(body.targetStoreIds) ? body.targetStoreIds.map(String).filter(Boolean) : []; if ( !sourceStoreId || copyScope !== 'template_and_schedule' || targetStoreIds.length === 0 ) { return { code: 200, data: { copiedCount: 0 }, }; } const sourceState = ensureStoreState(sourceStoreId); const sourceScheduleMap = createScheduleMap(sourceState.schedules); const uniqueTargets = [...new Set(targetStoreIds)].filter( (id) => id !== sourceStoreId, ); for (const targetStoreId of uniqueTargets) { const targetState = ensureStoreState(targetStoreId); targetState.templates = cloneTemplates(sourceState.templates); targetState.schedules = targetState.staffs.map((staff) => { const targetShiftFallback = sourceScheduleMap.get(staff.id) ?? createDefaultWeekByRole(staff.roleType, targetState.templates); return { staffId: staff.id, shifts: staff.status === 'resigned' ? Array.from({ length: 7 }).map((_, dayOfWeek) => createDayShift(dayOfWeek, 'off', targetState.templates), ) : normalizeShifts( targetShiftFallback, targetState.templates, targetShiftFallback, ), }; }); } return { code: 200, data: { copiedCount: uniqueTargets.length, copyScope: 'template_and_schedule', }, }; });