feat: 重构订单大厅看板UI对齐设计稿
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 51s

This commit is contained in:
2026-02-27 17:15:24 +08:00
parent e1512bcfc1
commit 01ac80d67d
14 changed files with 1159 additions and 415 deletions

View File

@@ -4,6 +4,8 @@ import type { BoardColumnKey } from '../types';
import type { OrderBoardCard } from '#/api/order-hall'; import type { OrderBoardCard } from '#/api/order-hall';
import { computed } from 'vue';
import BoardOrderCard from './BoardOrderCard.vue'; import BoardOrderCard from './BoardOrderCard.vue';
const { const {
@@ -13,11 +15,11 @@ const {
columnKey, columnKey,
title = '', title = '',
} = defineProps<{ } = defineProps<{
cards: OrderBoardCard[]; cards?: OrderBoardCard[];
colorClass: string; colorClass?: string;
columnKey: BoardColumnKey; columnKey: BoardColumnKey;
isLoading: boolean; isLoading?: boolean;
title: string; title?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -26,20 +28,49 @@ const emit = defineEmits<{
confirmDelivery: [orderId: string]; confirmDelivery: [orderId: string];
reject: [orderId: string]; reject: [orderId: string];
}>(); }>();
const deliveryChannelCount = computed(
() => cards.filter((item) => item.deliveryType === 2).length,
);
const shouldShowCount = computed(() => columnKey !== 'completed');
const countText = computed(() => {
if (columnKey === 'delivering') {
return `(${deliveryChannelCount.value}/${cards.length})`;
}
return String(cards.length);
});
</script> </script>
<template> <template>
<div class="ob-column" :class="[colorClass]"> <div class="ob-column" :class="[colorClass]">
<!-- 1. 列标题 --> <!-- 1. 列标题 -->
<div class="ob-column__header"> <div class="ob-col-hd" :class="[colorClass]">
<span class="ob-column__title">{{ title }}</span> <span>{{ title }}</span>
<a-badge :count="cards.length" :overflow-count="999" /> <span
v-if="shouldShowCount"
class="ob-col-count"
:class="`is-${columnKey}`"
>
{{ countText }}
</span>
</div> </div>
<!-- 2. 滚动区 --> <!-- 2. 滚动区 -->
<div class="ob-column__body"> <div class="ob-col-body">
<a-spin :spinning="isLoading"> <a-spin :spinning="isLoading">
<div v-if="cards.length === 0" class="ob-column__empty">暂无订单</div> <div
v-if="cards.length === 0 && columnKey !== 'completed'"
class="ob-column__empty"
>
暂无订单
</div>
<div
v-if="cards.length === 0 && columnKey === 'completed'"
class="ob-column__placeholder"
></div>
<BoardOrderCard <BoardOrderCard
v-for="card in cards" v-for="card in cards"
:key="card.id" :key="card.id"

View File

@@ -1,17 +1,14 @@
<!-- 文件职责订单看板卡片信息 + 操作按钮 --> <!-- 文件职责订单看板卡片视觉对齐新版原型 -->
<script setup lang="ts"> <script setup lang="ts">
import type { BoardColumnKey } from '../types'; import type { BoardColumnKey } from '../types';
import type { OrderBoardCard } from '#/api/order-hall'; import type { OrderBoardCard } from '#/api/order-hall';
import { CHANNEL_TAG_COLORS } from '../composables/order-hall-page/constants'; import { IconifyIcon } from '@vben/icons';
import {
channelText, import dayjs from 'dayjs';
deliveryTypeText,
elapsedMinutes, import { formatAmount } from '../composables/order-hall-page/helpers';
formatAmount,
formatTime,
} from '../composables/order-hall-page/helpers';
const { card, columnKey } = defineProps<{ const { card, columnKey } = defineProps<{
card: OrderBoardCard; card: OrderBoardCard;
@@ -24,81 +21,175 @@ const emit = defineEmits<{
confirmDelivery: [orderId: string]; confirmDelivery: [orderId: string];
reject: [orderId: string]; reject: [orderId: string];
}>(); }>();
function resolveDeliveryLabel(deliveryType: number): string {
switch (deliveryType) {
case 0: {
return '堂食';
}
case 1: {
return '自提';
}
case 2: {
return '外卖';
}
default: {
return '未知';
}
}
}
function resolveDeliveryClass(deliveryType: number): string {
switch (deliveryType) {
case 0: {
return 'is-dine-in';
}
case 1: {
return 'is-pickup';
}
case 2: {
return 'is-delivery';
}
default: {
return 'is-default';
}
}
}
function resolveShortOrderNo(orderNo: string): string {
const digits = orderNo.match(/\d+/g)?.join('') ?? '';
if (!digits) {
return orderNo;
}
const value = Number.parseInt(digits, 10);
if (Number.isNaN(value)) {
return orderNo;
}
return `#${value}`;
}
function resolveElapsedClock(value?: null | string): string {
if (!value) return '--:--';
const minutes = Math.max(dayjs().diff(dayjs(value), 'minute'), 0);
if (minutes > 99) {
return dayjs(value).format('HH:mm');
}
const seconds = Math.max(dayjs().diff(dayjs(value), 'second'), 0);
const minutesPart = Math.floor(seconds / 60);
const secondPart = String(seconds % 60).padStart(2, '0');
return `${minutesPart}:${secondPart}`;
}
function resolveMakingProgress(order: OrderBoardCard): number {
const since = order.acceptedAt ?? order.createdAt;
if (!since) return 0;
const minutes = Math.max(dayjs().diff(dayjs(since), 'minute'), 0);
if (minutes > 120) {
const fallbackSource = Number.parseInt(
(order.orderNo.match(/\d+/g)?.join('') ?? '').slice(-2),
10,
);
const fallback = Number.isNaN(fallbackSource) ? 35 : fallbackSource;
return 25 + (fallback % 55);
}
return Math.min(95, Math.max(8, minutes * 6));
}
function resolveDeliverInfoText(order: OrderBoardCard): string {
if (order.deliveryType === 1) {
return '等待取餐';
}
return order.customerName || '配送中';
}
function resolveDeliverInfoIcon(order: OrderBoardCard): string {
if (order.deliveryType === 1) {
return 'lucide:user-round';
}
return 'lucide:bike';
}
</script> </script>
<template> <template>
<div <div class="ob-order" :class="[columnKey, { 'is-urged': card.isUrged }]">
class="ob-card" <div class="ob-order-top">
:class="[ <div class="ob-order-top-left">
columnKey === 'pending' && 'ob-card--pulse', <span
card.isUrged && 'ob-card--urged', class="ob-type-tag"
columnKey === 'completed' && 'ob-card--dimmed', :class="[resolveDeliveryClass(card.deliveryType)]"
]" >
> {{ resolveDeliveryLabel(card.deliveryType) }}
<!-- 1. 卡片头部 --> </span>
<div class="ob-card__header"> <span class="ob-short-no">{{ resolveShortOrderNo(card.orderNo) }}</span>
<span class="ob-card__order-no">{{ card.orderNo }}</span> <span v-if="card.tableNo" class="ob-table-chip">
<a-tag 桌台: {{ card.tableNo }}
v-if="card.channel" </span>
:color="CHANNEL_TAG_COLORS[card.channel] ?? 'default'" </div>
<div class="ob-time-chip">
<IconifyIcon icon="lucide:clock-3" />
<span>{{ resolveElapsedClock(card.createdAt) }}</span>
</div>
</div>
<div class="ob-main-line">
<IconifyIcon icon="lucide:utensils-crossed" />
<span>{{ card.itemsSummary || '--' }}</span>
</div>
<div class="ob-order-mid">
<span class="ob-full-no">{{ card.orderNo }}</span>
<span class="ob-amount">{{ formatAmount(card.paidAmount) }}</span>
</div>
<div v-if="columnKey === 'pending'" class="ob-actions ob-actions-pending">
<a-button class="ob-btn ob-btn-ghost" @click="emit('reject', card.id)">
拒单
</a-button>
<a-button class="ob-btn ob-btn-orange" @click="emit('accept', card.id)">
接单
</a-button>
</div>
<div
v-else-if="columnKey === 'making'"
class="ob-actions ob-actions-making"
>
<a-progress
:percent="resolveMakingProgress(card)"
:show-info="false"
size="small" size="small"
stroke-color="#3f7fe8"
trail-color="#eceff3"
/>
<a-button
class="ob-btn ob-btn-blue"
@click="emit('completePreparation', card.id)"
> >
{{ channelText(card.channel) }} 出餐完成
</a-tag> </a-button>
<a-tag v-if="card.isUrged" color="red" size="small">
{{ card.urgeCount }}
</a-tag>
</div> </div>
<!-- 2. 卡片内容 --> <div
<div class="ob-card__body"> v-else-if="columnKey === 'delivering'"
<div class="ob-card__row"> class="ob-actions ob-actions-delivering"
<span>{{ deliveryTypeText(card.deliveryType) }}</span> >
<span v-if="card.tableNo">桌号: {{ card.tableNo }}</span> <div class="ob-deliver-info">
<IconifyIcon :icon="resolveDeliverInfoIcon(card)" />
<span>{{ resolveDeliverInfoText(card) }}</span>
</div> </div>
<div v-if="card.customerName" class="ob-card__row"> <a-button
{{ card.customerName }} class="ob-btn ob-btn-green"
</div> @click="emit('confirmDelivery', card.id)"
<div v-if="card.itemsSummary" class="ob-card__row ob-card__items"> >
{{ card.itemsSummary }} 确认
</div> </a-button>
<div class="ob-card__row">
<span class="ob-card__amount">{{ formatAmount(card.paidAmount) }}</span>
<span class="ob-card__time">{{ formatTime(card.createdAt) }}</span>
</div>
<div v-if="columnKey === 'making'" class="ob-card__elapsed">
已用 {{ elapsedMinutes(card.acceptedAt) }} 分钟
</div>
</div>
<!-- 3. 操作按钮 -->
<div class="ob-card__actions">
<template v-if="columnKey === 'pending'">
<a-button type="primary" size="small" @click="emit('accept', card.id)">
接单
</a-button>
<a-button size="small" danger @click="emit('reject', card.id)">
拒单
</a-button>
</template>
<template v-else-if="columnKey === 'making'">
<a-button
type="primary"
size="small"
@click="emit('completePreparation', card.id)"
>
出餐完成
</a-button>
</template>
<template v-else-if="columnKey === 'delivering'">
<a-button
type="primary"
size="small"
@click="emit('confirmDelivery', card.id)"
>
{{ card.deliveryType === 2 ? '确认送达' : '确认取餐' }}
</a-button>
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -7,35 +7,42 @@ const {
pendingCount = 0, pendingCount = 0,
todayTotal = 0, todayTotal = 0,
} = defineProps<{ } = defineProps<{
completedCount: number; completedCount?: number;
deliveringCount: number; deliveringCount?: number;
makingCount: number; makingCount?: number;
pendingCount: number; pendingCount?: number;
todayTotal: number; todayTotal?: number;
}>(); }>();
</script> </script>
<template> <template>
<div class="ob-stats"> <div class="ob-stats">
<div class="ob-stats__item"> <div class="ob-stats-main">
<span class="ob-stats__value">{{ todayTotal }}</span> <div class="ob-summary-item">
<span class="ob-stats__label">今日订单</span> <span class="ob-summary-label">今日订单</span>
<span class="ob-summary-value is-default">{{ todayTotal }}</span>
</div>
<div class="ob-summary-item is-active">
<span class="ob-summary-label">待接单</span>
<span class="ob-summary-value is-pending">{{ pendingCount }}</span>
</div>
<div class="ob-summary-item">
<span class="ob-summary-label">制作中</span>
<span class="ob-summary-value is-making">{{ makingCount }}</span>
</div>
<div class="ob-summary-item">
<span class="ob-summary-label">配送/待取</span>
<span class="ob-summary-value is-delivering">{{
deliveringCount
}}</span>
</div>
<div class="ob-summary-item">
<span class="ob-summary-label">已完成</span>
<span class="ob-summary-value is-muted">{{ completedCount }}</span>
</div>
</div> </div>
<div class="ob-stats__item ob-stats__item--pending"> <div class="ob-stats-actions">
<span class="ob-stats__value">{{ pendingCount }}</span> <slot name="actions"></slot>
<span class="ob-stats__label">待接单</span>
</div>
<div class="ob-stats__item ob-stats__item--making">
<span class="ob-stats__value">{{ makingCount }}</span>
<span class="ob-stats__label">制作中</span>
</div>
<div class="ob-stats__item ob-stats__item--delivering">
<span class="ob-stats__value">{{ deliveringCount }}</span>
<span class="ob-stats__label">配送/待取</span>
</div>
<div class="ob-stats__item ob-stats__item--completed">
<span class="ob-stats__value">{{ completedCount }}</span>
<span class="ob-stats__label">已完成</span>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,75 +1,98 @@
<!-- 文件职责订单大厅工具栏门店选择 + 渠道筛选 + 声音开关 + 连接状态 --> <!-- 文件职责订单大厅顶部控制区门店 + 履约筛选 + 声音开关 + 刷新 -->
<script setup lang="ts"> <script setup lang="ts">
import type { BoardChannelFilter } from '../types'; import type { BoardDeliveryFilter, StoreOptionItem } from '../types';
import { CHANNEL_OPTIONS } from '../composables/order-hall-page/constants'; import { IconifyIcon } from '@vben/icons';
import { DELIVERY_FILTER_OPTIONS } from '../composables/order-hall-page/constants';
const { const {
channel = 'all', deliveryFilter = 'all',
isConnected = false, isConnected = false,
isSoundEnabled = true, isSoundEnabled = true,
isStoreLoading = false,
storeId = '', storeId = '',
storeOptions = [],
} = defineProps<{ } = defineProps<{
channel: BoardChannelFilter; deliveryFilter?: BoardDeliveryFilter;
isConnected: boolean; isConnected?: boolean;
isSoundEnabled: boolean; isSoundEnabled?: boolean;
storeId: string; isStoreLoading?: boolean;
storeId?: string;
storeOptions?: StoreOptionItem[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'update:channel': [value: BoardChannelFilter]; refresh: [];
'update:deliveryFilter': [value: BoardDeliveryFilter];
'update:isSoundEnabled': [value: boolean]; 'update:isSoundEnabled': [value: boolean];
'update:storeId': [value: string]; 'update:storeId': [value: string];
}>(); }>();
function handleStoreChange(value: unknown) {
if (typeof value === 'number' || typeof value === 'string') {
emit('update:storeId', String(value));
return;
}
emit('update:storeId', '');
}
</script> </script>
<template> <template>
<div class="ob-toolbar"> <div class="ob-toolbar-shell">
<!-- 1. 门店 ID 输入 --> <div class="ob-toolbar-actions">
<div class="ob-toolbar__store"> <a-select
<span class="ob-toolbar__label">门店ID:</span>
<a-input
:value="storeId" :value="storeId"
placeholder="输入门店 ID" class="ob-store-select"
style="width: 200px" placeholder="请选择门店"
@change="(e: any) => emit('update:storeId', e.target.value)" :loading="isStoreLoading"
:options="storeOptions"
:disabled="isStoreLoading || storeOptions.length === 0"
@update:value="handleStoreChange"
/> />
</div>
<!-- 2. 渠道筛选 --> <div class="ob-channels">
<div class="ob-toolbar__channel"> <button
<a-radio-group v-for="option in DELIVERY_FILTER_OPTIONS"
:value="channel" :key="option.value"
button-style="solid" type="button"
size="small" class="ob-channel"
@change="(e: any) => emit('update:channel', e.target.value)" :class="{ active: deliveryFilter === option.value }"
> @click="emit('update:deliveryFilter', option.value)"
<a-radio-button
v-for="opt in CHANNEL_OPTIONS"
:key="opt.value"
:value="opt.value"
> >
{{ opt.label }} {{ option.label }}
</a-radio-button> </button>
</a-radio-group> </div>
</div>
<!-- 3. 声音开关 + 连接状态 --> <div class="ob-toolbar-right">
<div class="ob-toolbar__right"> <span class="ob-connection" :class="{ offline: !isConnected }">
<a-switch <span class="ob-connection-dot"></span>
:checked="isSoundEnabled" {{ isConnected ? '已连接' : '连接中' }}
checked-children="声音开" </span>
un-checked-children="声音关"
@change="(val: boolean) => emit('update:isSoundEnabled', val)" <button
/> type="button"
<span class="ob-icon-btn"
class="ob-toolbar__status" :class="{ active: isSoundEnabled }"
:class="[ :title="isSoundEnabled ? '提示音开启' : '提示音关闭'"
isConnected ? 'ob-toolbar__status--on' : 'ob-toolbar__status--off', @click="emit('update:isSoundEnabled', !isSoundEnabled)"
]" >
> <IconifyIcon
{{ isConnected ? '已连接' : '未连接' }} :icon="isSoundEnabled ? 'lucide:volume-2' : 'lucide:volume-x'"
</span> />
</button>
<button
type="button"
class="ob-icon-btn"
:class="{ 'ob-icon-btn--offline': !isConnected }"
:title="isConnected ? '刷新' : '连接中,点击重试刷新'"
@click="emit('refresh')"
>
<IconifyIcon icon="lucide:refresh-cw" />
</button>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -3,7 +3,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
const { open = false } = defineProps<{ const { open = false } = defineProps<{
open: boolean; open?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -1,7 +1,7 @@
/** /**
* 文件职责:订单大厅常量与配置。 * 文件职责:订单大厅常量与配置。
*/ */
import type { BoardChannelFilter, BoardColumnKey } from '../../types'; import type { BoardColumnKey, BoardDeliveryFilter } from '../../types';
/** 看板列定义。 */ /** 看板列定义。 */
export const HALL_COLUMNS: { export const HALL_COLUMNS: {
@@ -9,20 +9,21 @@ export const HALL_COLUMNS: {
key: BoardColumnKey; key: BoardColumnKey;
title: string; title: string;
}[] = [ }[] = [
{ key: 'pending', title: '待接单', colorClass: 'ob-col-pending' }, { key: 'pending', title: '待接单', colorClass: 'pending' },
{ key: 'making', title: '制作中', colorClass: 'ob-col-making' }, { key: 'making', title: '制作中', colorClass: 'making' },
{ key: 'delivering', title: '配送/待取', colorClass: 'ob-col-delivering' }, { key: 'delivering', title: '配送/待取', colorClass: 'delivering' },
{ key: 'completed', title: '已完成', colorClass: 'ob-col-completed' }, { key: 'completed', title: '已完成', colorClass: 'done' },
]; ];
/** 渠道筛选选项。 */ /** 履约类型筛选选项。 */
export const CHANNEL_OPTIONS: { label: string; value: BoardChannelFilter }[] = [ export const DELIVERY_FILTER_OPTIONS: {
{ value: 'all', label: '全部渠道' }, label: string;
{ value: 'miniprogram', label: '小程序' }, value: BoardDeliveryFilter;
{ value: 'scan', label: '扫码点餐' }, }[] = [
{ value: 'staff', label: '收银台' }, { value: 'all', label: '全部' },
{ value: 'phone', label: '电话预约' }, { value: 'delivery', label: '外卖' },
{ value: 'thirdparty', label: '第三方' }, { value: 'pickup', label: '自提' },
{ value: 'dineIn', label: '堂食' },
]; ];
/** 订单状态映射。 */ /** 订单状态映射。 */
@@ -60,3 +61,18 @@ export const CHANNEL_TAG_COLORS: Record<number, string> = {
4: 'purple', 4: 'purple',
5: 'red', 5: 'red',
}; };
/** 履约类型标签颜色(原型:外卖=blue, 自提=green, 堂食=orange。 */
export const DELIVERY_TYPE_TAG_COLORS: Record<number, string> = {
0: 'orange',
1: 'green',
2: 'blue',
};
/** 履约类型筛选值到 API 参数的映射。 */
export const DELIVERY_FILTER_VALUE_MAP: Record<string, number | undefined> = {
all: undefined,
delivery: 2,
dineIn: 0,
pickup: 1,
};

View File

@@ -3,41 +3,70 @@
*/ */
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { OrderHallPageState } from '../../types'; import type { OrderHallPageState, OrderHallStatsState } from '../../types';
import type { OrderBoardCard } from '#/api/order-hall'; import type { OrderBoardCard } from '#/api/order-hall';
import type { StoreListItemDto } from '#/api/store';
import { import {
getOrderBoardApi, getOrderBoardApi,
getOrderBoardStatsApi, getOrderBoardStatsApi,
getPendingSinceApi, getPendingSinceApi,
} from '#/api/order-hall'; } from '#/api/order-hall';
import { getStoreListApi } from '#/api/store';
import { mapStoreOptions } from '../../types';
/** 数据加载动作选项。 */ /** 数据加载动作选项。 */
interface DataActionsOptions { interface DataActionsOptions {
state: OrderHallPageState; state: OrderHallPageState;
statsRef: Ref<{ statsRef: Ref<OrderHallStatsState>;
completedCount: number;
deliveringCount: number;
makingCount: number;
pendingCount: number;
todayTotal: number;
}>;
} }
/** 创建数据加载动作。 */ /** 创建数据加载动作。 */
export function createDataActions(options: DataActionsOptions) { export function createDataActions(options: DataActionsOptions) {
const { state, statsRef } = options; const { state, statsRef } = options;
// 1. 加载完整看板数据 // 1. 加载门店并初始化默认门店
async function loadStores(): Promise<void> {
state.isStoreLoading = true;
try {
const result = await getStoreListApi({ page: 1, pageSize: 200 });
const stores = result.items ?? ([] as StoreListItemDto[]);
state.storeOptions = mapStoreOptions(stores);
if (stores.length === 0) {
state.selectedStoreId = '';
state.pendingCards = [];
state.makingCards = [];
state.deliveringCards = [];
state.completedCards = [];
statsRef.value = {
todayTotal: 0,
pendingCount: 0,
makingCount: 0,
deliveringCount: 0,
completedCount: 0,
};
return;
}
const matched = stores.some((item) => item.id === state.selectedStoreId);
if (!matched) {
state.selectedStoreId = stores[0]?.id ?? '';
}
} finally {
state.isStoreLoading = false;
}
}
// 2. 加载完整看板数据
async function loadFullBoard(): Promise<void> { async function loadFullBoard(): Promise<void> {
if (!state.selectedStoreId) return; if (!state.selectedStoreId) return;
state.isLoading = true; state.isLoading = true;
try { try {
const channelParam = state.channel === 'all' ? undefined : state.channel;
const result = await getOrderBoardApi({ const result = await getOrderBoardApi({
storeId: state.selectedStoreId, storeId: state.selectedStoreId,
channel: channelParam,
}); });
state.pendingCards = result.pending; state.pendingCards = result.pending;
state.makingCards = result.making; state.makingCards = result.making;
@@ -48,7 +77,7 @@ export function createDataActions(options: DataActionsOptions) {
} }
} }
// 2. 加载统计数据 // 3. 加载统计数据
async function loadStats(): Promise<void> { async function loadStats(): Promise<void> {
if (!state.selectedStoreId) return; if (!state.selectedStoreId) return;
const result = await getOrderBoardStatsApi({ const result = await getOrderBoardStatsApi({
@@ -57,7 +86,7 @@ export function createDataActions(options: DataActionsOptions) {
statsRef.value = result; statsRef.value = result;
} }
// 3. 重连补偿拉取 // 4. 重连补偿拉取
async function catchUpSince(since: Date): Promise<OrderBoardCard[]> { async function catchUpSince(since: Date): Promise<OrderBoardCard[]> {
if (!state.selectedStoreId) return []; if (!state.selectedStoreId) return [];
return getPendingSinceApi({ return getPendingSinceApi({
@@ -66,5 +95,5 @@ export function createDataActions(options: DataActionsOptions) {
}); });
} }
return { loadFullBoard, loadStats, catchUpSince }; return { loadStores, loadFullBoard, loadStats, catchUpSince };
} }

View File

@@ -1,4 +1,4 @@
import type { OrderHallPageState } from '../types'; import type { OrderHallPageState, OrderHallStatsState } from '../types';
/** /**
* 文件职责:订单大厅页面编排器,组合所有子动作模块。 * 文件职责:订单大厅页面编排器,组合所有子动作模块。
@@ -15,16 +15,18 @@ export function useOrderHallPage() {
// 1. 页面状态 // 1. 页面状态
const state = reactive<OrderHallPageState>({ const state = reactive<OrderHallPageState>({
selectedStoreId: '', selectedStoreId: '',
channel: 'all', deliveryFilter: 'all',
isStoreLoading: false,
isLoading: false, isLoading: false,
isSoundEnabled: true, isSoundEnabled: true,
storeOptions: [],
pendingCards: [], pendingCards: [],
makingCards: [], makingCards: [],
deliveringCards: [], deliveringCards: [],
completedCards: [], completedCards: [],
}); });
const stats = ref({ const stats = ref<OrderHallStatsState>({
todayTotal: 0, todayTotal: 0,
pendingCount: 0, pendingCount: 0,
makingCount: 0, makingCount: 0,
@@ -35,6 +37,7 @@ export function useOrderHallPage() {
// 2. 拒单弹窗状态 // 2. 拒单弹窗状态
const isRejectModalOpen = ref(false); const isRejectModalOpen = ref(false);
const rejectingOrderId = ref(''); const rejectingOrderId = ref('');
const hasSubscribed = ref(false);
// 3. 创建通知动作 // 3. 创建通知动作
const notificationActions = createNotificationActions({ const notificationActions = createNotificationActions({
@@ -73,7 +76,6 @@ export function useOrderHallPage() {
// 7. 创建卡片动作 // 7. 创建卡片动作
const cardActions = createCardActions({ const cardActions = createCardActions({
state,
onRefresh: refresh, onRefresh: refresh,
onAccepted: () => { onAccepted: () => {
// 接单后如果待接单列为空则停止声音 // 接单后如果待接单列为空则停止声音
@@ -104,21 +106,35 @@ export function useOrderHallPage() {
watch( watch(
() => state.selectedStoreId, () => state.selectedStoreId,
async (newStoreId) => { async (newStoreId) => {
if (!newStoreId) return; if (!newStoreId) {
return;
}
await refresh(); await refresh();
if (!hasSubscribed.value) {
await signalRActions.connectAndSubscribe();
hasSubscribed.value = true;
return;
}
await signalRActions.switchStore(newStoreId); await signalRActions.switchStore(newStoreId);
}, },
); );
// 10. 监听渠道切换 // 10. 监听提示音开关(关闭时立即停掉循环音)
watch( watch(
() => state.channel, () => state.isSoundEnabled,
() => dataActions.loadFullBoard(), (enabled) => {
if (!enabled) {
notificationActions.stopNewOrderSound();
}
},
); );
// 11. 生命周期 // 11. 生命周期
onMounted(() => { onMounted(async () => {
notificationActions.requestNotificationPermission(); notificationActions.requestNotificationPermission();
await dataActions.loadStores();
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -133,6 +149,7 @@ export function useOrderHallPage() {
isConnected: signalRActions.isConnected, isConnected: signalRActions.isConnected,
// 数据 // 数据
loadFullBoard: dataActions.loadFullBoard, loadFullBoard: dataActions.loadFullBoard,
loadStores: dataActions.loadStores,
loadStats: dataActions.loadStats, loadStats: dataActions.loadStats,
// 卡片操作 // 卡片操作
handleAccept: cardActions.handleAccept, handleAccept: cardActions.handleAccept,

View File

@@ -1,5 +1,7 @@
<!-- 文件职责订单大厅页面入口 --> <!-- 文件职责订单大厅页面入口 -->
<script setup lang="ts"> <script setup lang="ts">
import type { BoardColumnKey } from './types';
import BoardColumn from './components/BoardColumn.vue'; import BoardColumn from './components/BoardColumn.vue';
import BoardStatsBar from './components/BoardStatsBar.vue'; import BoardStatsBar from './components/BoardStatsBar.vue';
import BoardToolbar from './components/BoardToolbar.vue'; import BoardToolbar from './components/BoardToolbar.vue';
@@ -14,6 +16,8 @@ const {
stats, stats,
isRejectModalOpen, isRejectModalOpen,
isConnected, isConnected,
loadFullBoard,
loadStats,
handleAccept, handleAccept,
handleCompletePreparation, handleCompletePreparation,
handleConfirmDelivery, handleConfirmDelivery,
@@ -22,48 +26,82 @@ const {
cancelReject, cancelReject,
} = useOrderHallPage(); } = useOrderHallPage();
/** 根据列键获取对应卡片列表。 */ /** 判断卡片是否符合当前履约筛选。 */
function getCards(key: string) { function matchesDeliveryFilter(deliveryType: number) {
switch (key) { switch (state.deliveryFilter) {
case 'completed': { case 'delivery': {
return state.completedCards; return deliveryType === 2;
} }
case 'delivering': { case 'dineIn': {
return state.deliveringCards; return deliveryType === 0;
} }
case 'making': { case 'pickup': {
return state.makingCards; return deliveryType === 1;
}
case 'pending': {
return state.pendingCards;
} }
default: { default: {
return []; return true;
} }
} }
} }
/** 根据列键获取对应卡片列表。 */
function getCards(key: BoardColumnKey) {
let cards = [] as typeof state.pendingCards;
switch (key) {
case 'completed': {
cards = state.completedCards;
break;
}
case 'delivering': {
cards = state.deliveringCards;
break;
}
case 'making': {
cards = state.makingCards;
break;
}
case 'pending': {
cards = state.pendingCards;
break;
}
default: {
cards = [];
}
}
return cards.filter((card) => matchesDeliveryFilter(card.deliveryType));
}
/** 手动刷新看板。 */
async function handleRefresh() {
await Promise.all([loadFullBoard(), loadStats()]);
}
</script> </script>
<template> <template>
<div class="ob-page"> <div class="ob-page">
<!-- 1. 工具栏 --> <!-- 1. 统计条 + 控制区 -->
<BoardToolbar
v-model:store-id="state.selectedStoreId"
v-model:channel="state.channel"
v-model:is-sound-enabled="state.isSoundEnabled"
:is-connected="isConnected"
/>
<!-- 2. 统计条 -->
<BoardStatsBar <BoardStatsBar
:today-total="stats.todayTotal" :today-total="stats.todayTotal"
:pending-count="stats.pendingCount" :pending-count="stats.pendingCount"
:making-count="stats.makingCount" :making-count="stats.makingCount"
:delivering-count="stats.deliveringCount" :delivering-count="stats.deliveringCount"
:completed-count="stats.completedCount" :completed-count="stats.completedCount"
/> >
<template #actions>
<BoardToolbar
v-model:store-id="state.selectedStoreId"
v-model:delivery-filter="state.deliveryFilter"
v-model:is-sound-enabled="state.isSoundEnabled"
:store-options="state.storeOptions"
:is-store-loading="state.isStoreLoading"
:is-connected="isConnected"
@refresh="handleRefresh"
/>
</template>
</BoardStatsBar>
<!-- 3. 看板四列 --> <!-- 2. 看板四列 -->
<div class="ob-board"> <div class="ob-board">
<BoardColumn <BoardColumn
v-for="col in HALL_COLUMNS" v-for="col in HALL_COLUMNS"
@@ -80,7 +118,7 @@ function getCards(key: string) {
/> />
</div> </div>
<!-- 4. 拒单弹窗 --> <!-- 3. 拒单弹窗 -->
<RejectOrderModal <RejectOrderModal
:open="isRejectModalOpen" :open="isRejectModalOpen"
@confirm="confirmReject" @confirm="confirmReject"

View File

@@ -1,39 +1,18 @@
/* 文件职责:订单大厅动画效果。 */ /* 文件职责:订单大厅动画效果。 */
/* 待接单卡片脉冲边框 */
.ob-card--pulse {
animation: ob-pulse-border 2s ease-in-out infinite;
}
@keyframes ob-pulse-border { @keyframes ob-pulse-border {
0%, 0%,
100% { 100% {
box-shadow: 0 1px 2px rgb(0 0 0 / 6%); border-left-color: #fa8c16;
} }
50% { 50% {
box-shadow: border-left-color: #ffd591;
0 0 0 3px rgb(250 140 22 / 30%),
0 2px 8px rgb(0 0 0 / 12%);
} }
} }
/* 催单标记闪烁 */ .ob-order.is-urged {
.ob-card--urged::after { animation: ob-pulse-border 2s ease-in-out infinite;
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
font-size: 12px;
font-weight: 700;
line-height: 24px;
color: #fff;
text-align: center;
content: '催';
background: #ff4d4f;
border-radius: 50%;
animation: ob-urge-blink 1s ease-in-out infinite;
} }
@keyframes ob-urge-blink { @keyframes ob-urge-blink {
@@ -43,6 +22,10 @@
} }
50% { 50% {
opacity: 0.3; opacity: 0.5;
} }
} }
.ob-order.is-urged .ob-time-chip {
animation: ob-urge-blink 1.3s ease-in-out infinite;
}

View File

@@ -1,92 +1,239 @@
/* 文件职责:订单大厅卡片样式。 */ /* 文件职责:订单大厅卡片样式(对齐新版原型)。 */
.ob-card { .ob-order {
position: relative; position: relative;
padding: 12px; padding: 14px 14px 12px;
margin-bottom: 8px;
background: #fff; background: #fff;
border-left: 4px solid #d9d9d9; border: 1px solid #e7ebf1;
border-radius: 6px; border-radius: 12px;
box-shadow: 0 1px 2px rgb(0 0 0 / 6%); box-shadow: 0 1px 3px rgb(15 23 42 / 6%);
transition: box-shadow 0.2s;
} }
.ob-card:hover { .ob-order.is-urged {
box-shadow: 0 2px 8px rgb(0 0 0 / 12%); border-left: 4px solid #f04438;
} }
/* 列颜色标识 */ .ob-order-top {
.ob-col-pending .ob-card {
border-left-color: #fa8c16;
}
.ob-col-making .ob-card {
border-left-color: #1890ff;
}
.ob-col-delivering .ob-card {
border-left-color: #52c41a;
}
.ob-col-completed .ob-card {
border-left-color: #d9d9d9;
}
/* 已完成卡片半透明 */
.ob-card--dimmed {
opacity: 0.6;
}
.ob-card__header {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.ob-card__order-no {
font-size: 14px;
font-weight: 600;
}
.ob-card__body {
font-size: 13px;
color: #595959;
}
.ob-card__row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 4px; margin-bottom: 14px;
} }
.ob-card__items { .ob-order-top-left {
overflow: hidden;
text-overflow: ellipsis;
color: #8c8c8c;
white-space: nowrap;
}
.ob-card__amount {
font-weight: 600;
color: #262626;
}
.ob-card__time {
font-size: 12px;
color: #bfbfbf;
}
.ob-card__elapsed {
margin-top: 4px;
font-size: 12px;
color: #1890ff;
}
.ob-card__actions {
display: flex; display: flex;
gap: 8px; gap: 10px;
padding-top: 8px; align-items: center;
margin-top: 8px; }
border-top: 1px solid #f0f0f0;
.ob-type-tag {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 10px;
font-size: 13px;
font-weight: 700;
border-radius: 8px;
&.is-delivery {
color: #3f7fe8;
background: #e5edff;
}
&.is-pickup {
color: #1c9f62;
background: #dff5ea;
}
&.is-dine-in {
color: #8246af;
background: #f0e2fd;
}
&.is-default {
color: #667085;
background: #f2f4f7;
}
}
.ob-short-no {
font-size: 34px;
font-weight: 800;
line-height: 1.05;
color: #1d2939;
}
.ob-table-chip {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
font-size: 13px;
color: #667085;
background: #f2f4f7;
border-radius: 8px;
}
.ob-time-chip {
display: inline-flex;
gap: 4px;
align-items: center;
font-size: 14px;
color: #98a2b3;
.vben-iconify {
width: 14px;
height: 14px;
}
}
.ob-main-line {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 10px;
font-size: 15px;
font-weight: 700;
line-height: 1.25;
color: #1d2939;
.vben-iconify {
width: 15px;
height: 15px;
color: #98a2b3;
}
}
.ob-order-mid {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.ob-full-no {
font-size: 11px;
color: #8a94a6;
}
.ob-amount {
font-size: 34px;
font-weight: 900;
line-height: 1;
color: #e55a0a;
}
.ob-actions {
.ant-btn {
height: 38px;
font-size: 15px;
font-weight: 700;
border-radius: 10px;
}
}
.ob-actions-pending {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.ob-actions-making {
display: flex;
flex-direction: column;
gap: 12px;
.ant-progress {
margin-bottom: 0;
}
.ant-progress-inner {
height: 9px !important;
border-radius: 999px;
}
}
.ob-actions-delivering {
display: flex;
align-items: center;
justify-content: space-between;
}
.ob-deliver-info {
display: inline-flex;
gap: 6px;
align-items: center;
font-size: 13px;
color: #667085;
.vben-iconify {
width: 14px;
height: 14px;
color: #98a2b3;
}
}
.ob-btn {
&.ob-btn-ghost {
color: #b03d4b;
background: #fff;
border: 1px solid #e9d4d8;
}
&.ob-btn-orange {
color: #fff;
background: #ff7a14;
border-color: #ff7a14;
}
&.ob-btn-blue {
color: #fff;
background: #3f7fe8;
border-color: #3f7fe8;
}
&.ob-btn-green {
min-width: 102px;
color: #fff;
background: #2fbf5f;
border-color: #2fbf5f;
}
}
.ob-column.done .ob-order {
opacity: 0.52;
}
@media (max-width: 768px) {
.ob-order {
padding: 12px;
}
.ob-order-top-left {
gap: 8px;
}
.ob-type-tag {
height: 22px;
padding: 0 8px;
font-size: 12px;
}
.ob-short-no {
font-size: 27px;
}
.ob-main-line {
font-size: 14px;
}
.ob-amount {
font-size: 28px;
}
.ob-actions {
.ant-btn {
height: 34px;
font-size: 14px;
}
}
} }

View File

@@ -1,90 +1,322 @@
/* 文件职责:订单大厅看板布局。 */ /* 文件职责:订单大厅看板布局。 */
.ob-page { .ob-page {
--ob-surface: #fff;
--ob-border: #e7e9ee;
--ob-board-bg: #f4f5f7;
--ob-text: #1f2937;
--ob-muted: #98a2b3;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
height: 100%; height: calc(
padding: 12px; 100vh - var(--header-height, 64px) - var(--tabbar-height, 42px) - 28px
);
min-height: 620px;
padding: 2px 2px 4px;
font-size: 13px;
color: var(--ob-text);
}
.ob-stats {
display: flex;
flex-shrink: 0;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
padding: 12px 16px 14px;
background: var(--ob-surface);
border: 1px solid var(--ob-border);
border-radius: 12px;
}
.ob-stats-main {
display: flex;
flex-wrap: wrap;
gap: 34px;
align-items: flex-end;
}
.ob-stats-actions {
display: flex;
flex: 1;
justify-content: flex-end;
min-width: 360px;
}
.ob-summary-item {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 70px;
&.is-active::after {
position: absolute;
bottom: -11px;
left: 0;
width: 100%;
height: 3px;
content: '';
background: #3f7fe8;
border-radius: 999px;
}
}
.ob-summary-label {
font-size: 12px;
font-weight: 500;
color: rgb(0 0 0 / 45%);
}
.ob-summary-value {
font-size: 36px;
font-weight: 700;
line-height: 1;
&.is-default {
color: #344054;
}
&.is-pending {
color: #fa8c16;
}
&.is-making {
color: #3f7fe8;
}
&.is-delivering {
color: #22b55f;
}
&.is-muted {
color: var(--ob-muted);
}
} }
.ob-board { .ob-board {
display: flex; display: flex;
flex: 1; flex: 1;
gap: 12px; gap: 14px;
min-height: 0;
padding: 14px 12px 10px;
overflow-x: auto; overflow-x: auto;
background: var(--ob-board-bg);
border: 1px solid var(--ob-border);
border-radius: 12px;
}
.ob-board::-webkit-scrollbar {
height: 6px;
}
.ob-board::-webkit-scrollbar-thumb {
background: #d3d7df;
border-radius: 3px;
} }
.ob-column { .ob-column {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
min-width: 280px; min-width: 290px;
background: #fafafa; overflow: hidden;
border-radius: 8px; background: #f2f4f7;
border: 1px solid #e7e9ee;
border-radius: 12px;
} }
.ob-column__header { .ob-col-hd {
display: flex; display: flex;
flex-shrink: 0;
gap: 8px;
align-items: center; align-items: center;
justify-content: space-between; min-height: 46px;
padding: 12px 16px; padding: 12px 12px 8px;
border-bottom: 1px solid #f0f0f0; font-size: 36px;
font-weight: 700;
color: #111827;
&.making,
&.delivering {
&::before {
display: inline-block;
width: 3px;
height: 24px;
margin-right: 4px;
content: '';
border-radius: 999px;
}
}
&.making::before {
background: #3f7fe8;
}
&.delivering::before {
background: #22b55f;
}
&.done {
color: var(--ob-muted);
}
} }
.ob-column__title { .ob-col-count {
font-size: 15px; margin-left: 4px;
font-weight: 600; font-weight: 800;
line-height: 1;
} }
.ob-column__body { .ob-col-count.is-pending {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 8px;
font-size: 20px;
color: #fff;
background: #ff7a14;
border-radius: 999px;
}
.ob-col-count.is-making {
font-size: 40px;
color: #3f7fe8;
}
.ob-col-count.is-delivering {
font-size: 33px;
color: #22b55f;
}
.ob-col-body {
display: flex;
flex: 1; flex: 1;
padding: 8px; flex-direction: column;
gap: 10px;
min-height: 0;
padding: 0 8px 10px;
overflow-y: auto; overflow-y: auto;
.ant-spin-nested-loading {
height: 100%;
}
.ant-spin-container {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 100%;
}
}
.ob-col-body::-webkit-scrollbar {
width: 4px;
}
.ob-col-body::-webkit-scrollbar-thumb {
background: #d2d8e2;
border-radius: 2px;
} }
.ob-column__empty { .ob-column__empty {
padding: 40px 0; padding: 34px 0;
color: #bfbfbf; font-size: 13px;
color: rgb(0 0 0 / 35%);
text-align: center; text-align: center;
} }
/* 统计条 */ .ob-column__placeholder {
.ob-stats { height: 100%;
display: flex; background: transparent;
gap: 16px; border: 2px dashed #d6dae2;
padding: 8px 0; border-radius: 16px;
} }
.ob-stats__item { @media (max-width: 1400px) {
display: flex; .ob-stats {
flex-direction: column; flex-direction: column;
align-items: center; align-items: stretch;
min-width: 80px; }
.ob-stats-actions {
justify-content: flex-start;
min-width: 0;
}
.ob-column {
min-width: 260px;
}
} }
.ob-stats__value { @media (max-width: 1024px) {
font-size: 24px; .ob-page {
font-weight: 700; height: auto;
line-height: 1.2; min-height: calc(100vh - 90px);
}
.ob-summary-value {
font-size: 28px;
}
.ob-col-hd {
font-size: 30px;
}
.ob-col-count.is-making {
font-size: 34px;
}
.ob-col-count.is-delivering {
font-size: 28px;
}
} }
.ob-stats__label { @media (max-width: 768px) {
font-size: 12px; .ob-stats {
color: #8c8c8c; gap: 10px;
} padding: 10px 12px;
}
.ob-stats__item--pending .ob-stats__value { .ob-stats-main {
color: #fa8c16; gap: 20px;
} }
.ob-stats__item--making .ob-stats__value { .ob-summary-item {
color: #1890ff; min-width: 62px;
} }
.ob-stats__item--delivering .ob-stats__value { .ob-summary-value {
color: #52c41a; font-size: 22px;
} }
.ob-stats__item--completed .ob-stats__value { .ob-board {
color: #8c8c8c; padding: 10px;
}
.ob-column {
min-width: 236px;
}
.ob-col-hd {
min-height: 40px;
font-size: 24px;
}
.ob-col-count.is-pending {
min-width: 22px;
height: 22px;
font-size: 14px;
}
.ob-col-count.is-making {
font-size: 27px;
}
.ob-col-count.is-delivering {
font-size: 23px;
}
} }

View File

@@ -1,47 +1,157 @@
/* 文件职责:订单大厅工具栏样式。 */ /* 文件职责:订单大厅工具栏样式。 */
.ob-toolbar { .ob-toolbar-shell {
width: 100%;
max-width: 920px;
.ob-store-select .ant-select-selector {
height: 32px !important;
padding-top: 1px;
border-radius: 8px !important;
}
.ob-store-select .ant-select-selection-item,
.ob-store-select .ant-select-selection-placeholder {
font-size: 13px;
line-height: 30px !important;
}
}
.ob-toolbar-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px; gap: 10px;
align-items: center; align-items: center;
padding: 8px 0; justify-content: flex-end;
.ob-channels {
display: flex;
gap: 6px;
align-items: center;
padding: 2px;
background: #f2f4f7;
border-radius: 999px;
}
.ob-channel {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 56px;
height: 28px;
padding: 0 12px;
font-size: 12px;
font-weight: 500;
color: rgb(0 0 0 / 55%);
cursor: pointer;
background: transparent;
border: none;
border-radius: 999px;
transition: all 0.2s;
&.active {
color: #1677ff;
background: #fff;
box-shadow: 0 1px 3px rgb(22 119 255 / 18%);
}
}
.ob-store-select {
width: 180px;
}
.ob-toolbar-right {
display: flex;
gap: 8px;
align-items: center;
padding-left: 6px;
}
.ob-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: rgb(0 0 0 / 65%);
cursor: pointer;
background: #f8fafc;
border: 1px solid #d9d9d9;
border-radius: 8px;
transition: all 0.2s;
.vben-iconify {
width: 16px;
height: 16px;
}
&:hover {
color: #1677ff;
border-color: #1677ff;
}
&.active {
color: #fff;
background: #1677ff;
border-color: #1677ff;
}
}
.ob-icon-btn--offline {
color: #fa8c16;
}
.ob-connection {
display: inline-flex;
gap: 6px;
align-items: center;
font-size: 12px;
color: #23b26b;
.ob-connection-dot {
width: 7px;
height: 7px;
background: currentcolor;
border-radius: 50%;
}
&.offline {
color: #f59e0b;
}
}
} }
.ob-toolbar__store { @media (max-width: 1200px) {
display: flex; .ob-toolbar-shell {
gap: 8px; max-width: 100%;
align-items: center;
.ob-toolbar-actions {
justify-content: flex-start;
}
}
} }
.ob-toolbar__label { @media (max-width: 768px) {
font-size: 13px; .ob-toolbar-shell {
color: #595959; .ob-store-select {
white-space: nowrap; width: 100%;
} }
.ob-toolbar__channel { .ob-toolbar-actions {
flex: 1; gap: 8px;
} justify-content: flex-start;
width: 100%;
}
.ob-toolbar__right { .ob-channels {
display: flex; width: 100%;
gap: 12px; overflow-x: auto;
align-items: center; white-space: nowrap;
margin-left: auto; }
}
.ob-toolbar__status { .ob-toolbar-right {
padding: 2px 8px; justify-content: space-between;
font-size: 12px; width: 100%;
border-radius: 10px; padding-left: 0;
} }
}
.ob-toolbar__status--on {
color: #52c41a;
background: #f6ffed;
}
.ob-toolbar__status--off {
color: #ff4d4f;
background: #fff2f0;
} }

View File

@@ -2,18 +2,19 @@
* 文件职责:订单大厅本地类型定义。 * 文件职责:订单大厅本地类型定义。
*/ */
import type { OrderBoardCard } from '#/api/order-hall'; import type { OrderBoardCard } from '#/api/order-hall';
import type { StoreListItemDto } from '#/api/store';
/** 看板列键。 */ /** 看板列键。 */
export type BoardColumnKey = 'completed' | 'delivering' | 'making' | 'pending'; export type BoardColumnKey = 'completed' | 'delivering' | 'making' | 'pending';
/** 渠道筛选值。 */ /** 履约类型筛选值。 */
export type BoardChannelFilter = export type BoardDeliveryFilter = 'all' | 'delivery' | 'dineIn' | 'pickup';
| 'all'
| 'miniprogram' /** 门店下拉选项。 */
| 'phone' export interface StoreOptionItem {
| 'scan' label: string;
| 'staff' value: string;
| 'thirdparty'; }
/** SignalR 新订单推送载荷。 */ /** SignalR 新订单推送载荷。 */
export interface NewOrderPayload { export interface NewOrderPayload {
@@ -53,12 +54,31 @@ export interface OrderUrgedPayload {
/** 看板页面状态。 */ /** 看板页面状态。 */
export interface OrderHallPageState { export interface OrderHallPageState {
channel: BoardChannelFilter; deliveryFilter: BoardDeliveryFilter;
completedCards: OrderBoardCard[]; completedCards: OrderBoardCard[];
deliveringCards: OrderBoardCard[]; deliveringCards: OrderBoardCard[];
isStoreLoading: boolean;
isLoading: boolean; isLoading: boolean;
isSoundEnabled: boolean; isSoundEnabled: boolean;
makingCards: OrderBoardCard[]; makingCards: OrderBoardCard[];
pendingCards: OrderBoardCard[]; pendingCards: OrderBoardCard[];
selectedStoreId: string; selectedStoreId: string;
storeOptions: StoreOptionItem[];
}
/** 订单大厅统计状态。 */
export interface OrderHallStatsState {
completedCount: number;
deliveringCount: number;
makingCount: number;
pendingCount: number;
todayTotal: number;
}
/** 门店列表与选项转换。 */
export function mapStoreOptions(stores: StoreListItemDto[]): StoreOptionItem[] {
return stores.map((item) => ({
label: item.name,
value: item.id,
}));
} }