feat: 新增堂食管理页面并对齐后端dine-in路由
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食区域新增/编辑抽屉。
|
||||
* 1. 展示区域基础字段。
|
||||
* 2. 通过回调更新父级状态并提交。
|
||||
*/
|
||||
import type { DineInAreaFormState } from '#/views/store/dine-in/types';
|
||||
|
||||
import { Button, Drawer, Input, InputNumber } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
form: DineInAreaFormState;
|
||||
isSaving: boolean;
|
||||
onSetDescription: (value: string) => void;
|
||||
onSetName: (value: string) => void;
|
||||
onSetSort: (value: number) => void;
|
||||
open: boolean;
|
||||
submitText: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'submit'): void;
|
||||
(event: 'update:open', value: boolean): void;
|
||||
}>();
|
||||
|
||||
function readInputValue(event: Event) {
|
||||
const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
|
||||
return target?.value ?? '';
|
||||
}
|
||||
|
||||
function toNumber(value: null | number | string, fallback = 1) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
class="dinein-area-drawer-wrap"
|
||||
:open="props.open"
|
||||
:title="props.title"
|
||||
:width="460"
|
||||
:mask-closable="true"
|
||||
@update:open="(value) => emit('update:open', value)"
|
||||
>
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label required">区域名称</label>
|
||||
<Input
|
||||
:value="props.form.name"
|
||||
:maxlength="20"
|
||||
placeholder="如:大厅、包间、露台"
|
||||
@input="(event) => props.onSetName(readInputValue(event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">区域描述</label>
|
||||
<Input.TextArea
|
||||
:value="props.form.description"
|
||||
:maxlength="120"
|
||||
:rows="3"
|
||||
placeholder="可选,如:主要用餐区域,可容纳约48人"
|
||||
@input="(event) => props.onSetDescription(readInputValue(event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">排序</label>
|
||||
<div class="drawer-input-with-unit">
|
||||
<InputNumber
|
||||
:value="props.form.sort"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="drawer-input"
|
||||
@update:value="(value) => props.onSetSort(toNumber(value, 1))"
|
||||
/>
|
||||
<span class="drawer-form-hint">数字越小越靠前</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<Button @click="emit('update:open', false)">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
{{ props.submitText }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食区域管理区块。
|
||||
* 1. 展示区域 pills 与当前区域说明。
|
||||
* 2. 抛出区域新增、编辑、删除与切换事件。
|
||||
*/
|
||||
import type { DineInAreaDto } from '#/api/store-dinein';
|
||||
|
||||
import { Button, Card, Empty, Popconfirm } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
getAreaTableCount: (areaId: string) => number;
|
||||
isSaving: boolean;
|
||||
selectedArea?: DineInAreaDto;
|
||||
selectedAreaId: string;
|
||||
areas: DineInAreaDto[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'add'): void;
|
||||
(event: 'delete', area: DineInAreaDto): void;
|
||||
(event: 'edit', area: DineInAreaDto): void;
|
||||
(event: 'selectArea', areaId: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :bordered="false" class="dinein-card">
|
||||
<template #title>
|
||||
<span class="section-title">区域管理</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button type="primary" size="small" @click="emit('add')">添加区域</Button>
|
||||
</template>
|
||||
|
||||
<template v-if="props.areas.length > 0">
|
||||
<div class="dinein-area-pills">
|
||||
<button
|
||||
v-for="area in props.areas"
|
||||
:key="area.id"
|
||||
type="button"
|
||||
class="dinein-area-pill"
|
||||
:class="{ active: props.selectedAreaId === area.id }"
|
||||
@click="emit('selectArea', area.id)"
|
||||
>
|
||||
{{ area.name }} ({{ props.getAreaTableCount(area.id) }}桌)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="props.selectedArea" class="dinein-area-info">
|
||||
<span class="dinein-area-description">
|
||||
{{ props.selectedArea.name }} -
|
||||
{{ props.selectedArea.description || '暂无描述' }}
|
||||
</span>
|
||||
<div class="dinein-area-actions">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="emit('edit', props.selectedArea)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除该区域吗?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="emit('delete', props.selectedArea)"
|
||||
>
|
||||
<Button type="link" danger size="small" :loading="props.isSaving">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Empty v-else description="暂无区域,请先添加" />
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食基础设置区块。
|
||||
* 1. 展示堂食开关、默认时长、超时提醒配置。
|
||||
* 2. 抛出保存与重置事件。
|
||||
*/
|
||||
import type { DineInBasicSettingsDto } from '#/api/store-dinein';
|
||||
|
||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
isSaving: boolean;
|
||||
onSetDefaultDiningMinutes: (value: number) => void;
|
||||
onSetEnabled: (value: boolean) => void;
|
||||
onSetOvertimeReminderMinutes: (value: number) => void;
|
||||
settings: DineInBasicSettingsDto;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'reset'): void;
|
||||
(event: 'save'): void;
|
||||
}>();
|
||||
|
||||
function toNumber(value: null | number | string, fallback = 0) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :bordered="false" class="dinein-card">
|
||||
<template #title>
|
||||
<span class="section-title">堂食设置</span>
|
||||
</template>
|
||||
|
||||
<div class="dinein-form-row">
|
||||
<label class="dinein-form-label">是否开启堂食</label>
|
||||
<div class="dinein-form-control">
|
||||
<Switch
|
||||
:checked="props.settings.enabled"
|
||||
@update:checked="(value) => props.onSetEnabled(Boolean(value))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dinein-form-row">
|
||||
<label class="dinein-form-label">默认用餐时长</label>
|
||||
<div class="dinein-form-control">
|
||||
<InputNumber
|
||||
:value="props.settings.defaultDiningMinutes"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="dinein-number-input"
|
||||
@update:value="
|
||||
(value) => props.onSetDefaultDiningMinutes(toNumber(value, 90))
|
||||
"
|
||||
/>
|
||||
<span class="dinein-form-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dinein-form-row">
|
||||
<label class="dinein-form-label">超时提醒</label>
|
||||
<div class="dinein-form-control">
|
||||
<InputNumber
|
||||
:value="props.settings.overtimeReminderMinutes"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="dinein-number-input"
|
||||
@update:value="
|
||||
(value) => props.onSetOvertimeReminderMinutes(toNumber(value, 10))
|
||||
"
|
||||
/>
|
||||
<span class="dinein-form-unit">分钟</span>
|
||||
<span class="dinein-form-hint">超过默认用餐时长后提醒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dinein-form-actions">
|
||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食桌位批量生成弹窗。
|
||||
* 1. 展示批量参数并实时预览编号。
|
||||
* 2. 通过回调更新父级状态并提交。
|
||||
*/
|
||||
import type { DineInSeatsOption } from '#/views/store/dine-in/types';
|
||||
|
||||
import { Input, InputNumber, Modal, Select, Tag } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
areaOptions: Array<{ label: string; value: string }>;
|
||||
form: {
|
||||
areaId: string;
|
||||
codePrefix: string;
|
||||
count: number;
|
||||
seats: number;
|
||||
startNumber: number;
|
||||
};
|
||||
isSaving: boolean;
|
||||
onSetAreaId: (value: string) => void;
|
||||
onSetCodePrefix: (value: string) => void;
|
||||
onSetCount: (value: number) => void;
|
||||
onSetSeats: (value: number) => void;
|
||||
onSetStartNumber: (value: number) => void;
|
||||
open: boolean;
|
||||
previewCodes: string[];
|
||||
seatsOptions: DineInSeatsOption[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'submit'): void;
|
||||
(event: 'update:open', value: boolean): void;
|
||||
}>();
|
||||
|
||||
function readInputValue(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
return target?.value ?? '';
|
||||
}
|
||||
|
||||
function readString(value: unknown) {
|
||||
if (typeof value === 'number' || typeof value === 'string')
|
||||
return String(value);
|
||||
return '';
|
||||
}
|
||||
|
||||
function readNumber(value: unknown, fallback = 1) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:open="props.open"
|
||||
title="批量生成桌位"
|
||||
:width="520"
|
||||
:confirm-loading="props.isSaving"
|
||||
ok-text="确认生成"
|
||||
cancel-text="取消"
|
||||
:mask-closable="true"
|
||||
wrap-class-name="dinein-batch-modal-wrap"
|
||||
@update:open="(value) => emit('update:open', value)"
|
||||
@ok="emit('submit')"
|
||||
@cancel="emit('update:open', false)"
|
||||
>
|
||||
<div class="batch-form-grid">
|
||||
<div class="batch-form-item full">
|
||||
<label>所属区域</label>
|
||||
<Select
|
||||
:value="props.form.areaId"
|
||||
:options="props.areaOptions"
|
||||
placeholder="请选择区域"
|
||||
@update:value="(value) => props.onSetAreaId(readString(value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="batch-form-item">
|
||||
<label>编号前缀</label>
|
||||
<Input
|
||||
:value="props.form.codePrefix"
|
||||
:maxlength="6"
|
||||
placeholder="如:A"
|
||||
@input="(event) => props.onSetCodePrefix(readInputValue(event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="batch-form-item">
|
||||
<label>起始编号</label>
|
||||
<InputNumber
|
||||
:value="props.form.startNumber"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="batch-input-number"
|
||||
@update:value="
|
||||
(value) => props.onSetStartNumber(readNumber(value, 1))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="batch-form-item">
|
||||
<label>生成数量</label>
|
||||
<InputNumber
|
||||
:value="props.form.count"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="batch-input-number"
|
||||
@update:value="(value) => props.onSetCount(readNumber(value, 1))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="batch-form-item">
|
||||
<label>座位数</label>
|
||||
<Select
|
||||
:value="props.form.seats"
|
||||
:options="props.seatsOptions"
|
||||
@update:value="(value) => props.onSetSeats(readNumber(value, 4))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-preview-wrap">
|
||||
<div class="batch-preview-title">预览:将生成以下桌位</div>
|
||||
<div class="batch-preview-tags">
|
||||
<Tag v-for="code in props.previewCodes" :key="code" color="processing">
|
||||
{{ code }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食桌位新增/编辑抽屉。
|
||||
* 1. 展示桌位编号、区域、座位、停用状态、标签。
|
||||
* 2. 通过回调更新父级状态并提交。
|
||||
*/
|
||||
import type { DineInTableStatus } from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInSeatsOption,
|
||||
DineInTableFormState,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
import { Button, Drawer, Input, Select, Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
areaOptions: Array<{ label: string; value: string }>;
|
||||
form: DineInTableFormState;
|
||||
isSaving: boolean;
|
||||
onSetAreaId: (value: string) => void;
|
||||
onSetCode: (value: string) => void;
|
||||
onSetDisabled: (value: boolean) => void;
|
||||
onSetSeats: (value: number) => void;
|
||||
onSetTags: (value: string[]) => void;
|
||||
open: boolean;
|
||||
seatsOptions: DineInSeatsOption[];
|
||||
statusLabelMap: Record<DineInTableStatus, string>;
|
||||
submitText: string;
|
||||
tagSuggestions: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'submit'): void;
|
||||
(event: 'update:open', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const statusHintText = '就餐中/已预约由业务驱动,管理端仅控制是否停用';
|
||||
|
||||
function readInputValue(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
return target?.value ?? '';
|
||||
}
|
||||
|
||||
function readString(value: unknown) {
|
||||
if (typeof value === 'number' || typeof value === 'string')
|
||||
return String(value);
|
||||
return '';
|
||||
}
|
||||
|
||||
function readNumber(value: unknown, fallback = 4) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function readTagValues(value: unknown) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item : String(item)))
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
class="dinein-table-drawer-wrap"
|
||||
:open="props.open"
|
||||
:title="props.title"
|
||||
:width="460"
|
||||
:mask-closable="true"
|
||||
@update:open="(value) => emit('update:open', value)"
|
||||
>
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label required">桌位编号</label>
|
||||
<Input
|
||||
:value="props.form.code"
|
||||
:maxlength="20"
|
||||
placeholder="如:A09"
|
||||
@input="(event) => props.onSetCode(readInputValue(event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">所属区域</label>
|
||||
<Select
|
||||
:value="props.form.areaId"
|
||||
:options="props.areaOptions"
|
||||
placeholder="请选择区域"
|
||||
@update:value="(value) => props.onSetAreaId(readString(value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-grid">
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label required">座位数</label>
|
||||
<Select
|
||||
:value="props.form.seats"
|
||||
:options="props.seatsOptions"
|
||||
@update:value="(value) => props.onSetSeats(readNumber(value, 4))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">停用状态</label>
|
||||
<div class="drawer-switch-row">
|
||||
<Switch
|
||||
:checked="props.form.isDisabled"
|
||||
@update:checked="(value) => props.onSetDisabled(Boolean(value))"
|
||||
/>
|
||||
<span class="drawer-form-hint">打开后桌位不可被分配</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">业务状态</label>
|
||||
<div class="drawer-status-preview">
|
||||
<Tag color="processing">
|
||||
{{ props.statusLabelMap[props.form.sourceStatus] ?? '--' }}
|
||||
</Tag>
|
||||
<span class="drawer-form-hint">{{ statusHintText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-form-block">
|
||||
<label class="drawer-form-label">标签</label>
|
||||
<Select
|
||||
mode="tags"
|
||||
:value="props.form.tags"
|
||||
:options="
|
||||
props.tagSuggestions.map((item) => ({ label: item, value: item }))
|
||||
"
|
||||
:max-tag-count="4"
|
||||
placeholder="输入后回车添加,如:靠窗、VIP"
|
||||
@update:value="(value) => props.onSetTags(readTagValues(value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<Button @click="emit('update:open', false)">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
{{ props.submitText }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:堂食桌位列表区块。
|
||||
* 1. 展示当前区域桌位卡片。
|
||||
* 2. 抛出二维码、编辑、删除、批量生成、新增事件。
|
||||
*/
|
||||
import type { DineInTableDto } from '#/api/store-dinein';
|
||||
import type { DineInStatusOption } from '#/views/store/dine-in/types';
|
||||
|
||||
import { Button, Card, Empty, Popconfirm, Tag } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
isSaving: boolean;
|
||||
resolveStatusClassName: (status: DineInTableDto['status']) => string;
|
||||
statusMap: Record<DineInTableDto['status'], DineInStatusOption>;
|
||||
tables: DineInTableDto[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'add'): void;
|
||||
(event: 'batch'): void;
|
||||
(event: 'delete', tableId: string): void;
|
||||
(event: 'edit', table: DineInTableDto): void;
|
||||
(event: 'qrcode', table: DineInTableDto): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :bordered="false" class="dinein-card">
|
||||
<template #title>
|
||||
<span class="section-title">桌位列表</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<div class="dinein-table-header-actions">
|
||||
<Button size="small" @click="emit('batch')">批量生成</Button>
|
||||
<Button type="primary" size="small" @click="emit('add')">
|
||||
添加桌位
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="props.tables.length > 0">
|
||||
<div class="dinein-table-grid">
|
||||
<div
|
||||
v-for="table in props.tables"
|
||||
:key="table.id"
|
||||
class="dinein-table-card"
|
||||
:class="{
|
||||
disabled: table.status === 'disabled',
|
||||
}"
|
||||
>
|
||||
<div class="dinein-table-code">{{ table.code }}</div>
|
||||
<div class="dinein-table-seat">{{ table.seats }}人桌</div>
|
||||
<div
|
||||
class="dinein-table-status"
|
||||
:class="props.resolveStatusClassName(table.status)"
|
||||
>
|
||||
<span class="status-dot"></span>
|
||||
{{ props.statusMap[table.status]?.label ?? '--' }}
|
||||
</div>
|
||||
|
||||
<div class="dinein-table-tags">
|
||||
<Tag v-for="tag in table.tags" :key="`${table.id}-${tag}`">
|
||||
{{ tag }}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div class="dinein-table-footer">
|
||||
<Button size="small" type="text" @click="emit('qrcode', table)">
|
||||
二维码
|
||||
</Button>
|
||||
<Button size="small" type="text" @click="emit('edit', table)">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除该桌位吗?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="emit('delete', table.id)"
|
||||
>
|
||||
<Button size="small" type="text" danger :loading="props.isSaving">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Empty v-else description="当前区域暂无桌位" />
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 文件职责:堂食区域动作。
|
||||
* 1. 管理区域新增/编辑抽屉状态与字段。
|
||||
* 2. 处理区域新增、编辑、删除流程。
|
||||
*/
|
||||
import type { DineInAreaDto, DineInTableDto } from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInAreaDrawerMode,
|
||||
DineInAreaFormState,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { deleteDineInAreaApi, saveDineInAreaApi } from '#/api/store-dinein';
|
||||
|
||||
import {
|
||||
countAreaTables,
|
||||
createDineInId,
|
||||
sortAreas,
|
||||
validateAreaForm,
|
||||
} from './helpers';
|
||||
|
||||
interface CreateAreaActionsOptions {
|
||||
areaDrawerMode: Ref<DineInAreaDrawerMode>;
|
||||
areaForm: DineInAreaFormState;
|
||||
areas: Ref<DineInAreaDto[]>;
|
||||
fixSelectedArea: () => void;
|
||||
isAreaDrawerOpen: Ref<boolean>;
|
||||
isSavingArea: Ref<boolean>;
|
||||
selectedAreaId: Ref<string>;
|
||||
selectedStoreId: Ref<string>;
|
||||
tables: Ref<DineInTableDto[]>;
|
||||
updateSnapshot: () => void;
|
||||
}
|
||||
|
||||
export function createAreaActions(options: CreateAreaActionsOptions) {
|
||||
/** 打开区域抽屉并初始化表单。 */
|
||||
function openAreaDrawer(mode: DineInAreaDrawerMode, area?: DineInAreaDto) {
|
||||
options.areaDrawerMode.value = mode;
|
||||
if (mode === 'edit' && area) {
|
||||
options.areaForm.id = area.id;
|
||||
options.areaForm.name = area.name;
|
||||
options.areaForm.description = area.description;
|
||||
options.areaForm.sort = area.sort;
|
||||
options.isAreaDrawerOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSort =
|
||||
options.areas.value.length === 0
|
||||
? 1
|
||||
: Math.max(...options.areas.value.map((item) => item.sort)) + 1;
|
||||
options.areaForm.id = '';
|
||||
options.areaForm.name = '';
|
||||
options.areaForm.description = '';
|
||||
options.areaForm.sort = nextSort;
|
||||
options.isAreaDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 控制区域抽屉显隐。 */
|
||||
function setAreaDrawerOpen(value: boolean) {
|
||||
options.isAreaDrawerOpen.value = value;
|
||||
}
|
||||
|
||||
function setAreaName(value: string) {
|
||||
options.areaForm.name = value;
|
||||
}
|
||||
|
||||
function setAreaDescription(value: string) {
|
||||
options.areaForm.description = value;
|
||||
}
|
||||
|
||||
function setAreaSort(value: number) {
|
||||
options.areaForm.sort = Math.max(1, Math.floor(Number(value || 1)));
|
||||
}
|
||||
|
||||
/** 提交区域表单。 */
|
||||
async function handleSubmitArea() {
|
||||
const validateMessage = validateAreaForm({
|
||||
areaId: options.areaForm.id,
|
||||
areas: options.areas.value,
|
||||
form: options.areaForm,
|
||||
});
|
||||
if (validateMessage) {
|
||||
message.error(validateMessage);
|
||||
return;
|
||||
}
|
||||
if (!options.selectedStoreId.value) return;
|
||||
|
||||
options.isSavingArea.value = true;
|
||||
try {
|
||||
const areaId = options.areaForm.id || createDineInId('area');
|
||||
const areaPayload: DineInAreaDto = {
|
||||
id: areaId,
|
||||
name: options.areaForm.name.trim(),
|
||||
description: options.areaForm.description.trim(),
|
||||
sort: Math.max(1, Math.floor(options.areaForm.sort)),
|
||||
};
|
||||
|
||||
await saveDineInAreaApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
area: areaPayload,
|
||||
});
|
||||
|
||||
options.areas.value =
|
||||
options.areaDrawerMode.value === 'edit' && options.areaForm.id
|
||||
? sortAreas(
|
||||
options.areas.value.map((item) =>
|
||||
item.id === options.areaForm.id ? areaPayload : item,
|
||||
),
|
||||
)
|
||||
: sortAreas([...options.areas.value, areaPayload]);
|
||||
|
||||
if (!options.selectedAreaId.value) {
|
||||
options.selectedAreaId.value = areaPayload.id;
|
||||
}
|
||||
options.fixSelectedArea();
|
||||
options.updateSnapshot();
|
||||
options.isAreaDrawerOpen.value = false;
|
||||
message.success(
|
||||
options.areaDrawerMode.value === 'edit' ? '区域已保存' : '区域已添加',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingArea.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除区域。 */
|
||||
async function handleDeleteArea(area: DineInAreaDto) {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
|
||||
const tableCount = countAreaTables(area.id, options.tables.value);
|
||||
if (tableCount > 0) {
|
||||
message.error('该区域仍有桌位,请先迁移或删除桌位');
|
||||
return;
|
||||
}
|
||||
|
||||
options.isSavingArea.value = true;
|
||||
try {
|
||||
await deleteDineInAreaApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
areaId: area.id,
|
||||
});
|
||||
options.areas.value = options.areas.value.filter(
|
||||
(item) => item.id !== area.id,
|
||||
);
|
||||
options.fixSelectedArea();
|
||||
options.updateSnapshot();
|
||||
message.success('区域已删除');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingArea.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleDeleteArea,
|
||||
handleSubmitArea,
|
||||
openAreaDrawer,
|
||||
setAreaDescription,
|
||||
setAreaDrawerOpen,
|
||||
setAreaName,
|
||||
setAreaSort,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 文件职责:堂食管理页面静态常量。
|
||||
* 1. 维护默认区域、桌位、基础设置。
|
||||
* 2. 提供状态、座位数等选项映射。
|
||||
*/
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInBasicSettingsDto,
|
||||
DineInEditableStatus,
|
||||
DineInTableDto,
|
||||
DineInTableStatus,
|
||||
} from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInSeatsOption,
|
||||
DineInStatusOption,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
export const DINE_IN_SEATS_OPTIONS: DineInSeatsOption[] = [
|
||||
{ label: '2人桌', value: 2 },
|
||||
{ label: '4人桌', value: 4 },
|
||||
{ label: '6人桌', value: 6 },
|
||||
{ label: '8人桌', value: 8 },
|
||||
{ label: '10人桌', value: 10 },
|
||||
{ label: '12人桌', value: 12 },
|
||||
];
|
||||
|
||||
export const DINE_IN_STATUS_MAP: Record<DineInTableStatus, DineInStatusOption> =
|
||||
{
|
||||
free: { value: 'free', label: '空闲', color: '#22c55e', className: 'free' },
|
||||
dining: {
|
||||
value: 'dining',
|
||||
label: '就餐中',
|
||||
color: '#f59e0b',
|
||||
className: 'dining',
|
||||
},
|
||||
reserved: {
|
||||
value: 'reserved',
|
||||
label: '已预约',
|
||||
color: '#1677ff',
|
||||
className: 'reserved',
|
||||
},
|
||||
disabled: {
|
||||
value: 'disabled',
|
||||
label: '停用',
|
||||
color: '#9ca3af',
|
||||
className: 'disabled',
|
||||
},
|
||||
};
|
||||
|
||||
export const DINE_IN_EDITABLE_STATUS_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: DineInEditableStatus;
|
||||
}> = [
|
||||
{ label: '空闲', value: 'free' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
];
|
||||
|
||||
export const DEFAULT_DINE_IN_BASIC_SETTINGS: DineInBasicSettingsDto = {
|
||||
enabled: true,
|
||||
defaultDiningMinutes: 90,
|
||||
overtimeReminderMinutes: 10,
|
||||
};
|
||||
|
||||
export const DEFAULT_DINE_IN_AREAS: DineInAreaDto[] = [
|
||||
{
|
||||
id: 'dinein-area-hall',
|
||||
name: '大厅',
|
||||
description: '主要用餐区域,共12张桌位,可容纳约48人同时用餐',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
id: 'dinein-area-private-room',
|
||||
name: '包间',
|
||||
description: '安静独立区域,适合聚餐与商务接待',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
id: 'dinein-area-terrace',
|
||||
name: '露台',
|
||||
description: '开放式外摆区域,适合休闲场景',
|
||||
sort: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_DINE_IN_TABLES: DineInTableDto[] = [
|
||||
{
|
||||
id: 'dinein-table-a01',
|
||||
code: 'A01',
|
||||
areaId: 'dinein-area-hall',
|
||||
seats: 4,
|
||||
status: 'free',
|
||||
tags: ['靠窗'],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-a02',
|
||||
code: 'A02',
|
||||
areaId: 'dinein-area-hall',
|
||||
seats: 2,
|
||||
status: 'dining',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-a03',
|
||||
code: 'A03',
|
||||
areaId: 'dinein-area-hall',
|
||||
seats: 6,
|
||||
status: 'free',
|
||||
tags: ['VIP', '靠窗'],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-a04',
|
||||
code: 'A04',
|
||||
areaId: 'dinein-area-hall',
|
||||
seats: 4,
|
||||
status: 'reserved',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-a07',
|
||||
code: 'A07',
|
||||
areaId: 'dinein-area-hall',
|
||||
seats: 4,
|
||||
status: 'disabled',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-v01',
|
||||
code: 'V01',
|
||||
areaId: 'dinein-area-private-room',
|
||||
seats: 8,
|
||||
status: 'dining',
|
||||
tags: ['包厢'],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-v02',
|
||||
code: 'V02',
|
||||
areaId: 'dinein-area-private-room',
|
||||
seats: 6,
|
||||
status: 'free',
|
||||
tags: ['VIP'],
|
||||
},
|
||||
{
|
||||
id: 'dinein-table-t01',
|
||||
code: 'T01',
|
||||
areaId: 'dinein-area-terrace',
|
||||
seats: 4,
|
||||
status: 'free',
|
||||
tags: ['露台'],
|
||||
},
|
||||
];
|
||||
|
||||
export const TABLE_TAG_SUGGESTIONS = [
|
||||
'VIP',
|
||||
'包厢',
|
||||
'吧台',
|
||||
'安静区',
|
||||
'家庭位',
|
||||
'靠窗',
|
||||
];
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 文件职责:堂食管理复制动作。
|
||||
* 1. 管理复制弹窗状态与目标门店勾选。
|
||||
* 2. 提交复制请求并反馈结果。
|
||||
*/
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { copyStoreDineInSettingsApi } from '#/api/store-dinein';
|
||||
|
||||
interface CreateCopyActionsOptions {
|
||||
copyCandidates: ComputedRef<StoreListItemDto[]>;
|
||||
copyTargetStoreIds: Ref<string[]>;
|
||||
isCopyModalOpen: Ref<boolean>;
|
||||
isCopySubmitting: Ref<boolean>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
export function createCopyActions(options: CreateCopyActionsOptions) {
|
||||
/** 打开复制弹窗。 */
|
||||
function openCopyModal() {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
options.copyTargetStoreIds.value = [];
|
||||
options.isCopyModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 切换单个目标门店。 */
|
||||
function toggleCopyStore(storeId: string, checked: boolean) {
|
||||
options.copyTargetStoreIds.value = checked
|
||||
? [...new Set([storeId, ...options.copyTargetStoreIds.value])]
|
||||
: options.copyTargetStoreIds.value.filter((id) => id !== storeId);
|
||||
}
|
||||
|
||||
/** 全选/取消全选。 */
|
||||
function handleCopyCheckAll(checked: boolean) {
|
||||
options.copyTargetStoreIds.value = checked
|
||||
? options.copyCandidates.value.map((item) => item.id)
|
||||
: [];
|
||||
}
|
||||
|
||||
/** 提交复制。 */
|
||||
async function handleCopySubmit() {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
if (options.copyTargetStoreIds.value.length === 0) {
|
||||
message.error('请至少选择一个目标门店');
|
||||
return;
|
||||
}
|
||||
|
||||
options.isCopySubmitting.value = true;
|
||||
try {
|
||||
await copyStoreDineInSettingsApi({
|
||||
sourceStoreId: options.selectedStoreId.value,
|
||||
targetStoreIds: options.copyTargetStoreIds.value,
|
||||
});
|
||||
message.success(
|
||||
`已复制到 ${options.copyTargetStoreIds.value.length} 家门店`,
|
||||
);
|
||||
options.isCopyModalOpen.value = false;
|
||||
options.copyTargetStoreIds.value = [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isCopySubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
openCopyModal,
|
||||
toggleCopyStore,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 文件职责:堂食管理数据动作。
|
||||
* 1. 加载门店列表与门店堂食设置。
|
||||
* 2. 保存基础设置并维护快照。
|
||||
*/
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInBasicSettingsDto,
|
||||
DineInTableDto,
|
||||
} from '#/api/store-dinein';
|
||||
import type { DineInSettingsSnapshot } from '#/views/store/dine-in/types';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getStoreListApi } from '#/api/store';
|
||||
import {
|
||||
getStoreDineInSettingsApi,
|
||||
saveStoreDineInBasicSettingsApi,
|
||||
} from '#/api/store-dinein';
|
||||
|
||||
import {
|
||||
DEFAULT_DINE_IN_AREAS,
|
||||
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||
DEFAULT_DINE_IN_TABLES,
|
||||
} from './constants';
|
||||
import {
|
||||
cloneAreas,
|
||||
cloneBasicSettings,
|
||||
cloneTables,
|
||||
createSettingsSnapshot,
|
||||
sortAreas,
|
||||
sortTables,
|
||||
} from './helpers';
|
||||
|
||||
interface CreateDataActionsOptions {
|
||||
areas: Ref<DineInAreaDto[]>;
|
||||
basicSettings: DineInBasicSettingsDto;
|
||||
isPageLoading: Ref<boolean>;
|
||||
isSavingBasic: Ref<boolean>;
|
||||
isStoreLoading: Ref<boolean>;
|
||||
selectedAreaId: Ref<string>;
|
||||
selectedStoreId: Ref<string>;
|
||||
snapshot: Ref<DineInSettingsSnapshot | null>;
|
||||
stores: Ref<StoreListItemDto[]>;
|
||||
tables: Ref<DineInTableDto[]>;
|
||||
}
|
||||
|
||||
export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 同步基础设置,保持 reactive 引用不变。 */
|
||||
function syncBasicSettings(next: DineInBasicSettingsDto) {
|
||||
options.basicSettings.enabled = next.enabled;
|
||||
options.basicSettings.defaultDiningMinutes = next.defaultDiningMinutes;
|
||||
options.basicSettings.overtimeReminderMinutes =
|
||||
next.overtimeReminderMinutes;
|
||||
}
|
||||
|
||||
/** 应用默认配置(接口异常兜底)。 */
|
||||
function applyDefaultSettings() {
|
||||
options.areas.value = sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS));
|
||||
options.tables.value = sortTables(cloneTables(DEFAULT_DINE_IN_TABLES));
|
||||
syncBasicSettings(cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS));
|
||||
options.selectedAreaId.value = options.areas.value[0]?.id ?? '';
|
||||
}
|
||||
|
||||
/** 构建当前快照。 */
|
||||
function buildCurrentSnapshot() {
|
||||
return createSettingsSnapshot({
|
||||
areas: options.areas.value,
|
||||
tables: options.tables.value,
|
||||
basicSettings: options.basicSettings,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据当前区域合法性回填选中值。 */
|
||||
function fixSelectedArea() {
|
||||
if (options.areas.value.length === 0) {
|
||||
options.selectedAreaId.value = '';
|
||||
return;
|
||||
}
|
||||
const hasSelected = options.areas.value.some(
|
||||
(area) => area.id === options.selectedAreaId.value,
|
||||
);
|
||||
if (!hasSelected) {
|
||||
options.selectedAreaId.value = options.areas.value[0]?.id ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载门店堂食设置。 */
|
||||
async function loadStoreSettings(storeId: string) {
|
||||
options.isPageLoading.value = true;
|
||||
try {
|
||||
const currentStoreId = storeId;
|
||||
const result = await getStoreDineInSettingsApi(storeId);
|
||||
if (options.selectedStoreId.value !== currentStoreId) return;
|
||||
|
||||
options.areas.value = sortAreas(
|
||||
result.areas?.length > 0
|
||||
? result.areas
|
||||
: cloneAreas(DEFAULT_DINE_IN_AREAS),
|
||||
);
|
||||
options.tables.value = sortTables(
|
||||
result.tables?.length > 0
|
||||
? result.tables
|
||||
: cloneTables(DEFAULT_DINE_IN_TABLES),
|
||||
);
|
||||
syncBasicSettings({
|
||||
...DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||
...result.basicSettings,
|
||||
});
|
||||
fixSelectedArea();
|
||||
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
applyDefaultSettings();
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
} finally {
|
||||
options.isPageLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载门店列表。 */
|
||||
async function loadStores() {
|
||||
options.isStoreLoading.value = true;
|
||||
try {
|
||||
const result = await getStoreListApi({
|
||||
keyword: undefined,
|
||||
businessStatus: undefined,
|
||||
auditStatus: undefined,
|
||||
serviceType: undefined,
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
options.stores.value = result.items ?? [];
|
||||
|
||||
if (options.stores.value.length === 0) {
|
||||
options.selectedStoreId.value = '';
|
||||
applyDefaultSettings();
|
||||
options.snapshot.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSelectedStore = options.stores.value.some(
|
||||
(item) => item.id === options.selectedStoreId.value,
|
||||
);
|
||||
if (!hasSelectedStore) {
|
||||
const firstStore = options.stores.value[0];
|
||||
if (firstStore) options.selectedStoreId.value = firstStore.id;
|
||||
return;
|
||||
}
|
||||
if (options.selectedStoreId.value) {
|
||||
await loadStoreSettings(options.selectedStoreId.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
options.stores.value = [];
|
||||
options.selectedStoreId.value = '';
|
||||
applyDefaultSettings();
|
||||
options.snapshot.value = null;
|
||||
} finally {
|
||||
options.isStoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存基础设置。 */
|
||||
async function saveBasicSettings() {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
options.isSavingBasic.value = true;
|
||||
try {
|
||||
await saveStoreDineInBasicSettingsApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
basicSettings: cloneBasicSettings(options.basicSettings),
|
||||
});
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
message.success('堂食设置已保存');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingBasic.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置基础设置到最近快照。 */
|
||||
function resetBasicSettings() {
|
||||
const source =
|
||||
options.snapshot.value?.basicSettings ??
|
||||
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS);
|
||||
syncBasicSettings(source);
|
||||
message.success('已恢复到最近一次保存状态');
|
||||
}
|
||||
|
||||
return {
|
||||
buildCurrentSnapshot,
|
||||
fixSelectedArea,
|
||||
loadStoreSettings,
|
||||
loadStores,
|
||||
resetBasicSettings,
|
||||
saveBasicSettings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 文件职责:堂食管理页面纯函数工具。
|
||||
* 1. 负责克隆、排序、格式化、校验等纯逻辑。
|
||||
* 2. 负责批量编号预览与冲突检测。
|
||||
*/
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInBasicSettingsDto,
|
||||
DineInTableDto,
|
||||
DineInTableStatus,
|
||||
} from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInAreaFormState,
|
||||
DineInBatchFormState,
|
||||
DineInSettingsSnapshot,
|
||||
DineInTableFormState,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
/** 深拷贝堂食设置。 */
|
||||
export function cloneBasicSettings(source: DineInBasicSettingsDto) {
|
||||
return { ...source };
|
||||
}
|
||||
|
||||
/** 深拷贝区域列表。 */
|
||||
export function cloneAreas(source: DineInAreaDto[]) {
|
||||
return source.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
/** 深拷贝桌位列表。 */
|
||||
export function cloneTables(source: DineInTableDto[]) {
|
||||
return source.map((item) => ({ ...item, tags: [...item.tags] }));
|
||||
}
|
||||
|
||||
/** 组装设置快照。 */
|
||||
export function createSettingsSnapshot(payload: {
|
||||
areas: DineInAreaDto[];
|
||||
basicSettings: DineInBasicSettingsDto;
|
||||
tables: DineInTableDto[];
|
||||
}): DineInSettingsSnapshot {
|
||||
return {
|
||||
basicSettings: cloneBasicSettings(payload.basicSettings),
|
||||
areas: cloneAreas(payload.areas),
|
||||
tables: cloneTables(payload.tables),
|
||||
};
|
||||
}
|
||||
|
||||
/** 按排序字段与名称稳定排序区域。 */
|
||||
export function sortAreas(source: DineInAreaDto[]) {
|
||||
return cloneAreas(source).toSorted((a, b) => {
|
||||
const sortDiff = a.sort - b.sort;
|
||||
if (sortDiff !== 0) return sortDiff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
/** 按编号排序桌位。 */
|
||||
export function sortTables(source: DineInTableDto[]) {
|
||||
return cloneTables(source).toSorted((a, b) => a.code.localeCompare(b.code));
|
||||
}
|
||||
|
||||
/** 生成唯一 ID。 */
|
||||
export function createDineInId(prefix: 'area' | 'table') {
|
||||
return `dinein-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
/** 规范化桌位编号(大写 + 去空格)。 */
|
||||
export function normalizeTableCode(code: string) {
|
||||
return code.trim().toUpperCase();
|
||||
}
|
||||
|
||||
/** 统计区域下桌位数量。 */
|
||||
export function countAreaTables(areaId: string, tables: DineInTableDto[]) {
|
||||
return tables.filter((item) => item.areaId === areaId).length;
|
||||
}
|
||||
|
||||
/** 根据状态计算样式 class。 */
|
||||
export function resolveStatusClassName(status: DineInTableStatus) {
|
||||
return `status-${status}`;
|
||||
}
|
||||
|
||||
/** 批量生成编号预览。 */
|
||||
export function generateBatchCodes(payload: {
|
||||
codePrefix: string;
|
||||
count: number;
|
||||
startNumber: number;
|
||||
}) {
|
||||
const prefix = payload.codePrefix.trim().toUpperCase() || 'A';
|
||||
const start = Math.max(1, Math.floor(payload.startNumber));
|
||||
const count = Math.max(1, Math.min(50, Math.floor(payload.count)));
|
||||
const width = Math.max(String(start + count - 1).length, 2);
|
||||
|
||||
return Array.from({ length: count }).map((_, index) => {
|
||||
const codeNumber = String(start + index).padStart(width, '0');
|
||||
return `${prefix}${codeNumber}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** 校验区域表单。 */
|
||||
export function validateAreaForm(payload: {
|
||||
areaId?: string;
|
||||
areas: DineInAreaDto[];
|
||||
form: DineInAreaFormState;
|
||||
}) {
|
||||
if (!payload.form.name.trim()) return '请输入区域名称';
|
||||
if (payload.form.sort <= 0) return '排序必须大于 0';
|
||||
|
||||
const duplicated = payload.areas.some((item) => {
|
||||
if (payload.areaId && item.id === payload.areaId) return false;
|
||||
return item.name.trim() === payload.form.name.trim();
|
||||
});
|
||||
if (duplicated) return '区域名称已存在,请更换';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 校验桌位表单。 */
|
||||
export function validateTableForm(payload: {
|
||||
form: DineInTableFormState;
|
||||
tableId?: string;
|
||||
tables: DineInTableDto[];
|
||||
}) {
|
||||
const code = normalizeTableCode(payload.form.code);
|
||||
if (!code) return '请输入桌位编号';
|
||||
if (!payload.form.areaId) return '请选择所属区域';
|
||||
if (payload.form.seats <= 0) return '座位数必须大于 0';
|
||||
|
||||
const duplicated = payload.tables.some((item) => {
|
||||
if (payload.tableId && item.id === payload.tableId) return false;
|
||||
return item.code === code;
|
||||
});
|
||||
if (duplicated) return '桌位编号已存在,请更换';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 校验批量生成表单。 */
|
||||
export function validateBatchForm(payload: {
|
||||
existingCodes: string[];
|
||||
form: DineInBatchFormState;
|
||||
}) {
|
||||
if (!payload.form.areaId) return '请选择所属区域';
|
||||
if (!payload.form.codePrefix.trim()) return '请输入编号前缀';
|
||||
if (payload.form.startNumber <= 0) return '起始编号必须大于 0';
|
||||
if (payload.form.count <= 0 || payload.form.count > 50)
|
||||
return '生成数量范围为 1-50';
|
||||
if (payload.form.seats <= 0) return '座位数必须大于 0';
|
||||
|
||||
const generatedCodes = generateBatchCodes(payload.form);
|
||||
const existingSet = new Set(
|
||||
payload.existingCodes.map((item) => item.toUpperCase()),
|
||||
);
|
||||
const conflictCode = generatedCodes.find((code) => existingSet.has(code));
|
||||
if (conflictCode) return `桌位编号 ${conflictCode} 已存在,请调整后重试`;
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 文件职责:堂食桌位动作。
|
||||
* 1. 管理桌位抽屉与批量生成弹窗状态。
|
||||
* 2. 处理桌位新增、编辑、删除与批量生成流程。
|
||||
*/
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInTableDto,
|
||||
DineInTableStatus,
|
||||
} from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInBatchFormState,
|
||||
DineInTableDrawerMode,
|
||||
DineInTableFormState,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
batchCreateDineInTablesApi,
|
||||
deleteDineInTableApi,
|
||||
saveDineInTableApi,
|
||||
} from '#/api/store-dinein';
|
||||
|
||||
import {
|
||||
createDineInId,
|
||||
generateBatchCodes,
|
||||
normalizeTableCode,
|
||||
sortTables,
|
||||
validateBatchForm,
|
||||
validateTableForm,
|
||||
} from './helpers';
|
||||
|
||||
interface CreateTableActionsOptions {
|
||||
areas: Ref<DineInAreaDto[]>;
|
||||
batchForm: DineInBatchFormState;
|
||||
batchPreviewCodes: ComputedRef<string[]>;
|
||||
isBatchModalOpen: Ref<boolean>;
|
||||
isSavingBatch: Ref<boolean>;
|
||||
isSavingTable: Ref<boolean>;
|
||||
isTableDrawerOpen: Ref<boolean>;
|
||||
selectedStoreId: Ref<string>;
|
||||
selectedTableAreaId: Ref<string>;
|
||||
tableDrawerMode: Ref<DineInTableDrawerMode>;
|
||||
tableForm: DineInTableFormState;
|
||||
tables: Ref<DineInTableDto[]>;
|
||||
updateSnapshot: () => void;
|
||||
}
|
||||
|
||||
export function createTableActions(options: CreateTableActionsOptions) {
|
||||
/** 打开桌位抽屉并初始化表单。 */
|
||||
function openTableDrawer(
|
||||
mode: DineInTableDrawerMode,
|
||||
table?: DineInTableDto,
|
||||
) {
|
||||
options.tableDrawerMode.value = mode;
|
||||
if (mode === 'edit' && table) {
|
||||
options.tableForm.id = table.id;
|
||||
options.tableForm.code = table.code;
|
||||
options.tableForm.areaId = table.areaId;
|
||||
options.tableForm.seats = table.seats;
|
||||
options.tableForm.tags = [...table.tags];
|
||||
options.tableForm.sourceStatus = table.status;
|
||||
options.tableForm.isDisabled = table.status === 'disabled';
|
||||
options.isTableDrawerOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
options.tableForm.id = '';
|
||||
options.tableForm.code = '';
|
||||
options.tableForm.areaId = options.selectedTableAreaId.value;
|
||||
options.tableForm.seats = 4;
|
||||
options.tableForm.tags = [];
|
||||
options.tableForm.sourceStatus = 'free';
|
||||
options.tableForm.isDisabled = false;
|
||||
options.isTableDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 控制桌位抽屉显隐。 */
|
||||
function setTableDrawerOpen(value: boolean) {
|
||||
options.isTableDrawerOpen.value = value;
|
||||
}
|
||||
|
||||
function setTableCode(value: string) {
|
||||
options.tableForm.code = value;
|
||||
}
|
||||
|
||||
function setTableAreaId(value: string) {
|
||||
options.tableForm.areaId = value;
|
||||
}
|
||||
|
||||
function setTableSeats(value: number) {
|
||||
options.tableForm.seats = Math.max(1, Math.floor(Number(value || 1)));
|
||||
}
|
||||
|
||||
function setTableDisabled(value: boolean) {
|
||||
options.tableForm.isDisabled = Boolean(value);
|
||||
}
|
||||
|
||||
function setTableTags(tags: string[]) {
|
||||
options.tableForm.tags = [
|
||||
...new Set(tags.map((item) => item.trim()).filter(Boolean)),
|
||||
];
|
||||
}
|
||||
|
||||
/** 提交桌位表单。 */
|
||||
async function handleSubmitTable() {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
const validateMessage = validateTableForm({
|
||||
tableId: options.tableForm.id,
|
||||
form: options.tableForm,
|
||||
tables: options.tables.value,
|
||||
});
|
||||
if (validateMessage) {
|
||||
message.error(validateMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
options.isSavingTable.value = true;
|
||||
try {
|
||||
const tableId = options.tableForm.id || createDineInId('table');
|
||||
let nextStatus: DineInTableStatus = options.tableForm.sourceStatus;
|
||||
if (options.tableForm.isDisabled) {
|
||||
nextStatus = 'disabled';
|
||||
} else if (options.tableForm.sourceStatus === 'disabled') {
|
||||
nextStatus = 'free';
|
||||
}
|
||||
|
||||
const tablePayload: DineInTableDto = {
|
||||
id: tableId,
|
||||
code: normalizeTableCode(options.tableForm.code),
|
||||
areaId: options.tableForm.areaId,
|
||||
seats: options.tableForm.seats,
|
||||
status: nextStatus,
|
||||
tags: [...options.tableForm.tags],
|
||||
};
|
||||
|
||||
await saveDineInTableApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
table: tablePayload,
|
||||
});
|
||||
|
||||
options.tables.value =
|
||||
options.tableDrawerMode.value === 'edit' && options.tableForm.id
|
||||
? sortTables(
|
||||
options.tables.value.map((item) =>
|
||||
item.id === options.tableForm.id ? tablePayload : item,
|
||||
),
|
||||
)
|
||||
: sortTables([...options.tables.value, tablePayload]);
|
||||
|
||||
options.updateSnapshot();
|
||||
options.isTableDrawerOpen.value = false;
|
||||
message.success(
|
||||
options.tableDrawerMode.value === 'edit' ? '桌位已保存' : '桌位已添加',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingTable.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除桌位。 */
|
||||
async function handleDeleteTable(tableId: string) {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
options.isSavingTable.value = true;
|
||||
try {
|
||||
await deleteDineInTableApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
tableId,
|
||||
});
|
||||
options.tables.value = options.tables.value.filter(
|
||||
(item) => item.id !== tableId,
|
||||
);
|
||||
options.updateSnapshot();
|
||||
message.success('桌位已删除');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingTable.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开批量生成弹窗并初始化参数。 */
|
||||
function openBatchModal() {
|
||||
options.batchForm.areaId = options.selectedTableAreaId.value;
|
||||
options.batchForm.codePrefix = 'A';
|
||||
options.batchForm.startNumber = 1;
|
||||
options.batchForm.count = 4;
|
||||
options.batchForm.seats = 4;
|
||||
options.isBatchModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 控制批量弹窗显隐。 */
|
||||
function setBatchModalOpen(value: boolean) {
|
||||
options.isBatchModalOpen.value = value;
|
||||
}
|
||||
|
||||
function setBatchAreaId(value: string) {
|
||||
options.batchForm.areaId = value;
|
||||
}
|
||||
|
||||
function setBatchCodePrefix(value: string) {
|
||||
options.batchForm.codePrefix = value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
function setBatchStartNumber(value: number) {
|
||||
options.batchForm.startNumber = Math.max(1, Math.floor(Number(value || 1)));
|
||||
}
|
||||
|
||||
function setBatchCount(value: number) {
|
||||
options.batchForm.count = Math.max(
|
||||
1,
|
||||
Math.min(50, Math.floor(Number(value || 1))),
|
||||
);
|
||||
}
|
||||
|
||||
function setBatchSeats(value: number) {
|
||||
options.batchForm.seats = Math.max(1, Math.floor(Number(value || 1)));
|
||||
}
|
||||
|
||||
/** 提交批量生成。 */
|
||||
async function handleSubmitBatch() {
|
||||
if (!options.selectedStoreId.value) return;
|
||||
|
||||
const validateMessage = validateBatchForm({
|
||||
form: options.batchForm,
|
||||
existingCodes: options.tables.value.map((item) => item.code),
|
||||
});
|
||||
if (validateMessage) {
|
||||
message.error(validateMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
options.isSavingBatch.value = true;
|
||||
try {
|
||||
await batchCreateDineInTablesApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
areaId: options.batchForm.areaId,
|
||||
codePrefix: options.batchForm.codePrefix,
|
||||
startNumber: options.batchForm.startNumber,
|
||||
count: options.batchForm.count,
|
||||
seats: options.batchForm.seats,
|
||||
});
|
||||
|
||||
const createdTables: DineInTableDto[] =
|
||||
options.batchPreviewCodes.value.map((code) => ({
|
||||
id: createDineInId('table'),
|
||||
areaId: options.batchForm.areaId,
|
||||
code,
|
||||
seats: options.batchForm.seats,
|
||||
status: 'free',
|
||||
tags: [],
|
||||
}));
|
||||
|
||||
options.tables.value = sortTables([
|
||||
...options.tables.value,
|
||||
...createdTables,
|
||||
]);
|
||||
options.updateSnapshot();
|
||||
options.isBatchModalOpen.value = false;
|
||||
message.success(`已生成 ${createdTables.length} 张桌位`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSavingBatch.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
generateBatchCodes,
|
||||
handleDeleteTable,
|
||||
handleSubmitBatch,
|
||||
handleSubmitTable,
|
||||
openBatchModal,
|
||||
openTableDrawer,
|
||||
setBatchAreaId,
|
||||
setBatchCodePrefix,
|
||||
setBatchCount,
|
||||
setBatchModalOpen,
|
||||
setBatchSeats,
|
||||
setBatchStartNumber,
|
||||
setTableAreaId,
|
||||
setTableCode,
|
||||
setTableDisabled,
|
||||
setTableDrawerOpen,
|
||||
setTableSeats,
|
||||
setTableTags,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* 文件职责:堂食管理页面主编排。
|
||||
* 1. 维护页面级状态(门店、区域、桌位、设置、抽屉、弹窗)。
|
||||
* 2. 组合数据加载、复制、区域/桌位动作。
|
||||
* 3. 对外暴露视图层可直接消费的状态与方法。
|
||||
*/
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInBasicSettingsDto,
|
||||
DineInTableDto,
|
||||
} from '#/api/store-dinein';
|
||||
import type {
|
||||
DineInAreaDrawerMode,
|
||||
DineInAreaFormState,
|
||||
DineInBatchFormState,
|
||||
DineInSettingsSnapshot,
|
||||
DineInTableDrawerMode,
|
||||
DineInTableFormState,
|
||||
} from '#/views/store/dine-in/types';
|
||||
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { createAreaActions } from './dinein-page/area-actions';
|
||||
import {
|
||||
DEFAULT_DINE_IN_AREAS,
|
||||
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||
DEFAULT_DINE_IN_TABLES,
|
||||
DINE_IN_SEATS_OPTIONS,
|
||||
DINE_IN_STATUS_MAP,
|
||||
TABLE_TAG_SUGGESTIONS,
|
||||
} from './dinein-page/constants';
|
||||
import { createCopyActions } from './dinein-page/copy-actions';
|
||||
import { createDataActions } from './dinein-page/data-actions';
|
||||
import {
|
||||
cloneAreas,
|
||||
cloneBasicSettings,
|
||||
cloneTables,
|
||||
countAreaTables,
|
||||
createSettingsSnapshot,
|
||||
generateBatchCodes,
|
||||
resolveStatusClassName,
|
||||
sortAreas,
|
||||
sortTables,
|
||||
} from './dinein-page/helpers';
|
||||
import { createTableActions } from './dinein-page/table-actions';
|
||||
|
||||
export function useStoreDineInPage() {
|
||||
// 1. 页面 loading / submitting 状态。
|
||||
const isStoreLoading = ref(false);
|
||||
const isPageLoading = ref(false);
|
||||
const isSavingBasic = ref(false);
|
||||
const isSavingArea = ref(false);
|
||||
const isSavingTable = ref(false);
|
||||
const isSavingBatch = ref(false);
|
||||
const isCopySubmitting = ref(false);
|
||||
|
||||
// 2. 页面核心业务数据。
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const areas = ref<DineInAreaDto[]>(
|
||||
sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS)),
|
||||
);
|
||||
const tables = ref<DineInTableDto[]>(
|
||||
sortTables(cloneTables(DEFAULT_DINE_IN_TABLES)),
|
||||
);
|
||||
const basicSettings = reactive<DineInBasicSettingsDto>(
|
||||
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS),
|
||||
);
|
||||
const selectedAreaId = ref(areas.value[0]?.id ?? '');
|
||||
const snapshot = ref<DineInSettingsSnapshot | null>(
|
||||
createSettingsSnapshot({
|
||||
areas: areas.value,
|
||||
tables: tables.value,
|
||||
basicSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. 复制弹窗状态。
|
||||
const isCopyModalOpen = ref(false);
|
||||
const copyTargetStoreIds = ref<string[]>([]);
|
||||
|
||||
// 4. 区域抽屉状态。
|
||||
const isAreaDrawerOpen = ref(false);
|
||||
const areaDrawerMode = ref<DineInAreaDrawerMode>('create');
|
||||
const areaForm = reactive<DineInAreaFormState>({
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
sort: 1,
|
||||
});
|
||||
|
||||
// 5. 桌位抽屉与批量弹窗状态。
|
||||
const isTableDrawerOpen = ref(false);
|
||||
const tableDrawerMode = ref<DineInTableDrawerMode>('create');
|
||||
const tableForm = reactive<DineInTableFormState>({
|
||||
id: '',
|
||||
code: '',
|
||||
areaId: selectedAreaId.value,
|
||||
seats: 4,
|
||||
tags: [],
|
||||
sourceStatus: 'free',
|
||||
isDisabled: false,
|
||||
});
|
||||
const isBatchModalOpen = ref(false);
|
||||
const batchForm = reactive<DineInBatchFormState>({
|
||||
areaId: selectedAreaId.value,
|
||||
codePrefix: 'A',
|
||||
startNumber: 1,
|
||||
count: 4,
|
||||
seats: 4,
|
||||
});
|
||||
|
||||
// 6. 页面衍生视图数据。
|
||||
const storeOptions = computed(() =>
|
||||
stores.value.map((store) => ({ label: store.name, value: store.id })),
|
||||
);
|
||||
|
||||
const selectedStoreName = computed(
|
||||
() =>
|
||||
stores.value.find((store) => store.id === selectedStoreId.value)?.name ??
|
||||
'',
|
||||
);
|
||||
|
||||
const selectedArea = computed(
|
||||
() =>
|
||||
areas.value.find((area) => area.id === selectedAreaId.value) ??
|
||||
areas.value[0],
|
||||
);
|
||||
|
||||
const selectedAreaTableCount = computed(() => {
|
||||
const area = selectedArea.value;
|
||||
if (!area) return 0;
|
||||
return countAreaTables(area.id, tables.value);
|
||||
});
|
||||
|
||||
const filteredTables = computed(() => {
|
||||
const area = selectedArea.value;
|
||||
if (!area) return [];
|
||||
return tables.value.filter((table) => table.areaId === area.id);
|
||||
});
|
||||
|
||||
const areaOptions = computed(() =>
|
||||
areas.value.map((area) => ({ label: area.name, value: area.id })),
|
||||
);
|
||||
|
||||
const copyCandidates = computed(() =>
|
||||
stores.value.filter((store) => store.id !== selectedStoreId.value),
|
||||
);
|
||||
|
||||
const isCopyAllChecked = computed(
|
||||
() =>
|
||||
copyCandidates.value.length > 0 &&
|
||||
copyTargetStoreIds.value.length === copyCandidates.value.length,
|
||||
);
|
||||
|
||||
const isCopyIndeterminate = computed(
|
||||
() =>
|
||||
copyTargetStoreIds.value.length > 0 &&
|
||||
copyTargetStoreIds.value.length < copyCandidates.value.length,
|
||||
);
|
||||
|
||||
const batchPreviewCodes = computed(() => generateBatchCodes(batchForm));
|
||||
|
||||
const areaDrawerTitle = computed(() =>
|
||||
areaDrawerMode.value === 'edit' ? '编辑区域' : '添加区域',
|
||||
);
|
||||
|
||||
const areaSubmitText = computed(() =>
|
||||
areaDrawerMode.value === 'edit' ? '保存修改' : '确认添加',
|
||||
);
|
||||
|
||||
const tableDrawerTitle = computed(() =>
|
||||
tableDrawerMode.value === 'edit'
|
||||
? `编辑桌位 - ${tableForm.code || '--'}`
|
||||
: '添加桌位',
|
||||
);
|
||||
|
||||
const tableSubmitText = computed(() =>
|
||||
tableDrawerMode.value === 'edit' ? '保存修改' : '确认添加',
|
||||
);
|
||||
|
||||
// 7. 数据域动作装配。
|
||||
const {
|
||||
buildCurrentSnapshot,
|
||||
fixSelectedArea,
|
||||
loadStoreSettings,
|
||||
loadStores,
|
||||
resetBasicSettings,
|
||||
saveBasicSettings,
|
||||
} = createDataActions({
|
||||
areas,
|
||||
basicSettings,
|
||||
isPageLoading,
|
||||
isSavingBasic,
|
||||
isStoreLoading,
|
||||
selectedAreaId,
|
||||
selectedStoreId,
|
||||
snapshot,
|
||||
stores,
|
||||
tables,
|
||||
});
|
||||
|
||||
const {
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
openCopyModal,
|
||||
toggleCopyStore,
|
||||
} = createCopyActions({
|
||||
copyCandidates,
|
||||
copyTargetStoreIds,
|
||||
isCopyModalOpen,
|
||||
isCopySubmitting,
|
||||
selectedStoreId,
|
||||
});
|
||||
|
||||
/** 更新快照供重置使用。 */
|
||||
function updateSnapshot() {
|
||||
snapshot.value = buildCurrentSnapshot();
|
||||
}
|
||||
|
||||
const {
|
||||
handleDeleteArea,
|
||||
handleSubmitArea,
|
||||
openAreaDrawer,
|
||||
setAreaDescription,
|
||||
setAreaDrawerOpen,
|
||||
setAreaName,
|
||||
setAreaSort,
|
||||
} = createAreaActions({
|
||||
areaDrawerMode,
|
||||
areaForm,
|
||||
areas,
|
||||
fixSelectedArea,
|
||||
isAreaDrawerOpen,
|
||||
isSavingArea,
|
||||
selectedAreaId,
|
||||
selectedStoreId,
|
||||
tables,
|
||||
updateSnapshot,
|
||||
});
|
||||
|
||||
const {
|
||||
handleDeleteTable,
|
||||
handleSubmitBatch,
|
||||
handleSubmitTable,
|
||||
openBatchModal,
|
||||
openTableDrawer,
|
||||
setBatchAreaId,
|
||||
setBatchCodePrefix,
|
||||
setBatchCount,
|
||||
setBatchModalOpen,
|
||||
setBatchSeats,
|
||||
setBatchStartNumber,
|
||||
setTableAreaId,
|
||||
setTableCode,
|
||||
setTableDisabled,
|
||||
setTableDrawerOpen,
|
||||
setTableSeats,
|
||||
setTableTags,
|
||||
} = createTableActions({
|
||||
areas,
|
||||
batchForm,
|
||||
batchPreviewCodes,
|
||||
isBatchModalOpen,
|
||||
isSavingBatch,
|
||||
isSavingTable,
|
||||
isTableDrawerOpen,
|
||||
selectedStoreId,
|
||||
selectedTableAreaId: selectedAreaId,
|
||||
tableDrawerMode,
|
||||
tableForm,
|
||||
tables,
|
||||
updateSnapshot,
|
||||
});
|
||||
|
||||
// 8. 页面字段更新方法。
|
||||
function setSelectedStoreId(value: string) {
|
||||
selectedStoreId.value = value;
|
||||
}
|
||||
|
||||
function setSelectedAreaId(value: string) {
|
||||
selectedAreaId.value = value;
|
||||
}
|
||||
|
||||
function setDineInEnabled(value: boolean) {
|
||||
basicSettings.enabled = Boolean(value);
|
||||
}
|
||||
|
||||
function setDefaultDiningMinutes(value: number) {
|
||||
basicSettings.defaultDiningMinutes = Math.max(
|
||||
1,
|
||||
Math.floor(Number(value || 1)),
|
||||
);
|
||||
}
|
||||
|
||||
function setOvertimeReminderMinutes(value: number) {
|
||||
basicSettings.overtimeReminderMinutes = Math.max(
|
||||
0,
|
||||
Math.floor(Number(value || 0)),
|
||||
);
|
||||
}
|
||||
|
||||
// 9. 门店切换时自动刷新配置。
|
||||
watch(selectedStoreId, async (storeId) => {
|
||||
if (!storeId) {
|
||||
areas.value = sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS));
|
||||
tables.value = sortTables(cloneTables(DEFAULT_DINE_IN_TABLES));
|
||||
basicSettings.enabled = DEFAULT_DINE_IN_BASIC_SETTINGS.enabled;
|
||||
basicSettings.defaultDiningMinutes =
|
||||
DEFAULT_DINE_IN_BASIC_SETTINGS.defaultDiningMinutes;
|
||||
basicSettings.overtimeReminderMinutes =
|
||||
DEFAULT_DINE_IN_BASIC_SETTINGS.overtimeReminderMinutes;
|
||||
selectedAreaId.value = areas.value[0]?.id ?? '';
|
||||
snapshot.value = null;
|
||||
return;
|
||||
}
|
||||
await loadStoreSettings(storeId);
|
||||
});
|
||||
|
||||
// 10. 页面首屏初始化。
|
||||
onMounted(loadStores);
|
||||
|
||||
return {
|
||||
DINE_IN_SEATS_OPTIONS,
|
||||
DINE_IN_STATUS_MAP,
|
||||
TABLE_TAG_SUGGESTIONS,
|
||||
areaDrawerTitle,
|
||||
areaForm,
|
||||
areaOptions,
|
||||
areaSubmitText,
|
||||
areas,
|
||||
basicSettings,
|
||||
batchForm,
|
||||
batchPreviewCodes,
|
||||
copyCandidates,
|
||||
copyTargetStoreIds,
|
||||
filteredTables,
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
handleDeleteArea,
|
||||
handleDeleteTable,
|
||||
handleSubmitArea,
|
||||
handleSubmitBatch,
|
||||
handleSubmitTable,
|
||||
isAreaDrawerOpen,
|
||||
isBatchModalOpen,
|
||||
isCopyAllChecked,
|
||||
isCopyIndeterminate,
|
||||
isCopyModalOpen,
|
||||
isCopySubmitting,
|
||||
isPageLoading,
|
||||
isSavingArea,
|
||||
isSavingBasic,
|
||||
isSavingBatch,
|
||||
isSavingTable,
|
||||
isStoreLoading,
|
||||
isTableDrawerOpen,
|
||||
openAreaDrawer,
|
||||
openBatchModal,
|
||||
openCopyModal,
|
||||
openTableDrawer,
|
||||
resetBasicSettings,
|
||||
resolveStatusClassName,
|
||||
saveBasicSettings,
|
||||
selectedArea,
|
||||
selectedAreaId,
|
||||
selectedAreaTableCount,
|
||||
selectedStoreId,
|
||||
selectedStoreName,
|
||||
setAreaDescription,
|
||||
setAreaDrawerOpen,
|
||||
setAreaName,
|
||||
setAreaSort,
|
||||
setBatchAreaId,
|
||||
setBatchCodePrefix,
|
||||
setBatchCount,
|
||||
setBatchModalOpen,
|
||||
setBatchSeats,
|
||||
setBatchStartNumber,
|
||||
setDefaultDiningMinutes,
|
||||
setDineInEnabled,
|
||||
setOvertimeReminderMinutes,
|
||||
setSelectedAreaId,
|
||||
setSelectedStoreId,
|
||||
setTableAreaId,
|
||||
setTableCode,
|
||||
setTableDisabled,
|
||||
setTableDrawerOpen,
|
||||
setTableSeats,
|
||||
setTableTags,
|
||||
storeOptions,
|
||||
tableDrawerTitle,
|
||||
tableForm,
|
||||
tableSubmitText,
|
||||
tables,
|
||||
toggleCopyStore,
|
||||
};
|
||||
}
|
||||
246
apps/web-antd/src/views/store/dine-in/index.vue
Normal file
246
apps/web-antd/src/views/store/dine-in/index.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
import type { DineInAreaDto } from '#/api/store-dinein';
|
||||
|
||||
/**
|
||||
* 文件职责:堂食管理页面主视图。
|
||||
* 1. 组合区域、桌位、堂食设置与抽屉/弹窗子组件。
|
||||
* 2. 承接门店维度切换与复制弹窗。
|
||||
*/
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, Empty, message, Spin } from 'ant-design-vue';
|
||||
|
||||
import CopyToStoresModal from '../components/CopyToStoresModal.vue';
|
||||
import StoreScopeToolbar from '../components/StoreScopeToolbar.vue';
|
||||
import DineInAreaDrawer from './components/DineInAreaDrawer.vue';
|
||||
import DineInAreaSection from './components/DineInAreaSection.vue';
|
||||
import DineInBasicSettingsCard from './components/DineInBasicSettingsCard.vue';
|
||||
import DineInBatchModal from './components/DineInBatchModal.vue';
|
||||
import DineInTableDrawer from './components/DineInTableDrawer.vue';
|
||||
import DineInTableGridSection from './components/DineInTableGridSection.vue';
|
||||
import { useStoreDineInPage } from './composables/useStoreDineInPage';
|
||||
|
||||
const {
|
||||
DINE_IN_SEATS_OPTIONS,
|
||||
DINE_IN_STATUS_MAP,
|
||||
TABLE_TAG_SUGGESTIONS,
|
||||
areaDrawerTitle,
|
||||
areaForm,
|
||||
areaOptions,
|
||||
areaSubmitText,
|
||||
areas,
|
||||
basicSettings,
|
||||
batchForm,
|
||||
batchPreviewCodes,
|
||||
copyCandidates,
|
||||
copyTargetStoreIds,
|
||||
filteredTables,
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
handleDeleteArea,
|
||||
handleDeleteTable,
|
||||
handleSubmitArea,
|
||||
handleSubmitBatch,
|
||||
handleSubmitTable,
|
||||
isAreaDrawerOpen,
|
||||
isBatchModalOpen,
|
||||
isCopyAllChecked,
|
||||
isCopyIndeterminate,
|
||||
isCopyModalOpen,
|
||||
isCopySubmitting,
|
||||
isPageLoading,
|
||||
isSavingArea,
|
||||
isSavingBasic,
|
||||
isSavingBatch,
|
||||
isSavingTable,
|
||||
isStoreLoading,
|
||||
isTableDrawerOpen,
|
||||
openAreaDrawer,
|
||||
openBatchModal,
|
||||
openCopyModal,
|
||||
openTableDrawer,
|
||||
resetBasicSettings,
|
||||
resolveStatusClassName,
|
||||
saveBasicSettings,
|
||||
selectedArea,
|
||||
selectedAreaId,
|
||||
selectedStoreId,
|
||||
selectedStoreName,
|
||||
setAreaDescription,
|
||||
setAreaDrawerOpen,
|
||||
setAreaName,
|
||||
setAreaSort,
|
||||
setBatchAreaId,
|
||||
setBatchCodePrefix,
|
||||
setBatchCount,
|
||||
setBatchModalOpen,
|
||||
setBatchSeats,
|
||||
setBatchStartNumber,
|
||||
setDefaultDiningMinutes,
|
||||
setDineInEnabled,
|
||||
setOvertimeReminderMinutes,
|
||||
setSelectedAreaId,
|
||||
setSelectedStoreId,
|
||||
setTableAreaId,
|
||||
setTableCode,
|
||||
setTableDisabled,
|
||||
setTableDrawerOpen,
|
||||
setTableSeats,
|
||||
setTableTags,
|
||||
storeOptions,
|
||||
tableDrawerTitle,
|
||||
tableForm,
|
||||
tableSubmitText,
|
||||
tables,
|
||||
toggleCopyStore,
|
||||
} = useStoreDineInPage();
|
||||
|
||||
/** 统计指定区域桌位数。 */
|
||||
function getAreaTableCount(areaId: string) {
|
||||
return tables.value.filter((item) => item.areaId === areaId).length;
|
||||
}
|
||||
|
||||
/** 桌位状态文案映射。 */
|
||||
const tableStatusLabelMap = {
|
||||
free: DINE_IN_STATUS_MAP.free.label,
|
||||
dining: DINE_IN_STATUS_MAP.dining.label,
|
||||
reserved: DINE_IN_STATUS_MAP.reserved.label,
|
||||
disabled: DINE_IN_STATUS_MAP.disabled.label,
|
||||
};
|
||||
|
||||
/** 当前区域删除处理。 */
|
||||
function onDeleteSelectedArea(area: DineInAreaDto) {
|
||||
handleDeleteArea(area);
|
||||
}
|
||||
|
||||
/** 二维码按钮占位处理。 */
|
||||
function onViewQrCode(tableCode: string) {
|
||||
message.info(`桌位 ${tableCode} 二维码功能待接入`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="堂食管理" content-class="space-y-4 page-store-dinein">
|
||||
<StoreScopeToolbar
|
||||
:selected-store-id="selectedStoreId"
|
||||
:store-options="storeOptions"
|
||||
:is-store-loading="isStoreLoading"
|
||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
||||
@update:selected-store-id="setSelectedStoreId"
|
||||
@copy="openCopyModal"
|
||||
/>
|
||||
|
||||
<template v-if="storeOptions.length === 0">
|
||||
<Card :bordered="false">
|
||||
<Empty description="暂无门店,请先创建门店" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Spin :spinning="isPageLoading">
|
||||
<DineInAreaSection
|
||||
:areas="areas"
|
||||
:selected-area-id="selectedAreaId"
|
||||
:selected-area="selectedArea"
|
||||
:is-saving="isSavingArea"
|
||||
:get-area-table-count="getAreaTableCount"
|
||||
@add="openAreaDrawer('create')"
|
||||
@edit="(area) => openAreaDrawer('edit', area)"
|
||||
@delete="onDeleteSelectedArea"
|
||||
@select-area="setSelectedAreaId"
|
||||
/>
|
||||
|
||||
<DineInTableGridSection
|
||||
:tables="filteredTables"
|
||||
:is-saving="isSavingTable"
|
||||
:status-map="DINE_IN_STATUS_MAP"
|
||||
:resolve-status-class-name="resolveStatusClassName"
|
||||
@add="openTableDrawer('create')"
|
||||
@batch="openBatchModal"
|
||||
@edit="(table) => openTableDrawer('edit', table)"
|
||||
@delete="handleDeleteTable"
|
||||
@qrcode="(table) => onViewQrCode(table.code)"
|
||||
/>
|
||||
|
||||
<DineInBasicSettingsCard
|
||||
:settings="basicSettings"
|
||||
:is-saving="isSavingBasic"
|
||||
:on-set-enabled="setDineInEnabled"
|
||||
:on-set-default-dining-minutes="setDefaultDiningMinutes"
|
||||
:on-set-overtime-reminder-minutes="setOvertimeReminderMinutes"
|
||||
@save="saveBasicSettings"
|
||||
@reset="resetBasicSettings"
|
||||
/>
|
||||
</Spin>
|
||||
</template>
|
||||
|
||||
<DineInAreaDrawer
|
||||
:open="isAreaDrawerOpen"
|
||||
:title="areaDrawerTitle"
|
||||
:submit-text="areaSubmitText"
|
||||
:form="areaForm"
|
||||
:is-saving="isSavingArea"
|
||||
:on-set-name="setAreaName"
|
||||
:on-set-description="setAreaDescription"
|
||||
:on-set-sort="setAreaSort"
|
||||
@update:open="setAreaDrawerOpen"
|
||||
@submit="handleSubmitArea"
|
||||
/>
|
||||
|
||||
<DineInTableDrawer
|
||||
:open="isTableDrawerOpen"
|
||||
:title="tableDrawerTitle"
|
||||
:submit-text="tableSubmitText"
|
||||
:form="tableForm"
|
||||
:is-saving="isSavingTable"
|
||||
:area-options="areaOptions"
|
||||
:seats-options="DINE_IN_SEATS_OPTIONS"
|
||||
:status-label-map="tableStatusLabelMap"
|
||||
:tag-suggestions="TABLE_TAG_SUGGESTIONS"
|
||||
:on-set-code="setTableCode"
|
||||
:on-set-area-id="setTableAreaId"
|
||||
:on-set-seats="setTableSeats"
|
||||
:on-set-disabled="setTableDisabled"
|
||||
:on-set-tags="setTableTags"
|
||||
@update:open="setTableDrawerOpen"
|
||||
@submit="handleSubmitTable"
|
||||
/>
|
||||
|
||||
<DineInBatchModal
|
||||
:open="isBatchModalOpen"
|
||||
:form="batchForm"
|
||||
:is-saving="isSavingBatch"
|
||||
:area-options="areaOptions"
|
||||
:seats-options="DINE_IN_SEATS_OPTIONS"
|
||||
:preview-codes="batchPreviewCodes"
|
||||
:on-set-area-id="setBatchAreaId"
|
||||
:on-set-code-prefix="setBatchCodePrefix"
|
||||
:on-set-start-number="setBatchStartNumber"
|
||||
:on-set-count="setBatchCount"
|
||||
:on-set-seats="setBatchSeats"
|
||||
@update:open="setBatchModalOpen"
|
||||
@submit="handleSubmitBatch"
|
||||
/>
|
||||
|
||||
<CopyToStoresModal
|
||||
v-model:open="isCopyModalOpen"
|
||||
title="复制堂食设置到其他门店"
|
||||
confirm-text="确认复制"
|
||||
:copy-candidates="copyCandidates"
|
||||
:target-store-ids="copyTargetStoreIds"
|
||||
:is-all-checked="isCopyAllChecked"
|
||||
:is-indeterminate="isCopyIndeterminate"
|
||||
:is-submitting="isCopySubmitting"
|
||||
:selected-store-name="selectedStoreName"
|
||||
@check-all="handleCopyCheckAll"
|
||||
@submit="handleCopySubmit"
|
||||
@toggle-store="
|
||||
({ storeId, checked }) => toggleCopyStore(storeId, checked)
|
||||
"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@import './styles/index.less';
|
||||
</style>
|
||||
54
apps/web-antd/src/views/store/dine-in/styles/area.less
Normal file
54
apps/web-antd/src/views/store/dine-in/styles/area.less
Normal file
@@ -0,0 +1,54 @@
|
||||
/* 文件职责:堂食管理区域区块样式。 */
|
||||
.page-store-dinein {
|
||||
.dinein-area-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dinein-area-pill {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dinein-area-pill:hover {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.dinein-area-pill.active {
|
||||
color: #fff;
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.dinein-area-info {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #f8f9fb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dinein-area-description {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dinein-area-actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
40
apps/web-antd/src/views/store/dine-in/styles/base.less
Normal file
40
apps/web-antd/src/views/store/dine-in/styles/base.less
Normal file
@@ -0,0 +1,40 @@
|
||||
/* 文件职责:堂食管理页面基础骨架与通用样式。 */
|
||||
.page-store-dinein {
|
||||
max-width: 980px;
|
||||
|
||||
.dinein-card {
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 8%);
|
||||
|
||||
.ant-card-head {
|
||||
min-height: 52px;
|
||||
padding: 0 18px;
|
||||
background: #f8f9fb;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.ant-card-extra {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.dinein-number-input {
|
||||
width: 92px;
|
||||
}
|
||||
}
|
||||
123
apps/web-antd/src/views/store/dine-in/styles/drawer.less
Normal file
123
apps/web-antd/src/views/store/dine-in/styles/drawer.less
Normal file
@@ -0,0 +1,123 @@
|
||||
/* 文件职责:堂食管理抽屉与批量弹窗样式。 */
|
||||
.dinein-area-drawer-wrap,
|
||||
.dinein-table-drawer-wrap {
|
||||
.ant-drawer-body {
|
||||
padding: 16px 20px 90px;
|
||||
}
|
||||
|
||||
.ant-drawer-footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 12px;
|
||||
}
|
||||
|
||||
.drawer-form-block {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.drawer-form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.drawer-form-label.required::before {
|
||||
margin-right: 4px;
|
||||
color: #ef4444;
|
||||
content: '*';
|
||||
}
|
||||
|
||||
.drawer-input-with-unit {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer-input {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
.drawer-form-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.drawer-switch-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer-status-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dinein-batch-modal-wrap {
|
||||
.ant-modal-content {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding-top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.batch-form-item.full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.batch-form-item label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.batch-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.batch-preview-wrap {
|
||||
padding: 12px;
|
||||
margin-top: 16px;
|
||||
background: #f8f9fb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.batch-preview-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.batch-preview-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
7
apps/web-antd/src/views/store/dine-in/styles/index.less
Normal file
7
apps/web-antd/src/views/store/dine-in/styles/index.less
Normal file
@@ -0,0 +1,7 @@
|
||||
/* 文件职责:堂食管理页面样式聚合入口(仅负责分片导入)。 */
|
||||
@import './base.less';
|
||||
@import './area.less';
|
||||
@import './table.less';
|
||||
@import './settings.less';
|
||||
@import './drawer.less';
|
||||
@import './responsive.less';
|
||||
44
apps/web-antd/src/views/store/dine-in/styles/responsive.less
Normal file
44
apps/web-antd/src/views/store/dine-in/styles/responsive.less
Normal file
@@ -0,0 +1,44 @@
|
||||
/* 文件职责:堂食管理页面响应式规则。 */
|
||||
.page-store-dinein {
|
||||
@media (max-width: 992px) {
|
||||
.dinein-table-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dinein-area-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dinein-table-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dinein-form-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dinein-form-label {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.drawer-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.batch-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.batch-form-item.full {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
50
apps/web-antd/src/views/store/dine-in/styles/settings.less
Normal file
50
apps/web-antd/src/views/store/dine-in/styles/settings.less
Normal file
@@ -0,0 +1,50 @@
|
||||
/* 文件职责:堂食管理基础设置区块样式。 */
|
||||
.page-store-dinein {
|
||||
.dinein-form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.dinein-form-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dinein-form-label {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dinein-form-control {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dinein-form-unit {
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dinein-form-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dinein-form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
}
|
||||
109
apps/web-antd/src/views/store/dine-in/styles/table.less
Normal file
109
apps/web-antd/src/views/store/dine-in/styles/table.less
Normal file
@@ -0,0 +1,109 @@
|
||||
/* 文件职责:堂食管理桌位区块样式。 */
|
||||
.page-store-dinein {
|
||||
.dinein-table-header-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dinein-table-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dinein-table-card {
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 8%);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dinein-table-card:hover {
|
||||
box-shadow: 0 6px 20px rgb(15 23 42 / 10%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dinein-table-card.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dinein-table-code {
|
||||
margin-bottom: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.dinein-table-seat {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dinein-table-status {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dinein-table-status .status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-free {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-free .status-dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-dining {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-dining .status-dot {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-reserved {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-reserved .status-dot {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-disabled {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dinein-table-status.status-disabled .status-dot {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.dinein-table-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
min-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dinein-table-footer {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
}
|
||||
57
apps/web-antd/src/views/store/dine-in/types.ts
Normal file
57
apps/web-antd/src/views/store/dine-in/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 文件职责:堂食管理页面类型定义。
|
||||
* 1. 声明区域、桌位、批量生成表单态。
|
||||
* 2. 声明页面快照与选择项类型。
|
||||
*/
|
||||
import type {
|
||||
DineInAreaDto,
|
||||
DineInBasicSettingsDto,
|
||||
DineInTableDto,
|
||||
DineInTableStatus,
|
||||
} from '#/api/store-dinein';
|
||||
|
||||
export type DineInAreaDrawerMode = 'create' | 'edit';
|
||||
export type DineInTableDrawerMode = 'create' | 'edit';
|
||||
|
||||
export interface DineInAreaFormState {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export interface DineInTableFormState {
|
||||
areaId: string;
|
||||
code: string;
|
||||
id: string;
|
||||
isDisabled: boolean;
|
||||
seats: number;
|
||||
sourceStatus: DineInTableStatus;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface DineInBatchFormState {
|
||||
areaId: string;
|
||||
codePrefix: string;
|
||||
count: number;
|
||||
seats: number;
|
||||
startNumber: number;
|
||||
}
|
||||
|
||||
export interface DineInSettingsSnapshot {
|
||||
areas: DineInAreaDto[];
|
||||
basicSettings: DineInBasicSettingsDto;
|
||||
tables: DineInTableDto[];
|
||||
}
|
||||
|
||||
export interface DineInSeatsOption {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DineInStatusOption {
|
||||
className: string;
|
||||
color: string;
|
||||
label: string;
|
||||
value: DineInTableStatus;
|
||||
}
|
||||
Reference in New Issue
Block a user