feat: 完成员工排班模块并统一门店抽屉底部操作样式
This commit is contained in:
919
apps/web-antd/src/mock/store-staff.ts
Normal file
919
apps/web-antd/src/mock/store-staff.ts
Normal file
@@ -0,0 +1,919 @@
|
||||
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<StaffRoleType>([
|
||||
'cashier',
|
||||
'chef',
|
||||
'courier',
|
||||
'manager',
|
||||
]);
|
||||
const STATUS_VALUES = new Set<StaffStatus>(['active', 'leave', 'resigned']);
|
||||
const SHIFT_VALUES = new Set<ShiftType>(['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<string, StoreStaffState>();
|
||||
|
||||
/** 解析 URL 查询参数。 */
|
||||
function parseUrlParams(url: string) {
|
||||
const parsed = new URL(url, 'http://localhost');
|
||||
const params: Record<string, string> = {};
|
||||
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<string, unknown>;
|
||||
} 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<StaffRoleType, ShiftType[]> = {
|
||||
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<string, StaffDayShiftMock[]>();
|
||||
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<string, unknown> }).morning as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: {};
|
||||
const evening =
|
||||
typeof (record as { evening?: unknown }).evening === 'object' &&
|
||||
(record as { evening?: unknown }).evening
|
||||
? ((record as { evening: Record<string, unknown> }).evening as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: {};
|
||||
const full =
|
||||
typeof (record as { full?: unknown }).full === 'object' &&
|
||||
(record as { full?: unknown }).full
|
||||
? ((record as { full: Record<string, unknown> }).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<number, StaffDayShiftMock>();
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
for (const rawShift of input) {
|
||||
const shiftRecord =
|
||||
typeof rawShift === 'object' && rawShift
|
||||
? (rawShift as Record<string, unknown>)
|
||||
: {};
|
||||
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<string, unknown>)
|
||||
: {};
|
||||
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',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user