fix: 员工排班接入真实数据并移除兜底

This commit is contained in:
2026-02-20 09:03:23 +08:00
parent fc1739a00d
commit 11cd789f38
14 changed files with 298 additions and 196 deletions

View File

@@ -58,9 +58,11 @@ export interface StaffScheduleDto {
/** 门店排班聚合 */ /** 门店排班聚合 */
export interface StoreStaffScheduleDto { export interface StoreStaffScheduleDto {
isScheduleConfigured: boolean;
isTemplateConfigured: boolean;
schedules: StaffScheduleDto[]; schedules: StaffScheduleDto[];
storeId: string; storeId: string;
templates: StoreShiftTemplatesDto; templates: null | StoreShiftTemplatesDto;
weekStartDate: string; weekStartDate: string;
} }

View File

@@ -13,6 +13,7 @@ import dayjs from 'dayjs';
interface Props { interface Props {
isSaving: boolean; isSaving: boolean;
isTemplateConfigured: boolean;
onSetTemplateTime: (payload: { onSetTemplateTime: (payload: {
field: 'endTime' | 'startTime'; field: 'endTime' | 'startTime';
shiftType: Exclude<ShiftType, 'off'>; shiftType: Exclude<ShiftType, 'off'>;
@@ -71,6 +72,10 @@ function handleTemplateTimeChange(payload: {
<span class="section-title">班次模板</span> <span class="section-title">班次模板</span>
</template> </template>
<div v-if="!props.isTemplateConfigured" class="template-guide">
当前门店尚未配置班次模板请先设置并保存模板再进行员工排班
</div>
<div class="template-list"> <div class="template-list">
<div <div
v-for="row in templateRows" v-for="row in templateRows"
@@ -113,7 +118,11 @@ function handleTemplateTimeChange(payload: {
</div> </div>
<div class="template-tip"> <div class="template-tip">
调整模板后个人排班和周排班中的同类型班次会同步到新的时间段 {{
props.isTemplateConfigured
? '调整模板后,个人排班和周排班中的同类型班次会同步到新的时间段。'
: '模板保存成功后,员工列表中的“排班”和“编辑排班”会自动可用。'
}}
</div> </div>
<div class="staff-card-actions"> <div class="staff-card-actions">

View File

@@ -40,6 +40,12 @@ function getDayShift(dayOfWeek: number) {
return props.form.shifts.find((item) => item.dayOfWeek === dayOfWeek); return props.form.shifts.find((item) => item.dayOfWeek === dayOfWeek);
} }
/** 判断某日是否展示时间编辑。 */
function isTimeHidden(dayOfWeek: number) {
const shift = getDayShift(dayOfWeek);
return !shift || shift.shiftType === 'off';
}
/** 转换时间组件值。 */ /** 转换时间组件值。 */
function toPickerValue(time: string) { function toPickerValue(time: string) {
if (!time) return undefined; if (!time) return undefined;
@@ -65,7 +71,7 @@ function toTimeText(value: Dayjs | null | string | undefined) {
@update:open="(value) => emit('update:open', value)" @update:open="(value) => emit('update:open', value)"
> >
<div class="staff-schedule-hint"> <div class="staff-schedule-hint">
点击班次胶囊可自动填充时间选择休息后该日不排班 点击班次胶囊可自动填充时间显示未配置表示该日尚未设置排班
</div> </div>
<div class="staff-schedule-list"> <div class="staff-schedule-list">
@@ -94,14 +100,19 @@ function toTimeText(value: Dayjs | null | string | undefined) {
</button> </button>
</div> </div>
<span v-if="!getDayShift(day.dayOfWeek)" class="staff-schedule-empty">
未配置
</span>
<div <div
class="staff-schedule-time" class="staff-schedule-time"
:class="{ hidden: getDayShift(day.dayOfWeek)?.shiftType === 'off' }" :class="{ hidden: isTimeHidden(day.dayOfWeek) }"
> >
<TimePicker <TimePicker
:value="toPickerValue(getDayShift(day.dayOfWeek)?.startTime || '')" :value="toPickerValue(getDayShift(day.dayOfWeek)?.startTime || '')"
format="HH:mm" format="HH:mm"
:allow-clear="false" :allow-clear="false"
:disabled="isTimeHidden(day.dayOfWeek)"
@update:value=" @update:value="
(value) => props.onSetShiftStart(day.dayOfWeek, toTimeText(value)) (value) => props.onSetShiftStart(day.dayOfWeek, toTimeText(value))
" "
@@ -111,6 +122,7 @@ function toTimeText(value: Dayjs | null | string | undefined) {
:value="toPickerValue(getDayShift(day.dayOfWeek)?.endTime || '')" :value="toPickerValue(getDayShift(day.dayOfWeek)?.endTime || '')"
format="HH:mm" format="HH:mm"
:allow-clear="false" :allow-clear="false"
:disabled="isTimeHidden(day.dayOfWeek)"
@update:value=" @update:value="
(value) => props.onSetShiftEnd(day.dayOfWeek, toTimeText(value)) (value) => props.onSetShiftEnd(day.dayOfWeek, toTimeText(value))
" "

View File

@@ -18,6 +18,7 @@ interface Props {
getShiftClass: (shiftType: ShiftType) => string; getShiftClass: (shiftType: ShiftType) => string;
getShiftLabel: (shiftType: ShiftType) => string; getShiftLabel: (shiftType: ShiftType) => string;
getShiftTimeText: (shift: StaffDayShiftDto) => string; getShiftTimeText: (shift: StaffDayShiftDto) => string;
isTemplateConfigured: boolean;
isSaving: boolean; isSaving: boolean;
legendItems: ShiftLegendItem[]; legendItems: ShiftLegendItem[];
open: boolean; open: boolean;
@@ -33,19 +34,30 @@ const emit = defineEmits<{
(event: 'update:open', value: boolean): void; (event: 'update:open', value: boolean): void;
}>(); }>();
/** 获取某行某天班次,缺失时兜底为休息。 */ /** 获取某行某天班次,缺失时返回未配置。 */
function resolveDayShift( function resolveDayShift(
row: WeekEditorRow, row: WeekEditorRow,
dayOfWeek: number, dayOfWeek: number,
): StaffDayShiftDto { ): StaffDayShiftDto | undefined {
return ( return row.shifts.find((item) => item.dayOfWeek === dayOfWeek);
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ?? { }
dayOfWeek,
shiftType: 'off', /** 获取单元格样式。 */
startTime: '', function resolveCellClass(shift: StaffDayShiftDto | undefined) {
endTime: '', if (!shift) return 'shift-unconfigured';
} return props.getShiftClass(shift.shiftType);
); }
/** 获取单元格班次文案。 */
function resolveCellLabel(shift: StaffDayShiftDto | undefined) {
if (!shift) return '未配置';
return props.getShiftLabel(shift.shiftType);
}
/** 获取单元格时间文案。 */
function resolveCellTimeText(shift: StaffDayShiftDto | undefined) {
if (!shift) return '未配置';
return props.getShiftTimeText(shift);
} }
</script> </script>
@@ -74,7 +86,11 @@ function resolveDayShift(
</span> </span>
</div> </div>
<div v-if="props.rows.length === 0" class="staff-week-empty"> <div v-if="!props.isTemplateConfigured" class="staff-week-empty">
<Empty description="未配置班次模板,请先配置模板后再编辑周排班" />
</div>
<div v-else-if="props.rows.length === 0" class="staff-week-empty">
<Empty description="暂无可编辑排班" /> <Empty description="暂无可编辑排班" />
</div> </div>
@@ -105,11 +121,7 @@ function resolveDayShift(
<button <button
type="button" type="button"
class="staff-week-cell" class="staff-week-cell"
:class=" :class="resolveCellClass(resolveDayShift(row, day.dayOfWeek))"
props.getShiftClass(
resolveDayShift(row, day.dayOfWeek).shiftType,
)
"
@click=" @click="
emit('cycleShift', { emit('cycleShift', {
staffId: row.staffId, staffId: row.staffId,
@@ -118,16 +130,10 @@ function resolveDayShift(
" "
> >
<span class="staff-week-cell-label"> <span class="staff-week-cell-label">
{{ {{ resolveCellLabel(resolveDayShift(row, day.dayOfWeek)) }}
props.getShiftLabel(
resolveDayShift(row, day.dayOfWeek).shiftType,
)
}}
</span> </span>
<span class="staff-week-cell-time"> <span class="staff-week-cell-time">
{{ {{ resolveCellTimeText(resolveDayShift(row, day.dayOfWeek)) }}
props.getShiftTimeText(resolveDayShift(row, day.dayOfWeek))
}}
</span> </span>
</button> </button>
</td> </td>
@@ -141,6 +147,7 @@ function resolveDayShift(
<Button @click="emit('update:open', false)">取消</Button> <Button @click="emit('update:open', false)">取消</Button>
<Button <Button
type="primary" type="primary"
:disabled="!props.isTemplateConfigured"
:loading="props.isSaving" :loading="props.isSaving"
@click="emit('submit')" @click="emit('submit')"
> >

View File

@@ -14,6 +14,8 @@ interface Props {
getShiftClass: (shiftType: ShiftType) => string; getShiftClass: (shiftType: ShiftType) => string;
getShiftLabel: (shiftType: ShiftType) => string; getShiftLabel: (shiftType: ShiftType) => string;
getShiftTimeText: (shift: StaffDayShiftDto) => string; getShiftTimeText: (shift: StaffDayShiftDto) => string;
isScheduleConfigured: boolean;
isTemplateConfigured: boolean;
roleLabelResolver: (roleType: WeekEditorRow['roleType']) => string; roleLabelResolver: (roleType: WeekEditorRow['roleType']) => string;
rows: WeekEditorRow[]; rows: WeekEditorRow[];
} }
@@ -24,19 +26,30 @@ const emit = defineEmits<{
(event: 'editWeek'): void; (event: 'editWeek'): void;
}>(); }>();
/** 读取员工某日排班,缺失时兜底为休息。 */ /** 读取员工某日排班,缺失时返回未配置。 */
function resolveDayShift( function resolveDayShift(
row: WeekEditorRow, row: WeekEditorRow,
dayOfWeek: number, dayOfWeek: number,
): StaffDayShiftDto { ): StaffDayShiftDto | undefined {
return ( return row.shifts.find((item) => item.dayOfWeek === dayOfWeek);
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ?? { }
dayOfWeek,
shiftType: 'off', /** 读取某单元格样式。 */
startTime: '', function resolveCellClass(shift: StaffDayShiftDto | undefined) {
endTime: '', if (!shift) return 'shift-unconfigured';
} return props.getShiftClass(shift.shiftType);
); }
/** 读取某单元格时间文案。 */
function resolveCellTimeText(shift: StaffDayShiftDto | undefined) {
if (!shift) return '未配置';
return props.getShiftTimeText(shift);
}
/** 读取某单元格班次文案。 */
function resolveCellLabel(shift: StaffDayShiftDto | undefined) {
if (!shift) return '未配置';
return props.getShiftLabel(shift.shiftType);
} }
</script> </script>
@@ -46,14 +59,28 @@ function resolveDayShift(
<span class="section-title">本周排班</span> <span class="section-title">本周排班</span>
</template> </template>
<template #extra> <template #extra>
<Button @click="emit('editWeek')">编辑排班</Button> <Button
:disabled="!props.isTemplateConfigured"
:title="props.isTemplateConfigured ? '' : '请先配置班次模板'"
@click="emit('editWeek')"
>
编辑排班
</Button>
</template> </template>
<div v-if="props.rows.length === 0" class="schedule-empty"> <div v-if="!props.isTemplateConfigured" class="schedule-empty">
<Empty description="未配置班次模板,请先在上方完成模板设置" />
</div>
<div v-else-if="props.rows.length === 0" class="schedule-empty">
<Empty description="暂无可排班员工" /> <Empty description="暂无可排班员工" />
</div> </div>
<div v-else class="schedule-board-wrap"> <div v-else class="schedule-board-wrap">
<div v-if="!props.isScheduleConfigured" class="schedule-board-tip">
当前门店还没有排班数据点击编辑排班开始配置
</div>
<table class="schedule-board-table"> <table class="schedule-board-table">
<thead> <thead>
<tr> <tr>
@@ -78,23 +105,13 @@ function resolveDayShift(
> >
<div <div
class="schedule-cell" class="schedule-cell"
:class=" :class="resolveCellClass(resolveDayShift(row, day.dayOfWeek))"
props.getShiftClass(
resolveDayShift(row, day.dayOfWeek).shiftType,
)
"
> >
<span class="schedule-cell-time"> <span class="schedule-cell-time">
{{ {{ resolveCellTimeText(resolveDayShift(row, day.dayOfWeek)) }}
props.getShiftTimeText(resolveDayShift(row, day.dayOfWeek))
}}
</span> </span>
<span class="schedule-cell-label"> <span class="schedule-cell-label">
{{ {{ resolveCellLabel(resolveDayShift(row, day.dayOfWeek)) }}
props.getShiftLabel(
resolveDayShift(row, day.dayOfWeek).shiftType,
)
}}
</span> </span>
</div> </div>
</td> </td>

View File

@@ -99,18 +99,18 @@ export const STAFF_PERMISSION_OPTIONS: StaffPermissionOption[] = [
{ value: '数据统计', label: '数据统计' }, { value: '数据统计', label: '数据统计' },
]; ];
export const DEFAULT_SHIFT_TEMPLATES: StoreShiftTemplatesDto = { export const EMPTY_SHIFT_TEMPLATES: StoreShiftTemplatesDto = {
morning: { morning: {
startTime: '09:00', startTime: '',
endTime: '14:00', endTime: '',
}, },
evening: { evening: {
startTime: '14:00', startTime: '',
endTime: '21:00', endTime: '',
}, },
full: { full: {
startTime: '09:00', startTime: '',
endTime: '21:00', endTime: '',
}, },
}; };

View File

@@ -22,16 +22,18 @@ import {
getStoreStaffScheduleApi, getStoreStaffScheduleApi,
} from '#/api/store-staff'; } from '#/api/store-staff';
import { EMPTY_SHIFT_TEMPLATES } from './constants';
import { import {
cloneScheduleMap, cloneScheduleMap,
cloneShifts, cloneShifts,
cloneTemplates, cloneTemplates,
createEmptyWeekShifts, normalizeSparseWeekShifts,
normalizeWeekShifts,
} from './helpers'; } from './helpers';
interface CreateDataActionsOptions { interface CreateDataActionsOptions {
filters: StaffFilterState; filters: StaffFilterState;
isScheduleConfigured: Ref<boolean>;
isTemplateConfigured: Ref<boolean>;
isPageLoading: Ref<boolean>; isPageLoading: Ref<boolean>;
isStoreLoading: Ref<boolean>; isStoreLoading: Ref<boolean>;
scheduleMap: Ref<Record<string, StaffDayShiftDto[]>>; scheduleMap: Ref<Record<string, StaffDayShiftDto[]>>;
@@ -45,11 +47,21 @@ interface CreateDataActionsOptions {
} }
export function createDataActions(options: CreateDataActionsOptions) { export function createDataActions(options: CreateDataActionsOptions) {
/** 判断当前排班映射是否包含任意配置。 */
function hasAnyConfiguredShift(
scheduleMap: Record<string, StaffDayShiftDto[]>,
) {
return Object.values(scheduleMap).some((shifts) => shifts.length > 0);
}
/** 清空当前门店相关数据。 */ /** 清空当前门店相关数据。 */
function clearStoreData() { function clearStoreData() {
options.staffRows.value = []; options.staffRows.value = [];
options.staffDirectory.value = []; options.staffDirectory.value = [];
options.staffTotal.value = 0; options.staffTotal.value = 0;
options.templates.value = cloneTemplates(EMPTY_SHIFT_TEMPLATES);
options.isTemplateConfigured.value = false;
options.isScheduleConfigured.value = false;
options.scheduleMap.value = {}; options.scheduleMap.value = {};
options.scheduleSnapshot.value = null; options.scheduleSnapshot.value = null;
} }
@@ -97,6 +109,9 @@ export function createDataActions(options: CreateDataActionsOptions) {
/** 加载班次模板与排班配置。 */ /** 加载班次模板与排班配置。 */
async function loadScheduleSettings() { async function loadScheduleSettings() {
if (!options.selectedStoreId.value) { if (!options.selectedStoreId.value) {
options.templates.value = cloneTemplates(EMPTY_SHIFT_TEMPLATES);
options.isTemplateConfigured.value = false;
options.isScheduleConfigured.value = false;
options.scheduleMap.value = {}; options.scheduleMap.value = {};
options.scheduleSnapshot.value = null; options.scheduleSnapshot.value = null;
return; return;
@@ -105,18 +120,19 @@ export function createDataActions(options: CreateDataActionsOptions) {
const result = await getStoreStaffScheduleApi( const result = await getStoreStaffScheduleApi(
options.selectedStoreId.value, options.selectedStoreId.value,
); );
const templates = cloneTemplates(result.templates); const templates = cloneTemplates(result.templates ?? EMPTY_SHIFT_TEMPLATES);
const scheduleMap: Record<string, StaffDayShiftDto[]> = {}; const scheduleMap: Record<string, StaffDayShiftDto[]> = {};
for (const schedule of result.schedules ?? []) { for (const schedule of result.schedules ?? []) {
scheduleMap[schedule.staffId] = normalizeWeekShifts({ scheduleMap[schedule.staffId] = normalizeSparseWeekShifts({
shifts: schedule.shifts, shifts: schedule.shifts,
fallback: createEmptyWeekShifts(templates),
templates, templates,
}); });
} }
options.templates.value = templates; options.templates.value = templates;
options.isTemplateConfigured.value = Boolean(result.isTemplateConfigured);
options.isScheduleConfigured.value = Boolean(result.isScheduleConfigured);
options.scheduleMap.value = scheduleMap; options.scheduleMap.value = scheduleMap;
options.scheduleSnapshot.value = { options.scheduleSnapshot.value = {
templates: cloneTemplates(templates), templates: cloneTemplates(templates),
@@ -195,6 +211,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
[staffId]: cloneShifts(shifts), [staffId]: cloneShifts(shifts),
}; };
options.scheduleMap.value = nextMap; options.scheduleMap.value = nextMap;
options.isScheduleConfigured.value = hasAnyConfiguredShift(nextMap);
options.scheduleSnapshot.value = { options.scheduleSnapshot.value = {
templates: cloneTemplates(options.templates.value), templates: cloneTemplates(options.templates.value),
scheduleMap: cloneScheduleMap(nextMap), scheduleMap: cloneScheduleMap(nextMap),
@@ -204,6 +221,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
/** 批量更新排班映射。 */ /** 批量更新排班映射。 */
function patchScheduleMap(nextMap: Record<string, StaffDayShiftDto[]>) { function patchScheduleMap(nextMap: Record<string, StaffDayShiftDto[]>) {
options.scheduleMap.value = cloneScheduleMap(nextMap); options.scheduleMap.value = cloneScheduleMap(nextMap);
options.isScheduleConfigured.value = hasAnyConfiguredShift(nextMap);
options.scheduleSnapshot.value = { options.scheduleSnapshot.value = {
templates: cloneTemplates(options.templates.value), templates: cloneTemplates(options.templates.value),
scheduleMap: cloneScheduleMap(nextMap), scheduleMap: cloneScheduleMap(nextMap),

View File

@@ -6,7 +6,6 @@
import type { import type {
ShiftType, ShiftType,
StaffDayShiftDto, StaffDayShiftDto,
StaffScheduleDto,
StoreShiftTemplatesDto, StoreShiftTemplatesDto,
} from '#/api/store-staff'; } from '#/api/store-staff';
import type { import type {
@@ -15,8 +14,6 @@ import type {
WeekEditorRow, WeekEditorRow,
} from '#/views/store/staff/types'; } from '#/views/store/staff/types';
import { DAY_OPTIONS } from './constants';
/** 深拷贝模板。 */ /** 深拷贝模板。 */
export function cloneTemplates( export function cloneTemplates(
source: StoreShiftTemplatesDto, source: StoreShiftTemplatesDto,
@@ -68,18 +65,6 @@ export function cloneStaffForm(
}; };
} }
/** 按模板创建空白一周(默认休息)。 */
export function createEmptyWeekShifts(
_templates: StoreShiftTemplatesDto,
): StaffDayShiftDto[] {
return DAY_OPTIONS.map((day) => ({
dayOfWeek: day.dayOfWeek,
shiftType: 'off' as const,
startTime: '',
endTime: '',
}));
}
/** 生成指定班次默认时间。 */ /** 生成指定班次默认时间。 */
export function resolveShiftTimeByType( export function resolveShiftTimeByType(
shiftType: ShiftType, shiftType: ShiftType,
@@ -124,58 +109,40 @@ export function normalizeDayShift(payload: {
return { return {
dayOfWeek: payload.dayOfWeek, dayOfWeek: payload.dayOfWeek,
shiftType, shiftType,
startTime: normalizeTime(payload.shift.startTime, templateTime.startTime), startTime: normalizeTime(
endTime: normalizeTime(payload.shift.endTime, templateTime.endTime), payload.shift.startTime,
payload.fallback?.startTime || templateTime.startTime,
),
endTime: normalizeTime(
payload.shift.endTime,
payload.fallback?.endTime || templateTime.endTime,
),
} satisfies StaffDayShiftDto; } satisfies StaffDayShiftDto;
} }
/** 归一化周排班数组,确保包含 0-6 共七天。 */ /** 归一化周排班数组(仅保留真实配置的天)。 */
export function normalizeWeekShifts(payload: { export function normalizeSparseWeekShifts(payload: {
fallback: StaffDayShiftDto[];
shifts: Partial<StaffDayShiftDto>[]; shifts: Partial<StaffDayShiftDto>[];
templates: StoreShiftTemplatesDto; templates: StoreShiftTemplatesDto;
}) { }) {
const shiftMap = new Map<number, Partial<StaffDayShiftDto>>(); const shiftMap = new Map<number, StaffDayShiftDto>();
for (const shift of payload.shifts) { for (const shift of payload.shifts) {
const dayOfWeek = Number(shift.dayOfWeek); const dayOfWeek = Number(shift.dayOfWeek);
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) { if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
continue; continue;
} }
shiftMap.set(dayOfWeek, shift);
}
return DAY_OPTIONS.map((day) => { shiftMap.set(
const fallbackShift = payload.fallback.find( dayOfWeek,
(item) => item.dayOfWeek === day.dayOfWeek, normalizeDayShift({
); dayOfWeek,
return normalizeDayShift({ shift,
dayOfWeek: day.dayOfWeek, templates: payload.templates,
shift: shiftMap.get(day.dayOfWeek) ?? fallbackShift ?? {},
fallback: fallbackShift,
templates: payload.templates,
});
});
}
/** 将接口排班数组转换为映射。 */
export function createScheduleMap(schedules: StaffScheduleDto[]) {
const scheduleMap: Record<string, StaffDayShiftDto[]> = {};
for (const schedule of schedules) {
scheduleMap[schedule.staffId] = normalizeWeekShifts({
shifts: schedule.shifts,
fallback: createEmptyWeekShifts({
morning: { startTime: '09:00', endTime: '14:00' },
evening: { startTime: '14:00', endTime: '21:00' },
full: { startTime: '09:00', endTime: '21:00' },
}), }),
templates: { );
morning: { startTime: '09:00', endTime: '14:00' },
evening: { startTime: '14:00', endTime: '21:00' },
full: { startTime: '09:00', endTime: '21:00' },
},
});
} }
return scheduleMap;
return [...shiftMap.values()].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
} }
/** 将员工排班矩阵转为可编辑周视图行。 */ /** 将员工排班矩阵转为可编辑周视图行。 */
@@ -187,16 +154,13 @@ export function buildWeekEditorRows(payload: {
roleType: WeekEditorRow['roleType']; roleType: WeekEditorRow['roleType'];
status: WeekEditorRow['status']; status: WeekEditorRow['status'];
}>; }>;
templates: StoreShiftTemplatesDto;
}) { }) {
return payload.staffs.map((staff) => ({ return payload.staffs.map((staff) => ({
staffId: staff.id, staffId: staff.id,
staffName: staff.name, staffName: staff.name,
roleType: staff.roleType, roleType: staff.roleType,
status: staff.status, status: staff.status,
shifts: cloneShifts( shifts: cloneShifts(payload.scheduleMap[staff.id] ?? []),
payload.scheduleMap[staff.id] ?? createEmptyWeekShifts(payload.templates),
),
})); }));
} }
@@ -207,21 +171,27 @@ export function updateWeekRowShift(payload: {
row: WeekEditorRow; row: WeekEditorRow;
templates: StoreShiftTemplatesDto; templates: StoreShiftTemplatesDto;
}) { }) {
const currentShift = payload.row.shifts.find(
(item) => item.dayOfWeek === payload.dayOfWeek,
);
const nextShift = normalizeDayShift({
dayOfWeek: payload.dayOfWeek,
shift: {
dayOfWeek: payload.dayOfWeek,
shiftType: payload.nextShiftType,
},
fallback: currentShift,
templates: payload.templates,
});
return { return {
...payload.row, ...payload.row,
shifts: payload.row.shifts.map((item) => { shifts: [
if (item.dayOfWeek !== payload.dayOfWeek) return { ...item }; ...payload.row.shifts
const normalized = normalizeDayShift({ .filter((item) => item.dayOfWeek !== payload.dayOfWeek)
dayOfWeek: payload.dayOfWeek, .map((item) => ({ ...item })),
shift: { nextShift,
dayOfWeek: payload.dayOfWeek, ].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek),
shiftType: payload.nextShiftType,
},
fallback: item,
templates: payload.templates,
});
return normalized;
}),
}; };
} }

View File

@@ -28,8 +28,8 @@ import {
buildWeekEditorRows, buildWeekEditorRows,
cloneScheduleMap, cloneScheduleMap,
cloneShifts, cloneShifts,
createEmptyWeekShifts,
normalizeDayShift, normalizeDayShift,
normalizeSparseWeekShifts,
normalizeTime, normalizeTime,
updateWeekRowShift, updateWeekRowShift,
} from './helpers'; } from './helpers';
@@ -57,50 +57,74 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
options.personalForm.staffName = staff.name; options.personalForm.staffName = staff.name;
options.personalForm.roleType = staff.roleType; options.personalForm.roleType = staff.roleType;
options.personalForm.shifts = cloneShifts( options.personalForm.shifts = cloneShifts(
options.scheduleMap.value[staff.id] ?? options.scheduleMap.value[staff.id] ?? [],
createEmptyWeekShifts(options.templates.value),
); );
options.isPersonalDrawerOpen.value = true; options.isPersonalDrawerOpen.value = true;
} }
/** 按天更新个人排班(不存在时自动补入)。 */
function upsertPersonalShift(dayOfWeek: number, nextShift: StaffDayShiftDto) {
options.personalForm.shifts = [
...options.personalForm.shifts
.filter((item) => item.dayOfWeek !== dayOfWeek)
.map((item) => ({ ...item })),
nextShift,
].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
}
/** 更新个人排班班次类型。 */ /** 更新个人排班班次类型。 */
function setPersonalShiftType(dayOfWeek: number, shiftType: ShiftType) { function setPersonalShiftType(dayOfWeek: number, shiftType: ShiftType) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => { const currentShift = options.personalForm.shifts.find(
if (item.dayOfWeek !== dayOfWeek) return { ...item }; (item) => item.dayOfWeek === dayOfWeek,
return normalizeDayShift({ );
const nextShift = normalizeDayShift({
dayOfWeek,
shift: {
dayOfWeek, dayOfWeek,
shift: { shiftType,
...item, },
shiftType, fallback: currentShift,
}, templates: options.templates.value,
fallback: item,
templates: options.templates.value,
});
}); });
upsertPersonalShift(dayOfWeek, nextShift);
} }
/** 更新个人排班开始时间。 */ /** 更新个人排班开始时间。 */
function setPersonalShiftStart(dayOfWeek: number, startTime: string) { function setPersonalShiftStart(dayOfWeek: number, startTime: string) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => { const currentShift = options.personalForm.shifts.find(
if (item.dayOfWeek !== dayOfWeek) return { ...item }; (item) => item.dayOfWeek === dayOfWeek,
if (item.shiftType === 'off') return { ...item }; );
return { if (!currentShift || currentShift.shiftType === 'off') return;
...item,
startTime: normalizeTime(startTime, item.startTime), const nextShift = normalizeDayShift({
}; dayOfWeek,
shift: {
...currentShift,
startTime: normalizeTime(startTime, currentShift.startTime),
},
fallback: currentShift,
templates: options.templates.value,
}); });
upsertPersonalShift(dayOfWeek, nextShift);
} }
/** 更新个人排班结束时间。 */ /** 更新个人排班结束时间。 */
function setPersonalShiftEnd(dayOfWeek: number, endTime: string) { function setPersonalShiftEnd(dayOfWeek: number, endTime: string) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => { const currentShift = options.personalForm.shifts.find(
if (item.dayOfWeek !== dayOfWeek) return { ...item }; (item) => item.dayOfWeek === dayOfWeek,
if (item.shiftType === 'off') return { ...item }; );
return { if (!currentShift || currentShift.shiftType === 'off') return;
...item,
endTime: normalizeTime(endTime, item.endTime), const nextShift = normalizeDayShift({
}; dayOfWeek,
shift: {
...currentShift,
endTime: normalizeTime(endTime, currentShift.endTime),
},
fallback: currentShift,
templates: options.templates.value,
}); });
upsertPersonalShift(dayOfWeek, nextShift);
} }
/** 提交个人排班。 */ /** 提交个人排班。 */
@@ -115,7 +139,13 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
shifts: cloneShifts(options.personalForm.shifts), shifts: cloneShifts(options.personalForm.shifts),
}); });
options.patchStaffSchedule(result.staffId, result.shifts); options.patchStaffSchedule(
result.staffId,
normalizeSparseWeekShifts({
shifts: result.shifts,
templates: options.templates.value,
}),
);
options.isPersonalDrawerOpen.value = false; options.isPersonalDrawerOpen.value = false;
message.success('员工排班已保存'); message.success('员工排班已保存');
} catch (error) { } catch (error) {
@@ -133,7 +163,6 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
options.weekRows.value = buildWeekEditorRows({ options.weekRows.value = buildWeekEditorRows({
staffs: editableStaffs, staffs: editableStaffs,
scheduleMap: options.scheduleMap.value, scheduleMap: options.scheduleMap.value,
templates: options.templates.value,
}); });
options.isWeekDrawerOpen.value = true; options.isWeekDrawerOpen.value = true;
} }
@@ -178,7 +207,10 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
const nextMap = cloneScheduleMap(options.scheduleMap.value); const nextMap = cloneScheduleMap(options.scheduleMap.value);
for (const schedule of result.schedules ?? []) { for (const schedule of result.schedules ?? []) {
nextMap[schedule.staffId] = cloneShifts(schedule.shifts); nextMap[schedule.staffId] = normalizeSparseWeekShifts({
shifts: cloneShifts(schedule.shifts),
templates: options.templates.value,
});
} }
options.patchScheduleMap(nextMap); options.patchScheduleMap(nextMap);

View File

@@ -24,11 +24,13 @@ import type {
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { import {
DAY_OPTIONS, DAY_OPTIONS,
DEFAULT_FILTER_STATE, DEFAULT_FILTER_STATE,
DEFAULT_SHIFT_TEMPLATES,
DEFAULT_STAFF_FORM, DEFAULT_STAFF_FORM,
EMPTY_SHIFT_TEMPLATES,
PAGE_SIZE_OPTIONS, PAGE_SIZE_OPTIONS,
SHIFT_OPTIONS, SHIFT_OPTIONS,
STAFF_PERMISSION_OPTIONS, STAFF_PERMISSION_OPTIONS,
@@ -42,11 +44,9 @@ import {
cloneShifts, cloneShifts,
cloneStaffForm, cloneStaffForm,
cloneTemplates, cloneTemplates,
createEmptyWeekShifts,
formatShiftTimeText, formatShiftTimeText,
getCurrentWeekStartDate, getCurrentWeekStartDate,
maskPhone, maskPhone,
normalizeWeekShifts,
resolveAvatarText, resolveAvatarText,
} from './staff-page/helpers'; } from './staff-page/helpers';
import { createScheduleActions } from './staff-page/schedule-actions'; import { createScheduleActions } from './staff-page/schedule-actions';
@@ -75,9 +75,11 @@ export function useStoreStaffPage() {
const staffRows = ref<StoreStaffDto[]>([]); const staffRows = ref<StoreStaffDto[]>([]);
const staffTotal = ref(0); const staffTotal = ref(0);
const staffDirectory = ref<StoreStaffDto[]>([]); const staffDirectory = ref<StoreStaffDto[]>([]);
const isTemplateConfigured = ref(false);
const isScheduleConfigured = ref(false);
const templates = ref<StoreShiftTemplatesDto>( const templates = ref<StoreShiftTemplatesDto>(
cloneTemplates(DEFAULT_SHIFT_TEMPLATES), cloneTemplates(EMPTY_SHIFT_TEMPLATES),
); );
const scheduleMap = ref<Record<string, StaffDayShiftDto[]>>({}); const scheduleMap = ref<Record<string, StaffDayShiftDto[]>>({});
const scheduleSnapshot = ref<null | { const scheduleSnapshot = ref<null | {
@@ -97,7 +99,7 @@ export function useStoreStaffPage() {
staffId: '', staffId: '',
staffName: '', staffName: '',
roleType: 'cashier', roleType: 'cashier',
shifts: createEmptyWeekShifts(DEFAULT_SHIFT_TEMPLATES), shifts: [],
}); });
const isWeekDrawerOpen = ref(false); const isWeekDrawerOpen = ref(false);
@@ -182,9 +184,7 @@ export function useStoreStaffPage() {
staffName: staff.name, staffName: staff.name,
roleType: staff.roleType, roleType: staff.roleType,
status: staff.status, status: staff.status,
shifts: cloneShifts( shifts: cloneShifts(scheduleMap.value[staff.id] ?? []),
scheduleMap.value[staff.id] ?? createEmptyWeekShifts(templates.value),
),
})); }));
}); });
@@ -195,7 +195,10 @@ export function useStoreStaffPage() {
type: item.value as Exclude<ShiftType, 'off'>, type: item.value as Exclude<ShiftType, 'off'>,
label: item.label, label: item.label,
className: item.className, className: item.className,
timeText: `${template.startTime}-${template.endTime}`, timeText:
template.startTime && template.endTime
? `${template.startTime}-${template.endTime}`
: '未配置',
}; };
}); });
}); });
@@ -212,6 +215,8 @@ export function useStoreStaffPage() {
reloadStoreData, reloadStoreData,
} = createDataActions({ } = createDataActions({
filters, filters,
isScheduleConfigured,
isTemplateConfigured,
isPageLoading, isPageLoading,
isStoreLoading, isStoreLoading,
scheduleMap, scheduleMap,
@@ -349,7 +354,7 @@ export function useStoreStaffPage() {
/** 判断员工是否可进入排班。 */ /** 判断员工是否可进入排班。 */
function isScheduleDisabled(staff: StoreStaffDto) { function isScheduleDisabled(staff: StoreStaffDto) {
return staff.status === 'resigned'; return staff.status === 'resigned' || !isTemplateConfigured.value;
} }
/** 打开个人排班抽屉。 */ /** 打开个人排班抽屉。 */
@@ -358,6 +363,15 @@ export function useStoreStaffPage() {
openPersonalScheduleDrawer(staff); openPersonalScheduleDrawer(staff);
} }
/** 打开周排班抽屉。 */
function handleOpenWeekSchedule() {
if (!isTemplateConfigured.value) {
message.warning('请先配置班次模板,再编辑排班');
return;
}
openWeekScheduleDrawer();
}
/** 获取角色展示文案。 */ /** 获取角色展示文案。 */
function getRoleLabel(roleType: StaffRoleType) { function getRoleLabel(roleType: StaffRoleType) {
return roleOptionMap.value.get(roleType)?.label ?? roleType; return roleOptionMap.value.get(roleType)?.label ?? roleType;
@@ -390,23 +404,6 @@ export function useStoreStaffPage() {
return shiftOptionMap.value.get(shiftType)?.className ?? 'shift-off'; return shiftOptionMap.value.get(shiftType)?.className ?? 'shift-off';
} }
/** 获取员工某天排班。 */
function getStaffShift(row: WeekEditorRow, dayOfWeek: number) {
return (
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ??
normalizeWeekShifts({
shifts: [],
fallback: createEmptyWeekShifts(templates.value),
templates: templates.value,
}).find((item) => item.dayOfWeek === dayOfWeek) ?? {
dayOfWeek,
shiftType: 'off',
startTime: '',
endTime: '',
}
);
}
// 7. 监听门店切换。 // 7. 监听门店切换。
watch(selectedStoreId, async (storeId) => { watch(selectedStoreId, async (storeId) => {
if (!storeId) { if (!storeId) {
@@ -437,11 +434,12 @@ export function useStoreStaffPage() {
deletingStaffId, deletingStaffId,
filters, filters,
getCurrentWeekStartDate, getCurrentWeekStartDate,
isScheduleConfigured,
isTemplateConfigured,
getRoleLabel, getRoleLabel,
getRoleTagClass, getRoleTagClass,
getShiftClass, getShiftClass,
getShiftLabel, getShiftLabel,
getStaffShift,
getStatusDotClass, getStatusDotClass,
getStatusLabel, getStatusLabel,
handleCopyCheckAll, handleCopyCheckAll,
@@ -470,7 +468,7 @@ export function useStoreStaffPage() {
maskPhone, maskPhone,
openCopyModal, openCopyModal,
openStaffDrawer, openStaffDrawer,
openWeekScheduleDrawer, handleOpenWeekSchedule,
personalDrawerTitle, personalDrawerTitle,
personalScheduleForm, personalScheduleForm,
resetFilters, resetFilters,

View File

@@ -43,6 +43,7 @@ const {
handleCopyCheckAll, handleCopyCheckAll,
handleCopySubmit, handleCopySubmit,
handleDeleteStaff, handleDeleteStaff,
handleOpenWeekSchedule,
handleOpenPersonalSchedule, handleOpenPersonalSchedule,
handlePageChange, handlePageChange,
handleSavePersonalSchedule, handleSavePersonalSchedule,
@@ -56,17 +57,18 @@ const {
isPageLoading, isPageLoading,
isPersonalDrawerOpen, isPersonalDrawerOpen,
isPersonalSaving, isPersonalSaving,
isScheduleConfigured,
isScheduleDisabled, isScheduleDisabled,
isStaffDrawerOpen, isStaffDrawerOpen,
isStaffSaving, isStaffSaving,
isStoreLoading, isStoreLoading,
isTemplateSaving, isTemplateSaving,
isTemplateConfigured,
isWeekDrawerOpen, isWeekDrawerOpen,
isWeekSaving, isWeekSaving,
maskPhone, maskPhone,
openCopyModal, openCopyModal,
openStaffDrawer, openStaffDrawer,
openWeekScheduleDrawer,
personalDrawerTitle, personalDrawerTitle,
personalScheduleForm, personalScheduleForm,
resetFilters, resetFilters,
@@ -164,6 +166,7 @@ const {
<ShiftTemplateCard <ShiftTemplateCard
:templates="templates" :templates="templates"
:is-saving="isTemplateSaving" :is-saving="isTemplateSaving"
:is-template-configured="isTemplateConfigured"
:on-set-template-time="setTemplateTime" :on-set-template-time="setTemplateTime"
@save="handleSaveTemplates" @save="handleSaveTemplates"
@reset="resetTemplates" @reset="resetTemplates"
@@ -175,8 +178,10 @@ const {
:get-shift-class="getShiftClass" :get-shift-class="getShiftClass"
:get-shift-label="getShiftLabel" :get-shift-label="getShiftLabel"
:get-shift-time-text="formatShiftTimeText" :get-shift-time-text="formatShiftTimeText"
:is-template-configured="isTemplateConfigured"
:is-schedule-configured="isScheduleConfigured"
:role-label-resolver="getRoleLabel" :role-label-resolver="getRoleLabel"
@edit-week="openWeekScheduleDrawer" @edit-week="handleOpenWeekSchedule"
/> />
</Spin> </Spin>
</template> </template>
@@ -223,6 +228,7 @@ const {
:get-shift-class="getShiftClass" :get-shift-class="getShiftClass"
:get-shift-label="getShiftLabel" :get-shift-label="getShiftLabel"
:get-shift-time-text="formatShiftTimeText" :get-shift-time-text="formatShiftTimeText"
:is-template-configured="isTemplateConfigured"
:role-label-resolver="getRoleLabel" :role-label-resolver="getRoleLabel"
@update:open="setWeekDrawerOpen" @update:open="setWeekDrawerOpen"
@cycle-shift="cycleWeekShift" @cycle-shift="cycleWeekShift"

View File

@@ -241,6 +241,12 @@
gap: 6px; gap: 6px;
} }
.staff-schedule-empty {
margin-left: 8px;
font-size: 12px;
color: #8c8c8c;
}
.staff-shift-pill { .staff-shift-pill {
min-width: 46px; min-width: 46px;
padding: 4px 10px; padding: 4px 10px;

View File

@@ -12,6 +12,14 @@
border-radius: 10px; border-radius: 10px;
} }
.schedule-board-tip {
padding: 10px 12px;
font-size: 12px;
color: #ad6800;
background: #fffbe6;
border-bottom: 1px solid #ffe58f;
}
.schedule-board-table, .schedule-board-table,
.staff-week-table { .staff-week-table {
width: 100%; width: 100%;
@@ -67,6 +75,13 @@
border-radius: 8px; border-radius: 8px;
} }
.schedule-cell.shift-unconfigured,
.staff-week-cell.shift-unconfigured {
color: #8c8c8c;
background: #fafafa;
border-color: #d9d9d9;
}
.schedule-cell-time { .schedule-cell-time {
font-size: 11px; font-size: 11px;
line-height: 1.3; line-height: 1.3;

View File

@@ -1,5 +1,15 @@
/* 文件职责:班次模板区样式。 */ /* 文件职责:班次模板区样式。 */
.page-store-staff { .page-store-staff {
.template-guide {
padding: 10px 12px;
margin-bottom: 10px;
font-size: 12px;
color: #ad6800;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 8px;
}
.template-list { .template-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;