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 {
isScheduleConfigured: boolean;
isTemplateConfigured: boolean;
schedules: StaffScheduleDto[];
storeId: string;
templates: StoreShiftTemplatesDto;
templates: null | StoreShiftTemplatesDto;
weekStartDate: string;
}

View File

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

View File

@@ -40,6 +40,12 @@ function getDayShift(dayOfWeek: number) {
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) {
if (!time) return undefined;
@@ -65,7 +71,7 @@ function toTimeText(value: Dayjs | null | string | undefined) {
@update:open="(value) => emit('update:open', value)"
>
<div class="staff-schedule-hint">
点击班次胶囊可自动填充时间选择休息后该日不排班
点击班次胶囊可自动填充时间显示未配置表示该日尚未设置排班
</div>
<div class="staff-schedule-list">
@@ -94,14 +100,19 @@ function toTimeText(value: Dayjs | null | string | undefined) {
</button>
</div>
<span v-if="!getDayShift(day.dayOfWeek)" class="staff-schedule-empty">
未配置
</span>
<div
class="staff-schedule-time"
:class="{ hidden: getDayShift(day.dayOfWeek)?.shiftType === 'off' }"
:class="{ hidden: isTimeHidden(day.dayOfWeek) }"
>
<TimePicker
:value="toPickerValue(getDayShift(day.dayOfWeek)?.startTime || '')"
format="HH:mm"
:allow-clear="false"
:disabled="isTimeHidden(day.dayOfWeek)"
@update: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 || '')"
format="HH:mm"
:allow-clear="false"
:disabled="isTimeHidden(day.dayOfWeek)"
@update:value="
(value) => props.onSetShiftEnd(day.dayOfWeek, toTimeText(value))
"

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@
import type {
ShiftType,
StaffDayShiftDto,
StaffScheduleDto,
StoreShiftTemplatesDto,
} from '#/api/store-staff';
import type {
@@ -15,8 +14,6 @@ import type {
WeekEditorRow,
} from '#/views/store/staff/types';
import { DAY_OPTIONS } from './constants';
/** 深拷贝模板。 */
export function cloneTemplates(
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(
shiftType: ShiftType,
@@ -124,58 +109,40 @@ export function normalizeDayShift(payload: {
return {
dayOfWeek: payload.dayOfWeek,
shiftType,
startTime: normalizeTime(payload.shift.startTime, templateTime.startTime),
endTime: normalizeTime(payload.shift.endTime, templateTime.endTime),
startTime: normalizeTime(
payload.shift.startTime,
payload.fallback?.startTime || templateTime.startTime,
),
endTime: normalizeTime(
payload.shift.endTime,
payload.fallback?.endTime || templateTime.endTime,
),
} satisfies StaffDayShiftDto;
}
/** 归一化周排班数组,确保包含 0-6 共七天。 */
export function normalizeWeekShifts(payload: {
fallback: StaffDayShiftDto[];
/** 归一化周排班数组(仅保留真实配置的天)。 */
export function normalizeSparseWeekShifts(payload: {
shifts: Partial<StaffDayShiftDto>[];
templates: StoreShiftTemplatesDto;
}) {
const shiftMap = new Map<number, Partial<StaffDayShiftDto>>();
const shiftMap = new Map<number, StaffDayShiftDto>();
for (const shift of payload.shifts) {
const dayOfWeek = Number(shift.dayOfWeek);
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
continue;
}
shiftMap.set(dayOfWeek, shift);
}
return DAY_OPTIONS.map((day) => {
const fallbackShift = payload.fallback.find(
(item) => item.dayOfWeek === day.dayOfWeek,
);
return normalizeDayShift({
dayOfWeek: day.dayOfWeek,
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' },
shiftMap.set(
dayOfWeek,
normalizeDayShift({
dayOfWeek,
shift,
templates: payload.templates,
}),
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'];
status: WeekEditorRow['status'];
}>;
templates: StoreShiftTemplatesDto;
}) {
return payload.staffs.map((staff) => ({
staffId: staff.id,
staffName: staff.name,
roleType: staff.roleType,
status: staff.status,
shifts: cloneShifts(
payload.scheduleMap[staff.id] ?? createEmptyWeekShifts(payload.templates),
),
shifts: cloneShifts(payload.scheduleMap[staff.id] ?? []),
}));
}
@@ -207,21 +171,27 @@ export function updateWeekRowShift(payload: {
row: WeekEditorRow;
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 {
...payload.row,
shifts: payload.row.shifts.map((item) => {
if (item.dayOfWeek !== payload.dayOfWeek) return { ...item };
const normalized = normalizeDayShift({
dayOfWeek: payload.dayOfWeek,
shift: {
dayOfWeek: payload.dayOfWeek,
shiftType: payload.nextShiftType,
},
fallback: item,
templates: payload.templates,
});
return normalized;
}),
shifts: [
...payload.row.shifts
.filter((item) => item.dayOfWeek !== payload.dayOfWeek)
.map((item) => ({ ...item })),
nextShift,
].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek),
};
}

View File

@@ -28,8 +28,8 @@ import {
buildWeekEditorRows,
cloneScheduleMap,
cloneShifts,
createEmptyWeekShifts,
normalizeDayShift,
normalizeSparseWeekShifts,
normalizeTime,
updateWeekRowShift,
} from './helpers';
@@ -57,50 +57,74 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
options.personalForm.staffName = staff.name;
options.personalForm.roleType = staff.roleType;
options.personalForm.shifts = cloneShifts(
options.scheduleMap.value[staff.id] ??
createEmptyWeekShifts(options.templates.value),
options.scheduleMap.value[staff.id] ?? [],
);
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) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
if (item.dayOfWeek !== dayOfWeek) return { ...item };
return normalizeDayShift({
const currentShift = options.personalForm.shifts.find(
(item) => item.dayOfWeek === dayOfWeek,
);
const nextShift = normalizeDayShift({
dayOfWeek,
shift: {
dayOfWeek,
shift: {
...item,
shiftType,
},
fallback: item,
templates: options.templates.value,
});
shiftType,
},
fallback: currentShift,
templates: options.templates.value,
});
upsertPersonalShift(dayOfWeek, nextShift);
}
/** 更新个人排班开始时间。 */
function setPersonalShiftStart(dayOfWeek: number, startTime: string) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
if (item.dayOfWeek !== dayOfWeek) return { ...item };
if (item.shiftType === 'off') return { ...item };
return {
...item,
startTime: normalizeTime(startTime, item.startTime),
};
const currentShift = options.personalForm.shifts.find(
(item) => item.dayOfWeek === dayOfWeek,
);
if (!currentShift || currentShift.shiftType === 'off') return;
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) {
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
if (item.dayOfWeek !== dayOfWeek) return { ...item };
if (item.shiftType === 'off') return { ...item };
return {
...item,
endTime: normalizeTime(endTime, item.endTime),
};
const currentShift = options.personalForm.shifts.find(
(item) => item.dayOfWeek === dayOfWeek,
);
if (!currentShift || currentShift.shiftType === 'off') return;
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),
});
options.patchStaffSchedule(result.staffId, result.shifts);
options.patchStaffSchedule(
result.staffId,
normalizeSparseWeekShifts({
shifts: result.shifts,
templates: options.templates.value,
}),
);
options.isPersonalDrawerOpen.value = false;
message.success('员工排班已保存');
} catch (error) {
@@ -133,7 +163,6 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
options.weekRows.value = buildWeekEditorRows({
staffs: editableStaffs,
scheduleMap: options.scheduleMap.value,
templates: options.templates.value,
});
options.isWeekDrawerOpen.value = true;
}
@@ -178,7 +207,10 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
const nextMap = cloneScheduleMap(options.scheduleMap.value);
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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,15 @@
/* 文件职责:班次模板区样式。 */
.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 {
display: flex;
flex-direction: column;