Files
gao-customer-h5/src/pages/store/StoreBusinessDayCalendar.vue
T
2026-08-26 21:18:02 +08:00

454 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
v-if="store"
v-can="permissionCodes.store.read"
class="store-business-day-calendar"
>
<header class="summary bg-white" v-entrance-transition>
<header class="header">
<div class="text-bold" v-text="store.name" />
<t-tag variant="outline" :content="store.code" />
</header>
<div class="address gray">
<span>地址:</span>
<span v-text="store.address" />
</div>
</header>
<card-container
class="calendar-card"
description="设置门店休息日,该日无任何登记记录不视为中断连续登记事件。已有登记不可设为休息,标记休息不能产生登记"
tips="点击日期即可新增营休日"
v-entrance-transition="70"
>
<t-calendar
:key="calendarRenderKey"
class="calendar"
:value="selectedCalendarDate"
:min-date="calendarMinDate"
:max-date="calendarMaxDate"
:format="calendarFormat"
switch-mode="month"
:use-popup="false"
@panel-change="handlePanelChange"
@select="selectCalendarDate"
/>
<div v-if="isCalendarLoading" class="loading" v-text="'正在加载当月营业日'" />
</card-container>
</div>
<t-popup v-model="editorVisible" placement="bottom">
<div class="editor">
<div class="header">
<div class="text-bold" v-text="editingId ? '编辑营业日' : '新增营业日'" />
<div v-text="formModel.dateText" />
</div>
<t-form label-align="left">
<t-form-item label="状态">
<t-radio-group v-model="formModel.status" variant="default-filled">
<t-radio :block="false" borderless value="OPEN">营业</t-radio>
<t-radio :block="false" borderless value="CLOSED">休息</t-radio>
</t-radio-group>
</t-form-item>
<t-form-item
class="reason"
:class="{ 'is-hidden': formModel.status !== 'CLOSED' }"
label="原因"
>
<t-input
v-model.trim="formModel.reason"
:disabled="formModel.status !== 'CLOSED'"
placeholder="休息原因或备注"
borderless
/>
</t-form-item>
</t-form>
<span class="hint orange">切换营业或休息可能影响连续登记统计,系统重排营业日事件序列可能需要较长时间。</span>
<div class="actions">
<t-button
v-if="editingId"
v-can="deletePermissionCode"
class="action"
theme="danger"
variant="outline"
:loading="isDeleting"
@click="confirmRemoveDay"
>删除</t-button>
<t-button
v-can="savePermissionCode"
class="action"
theme="primary"
:loading="isSaving"
@click="saveDay"
>保存</t-button>
</div>
</div>
</t-popup>
</template>
<script setup lang="ts">
import { type CalendarProps, DialogPlugin, type TDate } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web-api";
import StoreAPI from "@/api/StoreAPI";
import type { Store, StoreBusinessDay, StoreBusinessDayStatus } from "@/types/Store";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { Message } from "@/utils/message";
defineOptions({
name: "StoreBusinessDayCalendar"
});
const route = useRoute();
const userStore = useUserStore();
const storeId = computed(() => String(route.params.id || ""));
const isStoreManagerScope = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) && !userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 门店信息 ----------
const store = ref<Store>();
// ---------- 日历数据 ----------
const today = new Date();
const calendarMinDate = new Date(today.getFullYear() - 2, 0, 1);
const calendarMaxDate = new Date(today.getFullYear() + 2, 11, 31);
const selectedCalendarDate = ref<Date>(today);
const calendarMonth = reactive({
year: today.getFullYear(),
month: today.getMonth() + 1
});
const businessDayList = ref<StoreBusinessDay[]>([]);
const isCalendarLoading = ref(false);
const calendarRenderKey = ref(0);
let calendarRequestId = 0;
const businessDayMap = computed(() => new Map(
businessDayList.value
.filter(item => item.dateValue)
.map(item => [item.dateValue as number, item])
));
const calendarFormat = computed<NonNullable<CalendarProps["format"]>>(() => {
const dayMap = businessDayMap.value;
return (dateItem: TDate) => {
const day = dayMap.get(toDateValue(dateItem.date));
if (!day) {
return dateItem;
}
return {
...dateItem,
className: `business-day-${day.status!.toLowerCase()}`,
suffix: day.status === "OPEN" ? "营" : "休"
};
};
});
async function loadCalendar(year: number, month: number): Promise<void> {
const requestId = ++calendarRequestId;
isCalendarLoading.value = true;
try {
const beginDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
const result = await StoreAPI.businessDayCalendar(storeId.value, toDateValue(beginDate), toDateValue(endDate));
if (requestId === calendarRequestId) {
businessDayList.value = result || [];
calendarRenderKey.value += 1;
}
} finally {
if (requestId === calendarRequestId) {
isCalendarLoading.value = false;
}
}
}
async function handlePanelChange(context: { year: number; month: number }): Promise<void> {
if (context.year === calendarMonth.year && context.month === calendarMonth.month) {
return;
}
calendarMonth.year = context.year;
calendarMonth.month = context.month;
selectedCalendarDate.value = new Date(context.year, context.month - 1, 1);
try {
await loadCalendar(context.year, context.month);
} catch {
Message.error("营业日加载失败");
}
}
// ---------- 编辑 ----------
const editorVisible = ref(false);
const isSaving = ref(false);
const isDeleting = ref(false);
const editingId = ref("");
const formModel = reactive({
dateText: "",
status: "OPEN" as StoreBusinessDayStatus,
reason: "正常休息"
});
const savePermissionCode = computed(() => {
if (isStoreManagerScope.value) {
return permissionCodes.store.update;
}
return editingId.value ? permissionCodes.store.update : permissionCodes.store.create;
});
const deletePermissionCode = computed(() => isStoreManagerScope.value ? permissionCodes.store.update : permissionCodes.store.delete);
function openDayEditor(day?: StoreBusinessDay, dateValue = toDateValue(new Date())): void {
editingId.value = day?.id || "";
Object.assign(formModel, {
dateText: formatDateValue(day?.dateValue || dateValue),
status: day?.status || "OPEN",
reason: day ? day.reason : "正常休息"
});
editorVisible.value = true;
}
function selectCalendarDate(date: Date): void {
selectedCalendarDate.value = date;
const dateValue = toDateValue(date);
openDayEditor(businessDayMap.value.get(dateValue), dateValue);
}
async function saveDay(): Promise<void> {
if (!validateDay() || isSaving.value) {
return;
}
isSaving.value = true;
try {
const day: StoreBusinessDay = {
id: editingId.value || undefined,
storeId: storeId.value,
dateValue: parseDateValue(formModel.dateText),
status: formModel.status,
reason: formModel.status === "CLOSED" ? formModel.reason || undefined : undefined
};
if (editingId.value) {
await StoreAPI.updateBusinessDay(day);
} else {
await StoreAPI.createBusinessDay(day);
}
Message.success("营业日已保存");
editorVisible.value = false;
await loadCalendar(calendarMonth.year, calendarMonth.month);
} catch {
Message.error("营业日保存失败");
} finally {
isSaving.value = false;
}
}
function validateDay(): boolean {
if (!formModel.dateText) {
Message.warning("请选择日期");
return false;
}
return true;
}
// ---------- 删除 ----------
function confirmRemoveDay(): void {
if (!editingId.value || isDeleting.value) {
return;
}
const dayId = editingId.value;
const dialog = DialogPlugin.confirm({
title: "删除营业日",
content: `确定删除 ${formModel.dateText} 的营业日配置吗?已有登记记录的营业日系统会拒绝删除`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeDay(dayId);
}
});
}
async function removeDay(id: string): Promise<void> {
isDeleting.value = true;
try {
await StoreAPI.removeBusinessDay(id);
Message.success("营业日已删除");
editorVisible.value = false;
await loadCalendar(calendarMonth.year, calendarMonth.month);
} catch {
Message.error("营业日删除失败");
} finally {
isDeleting.value = false;
}
}
// ---------- 日期 ----------
function formatDateValue(value?: number): string {
if (!value) {
return "";
}
const text = String(value);
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
}
function parseDateValue(text: string): number {
return Number(text.replaceAll("-", ""));
}
function toDateValue(date: Date): number {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return Number(`${year}${month}${day}`);
}
onMounted(async () => {
try {
const [currentStore] = await Promise.all([
StoreAPI.detail(storeId.value),
loadCalendar(calendarMonth.year, calendarMonth.month)
]);
store.value = currentStore;
} catch {
Message.error("营业日数据加载失败");
}
});
</script>
<style scoped lang="less">
.store-business-day-calendar {
gap: .75rem;
display: grid;
padding: calc(var(--app-nav-height) + 1rem) 0 6rem;
.summary {
gap: .75rem;
margin: 0 1rem;
display: flex;
padding: .75rem;
border-radius: .75rem;
flex-direction: column;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.address {
font-size: .875rem;
}
}
.legend {
gap: .375rem;
display: flex;
flex: none;
}
.calendar-card {
:deep(.t-calendar-header) {
padding-top: 8px;
}
:deep(.t-calendar) {
z-index: 1;
overflow: hidden;
border-radius: .75rem;
&__title {
display: none;
}
&__months {
height: unset;
}
&__dates-item {
height: 40px;
align-items: unset;
&.business-day-open {
--td-calendar-item-suffix-color: var(--td-brand-color);
}
&.business-day-closed .t-calendar__dates-item-suffix {
--td-calendar-item-suffix-color: var(--td-error-color);
}
}
}
:deep(.business-day-open),
:deep(.business-day-closed) {
font-weight: 700;
}
:deep(.business-day-closed .t-calendar__dates-item-suffix) {
}
}
.loading,
.empty {
color: #52607a;
padding: .75rem 1rem;
font-size: .8125rem;
text-align: center;
}
}
.editor {
gap: .75rem;
display: flex;
padding: 1rem;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
}
.reason.is-hidden {
visibility: hidden;
}
.hint {
text-indent: 2em;
}
.actions {
gap: .75rem;
display: flex;
.action {
flex: 1;
}
}
}
:global(html.theme-dark) {
.store-business-day-calendar {
.summary,
.calendar-card,
.editor {
background: #1e293b;
}
.summary span,
.loading,
.empty,
.editor .hint {
color: #94a3b8;
}
.summary strong,
.editor h3 {
color: #e2e8f0;
}
}
}
</style>