diff --git a/apps/web-antd/public/sounds/README.md b/apps/web-antd/public/sounds/README.md new file mode 100644 index 0000000..e021308 --- /dev/null +++ b/apps/web-antd/public/sounds/README.md @@ -0,0 +1,5 @@ +Sound placeholder files: +- new-order.mp3: 新订单提示音(需替换为实际音频文件) +- urge.mp3: 催单提示音(需替换为实际音频文件) + +请将实际的 MP3 音频文件放置到此目录下。 diff --git a/apps/web-antd/src/router/routes/modules/order.ts b/apps/web-antd/src/router/routes/modules/order.ts index 9d0aa19..b8fa730 100644 --- a/apps/web-antd/src/router/routes/modules/order.ts +++ b/apps/web-antd/src/router/routes/modules/order.ts @@ -11,6 +11,15 @@ const routes: RouteRecordRaw[] = [ name: 'Order', path: '/order', children: [ + { + name: 'OrderBoard', + path: '/order/board', + component: () => import('#/views/order/board/index.vue'), + meta: { + icon: 'lucide:layout-dashboard', + title: '订单大厅', + }, + }, { name: 'OrderAll', path: '/order/list', diff --git a/apps/web-antd/src/views/order/board/components/BoardColumn.vue b/apps/web-antd/src/views/order/board/components/BoardColumn.vue new file mode 100644 index 0000000..703f81d --- /dev/null +++ b/apps/web-antd/src/views/order/board/components/BoardColumn.vue @@ -0,0 +1,56 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/components/BoardOrderCard.vue b/apps/web-antd/src/views/order/board/components/BoardOrderCard.vue new file mode 100644 index 0000000..2a631a4 --- /dev/null +++ b/apps/web-antd/src/views/order/board/components/BoardOrderCard.vue @@ -0,0 +1,104 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/components/BoardStatsBar.vue b/apps/web-antd/src/views/order/board/components/BoardStatsBar.vue new file mode 100644 index 0000000..2c11c04 --- /dev/null +++ b/apps/web-antd/src/views/order/board/components/BoardStatsBar.vue @@ -0,0 +1,41 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/components/BoardToolbar.vue b/apps/web-antd/src/views/order/board/components/BoardToolbar.vue new file mode 100644 index 0000000..7421bf2 --- /dev/null +++ b/apps/web-antd/src/views/order/board/components/BoardToolbar.vue @@ -0,0 +1,75 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/components/RejectOrderModal.vue b/apps/web-antd/src/views/order/board/components/RejectOrderModal.vue new file mode 100644 index 0000000..4029331 --- /dev/null +++ b/apps/web-antd/src/views/order/board/components/RejectOrderModal.vue @@ -0,0 +1,32 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/card-actions.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/card-actions.ts new file mode 100644 index 0000000..ea4e534 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/card-actions.ts @@ -0,0 +1,58 @@ +/** + * 文件职责:订单大厅卡片操作动作(接单、拒单、出餐、确认送达)。 + */ +import { message } from 'ant-design-vue'; + +import { + acceptOrderApi, + completePreparationApi, + confirmDeliveryApi, + rejectOrderApi, +} from '#/api/order-board'; + +/** 卡片操作动作选项。 */ +interface CardActionsOptions { + onRefresh: () => Promise; + onAccepted?: () => void; +} + +/** 创建卡片操作动作。 */ +export function createCardActions(options: CardActionsOptions) { + const { onRefresh, onAccepted } = options; + + // 1. 接单 + async function handleAccept(orderId: string): Promise { + await acceptOrderApi(orderId); + message.success('接单成功'); + onAccepted?.(); + await onRefresh(); + } + + // 2. 拒单 + async function handleReject(orderId: string, reason: string): Promise { + await rejectOrderApi(orderId, { reason }); + message.success('已拒单'); + await onRefresh(); + } + + // 3. 出餐完成 + async function handleCompletePreparation(orderId: string): Promise { + await completePreparationApi(orderId); + message.success('出餐完成'); + await onRefresh(); + } + + // 4. 确认送达/取餐 + async function handleConfirmDelivery(orderId: string): Promise { + await confirmDeliveryApi(orderId); + message.success('已确认送达'); + await onRefresh(); + } + + return { + handleAccept, + handleReject, + handleCompletePreparation, + handleConfirmDelivery, + }; +} diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/constants.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/constants.ts new file mode 100644 index 0000000..51633d4 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/constants.ts @@ -0,0 +1,62 @@ +/** + * 文件职责:订单大厅常量与配置。 + */ +import type { BoardChannelFilter, BoardColumnKey } from '../../types'; + +/** 看板列定义。 */ +export const BOARD_COLUMNS: { + colorClass: string; + key: BoardColumnKey; + title: string; +}[] = [ + { key: 'pending', title: '待接单', colorClass: 'ob-col-pending' }, + { key: 'making', title: '制作中', colorClass: 'ob-col-making' }, + { key: 'delivering', title: '配送/待取', colorClass: 'ob-col-delivering' }, + { key: 'completed', title: '已完成', colorClass: 'ob-col-completed' }, +]; + +/** 渠道筛选选项。 */ +export const CHANNEL_OPTIONS: { label: string; value: BoardChannelFilter }[] = [ + { value: 'all', label: '全部渠道' }, + { value: 'miniprogram', label: '小程序' }, + { value: 'scan', label: '扫码点餐' }, + { value: 'staff', label: '收银台' }, + { value: 'phone', label: '电话预约' }, + { value: 'thirdparty', label: '第三方' }, +]; + +/** 订单状态映射。 */ +export const ORDER_STATUS_MAP: Record = { + 0: '待付款', + 1: '待接单', + 2: '制作中', + 3: '待取餐', + 4: '已完成', + 5: '已取消', +}; + +/** 渠道映射。 */ +export const CHANNEL_MAP: Record = { + 0: '未知', + 1: '小程序', + 2: '扫码点餐', + 3: '收银台', + 4: '电话预约', + 5: '第三方', +}; + +/** 履约类型映射。 */ +export const DELIVERY_TYPE_MAP: Record = { + 0: '堂食', + 1: '自提', + 2: '外卖', +}; + +/** 渠道标签颜色。 */ +export const CHANNEL_TAG_COLORS: Record = { + 1: 'green', + 2: 'blue', + 3: 'orange', + 4: 'purple', + 5: 'red', +}; diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/data-actions.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/data-actions.ts new file mode 100644 index 0000000..54ea568 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/data-actions.ts @@ -0,0 +1,70 @@ +/** + * 文件职责:订单大厅数据加载动作。 + */ +import type { Ref } from 'vue'; + +import type { OrderBoardPageState } from '../../types'; + +import type { OrderBoardCard } from '#/api/order-board'; + +import { + getOrderBoardApi, + getOrderBoardStatsApi, + getPendingSinceApi, +} from '#/api/order-board'; + +/** 数据加载动作选项。 */ +interface DataActionsOptions { + state: OrderBoardPageState; + statsRef: Ref<{ + completedCount: number; + deliveringCount: number; + makingCount: number; + pendingCount: number; + todayTotal: number; + }>; +} + +/** 创建数据加载动作。 */ +export function createDataActions(options: DataActionsOptions) { + const { state, statsRef } = options; + + // 1. 加载完整看板数据 + async function loadFullBoard(): Promise { + if (!state.selectedStoreId) return; + state.isLoading = true; + try { + const channelParam = state.channel === 'all' ? undefined : state.channel; + const result = await getOrderBoardApi({ + storeId: state.selectedStoreId, + channel: channelParam, + }); + state.pendingCards = result.pending; + state.makingCards = result.making; + state.deliveringCards = result.delivering; + state.completedCards = result.completed; + } finally { + state.isLoading = false; + } + } + + // 2. 加载统计数据 + async function loadStats(): Promise { + if (!state.selectedStoreId) return; + const result = await getOrderBoardStatsApi({ + storeId: state.selectedStoreId, + }); + statsRef.value = result; + } + + // 3. 重连补偿拉取 + async function catchUpSince(since: Date): Promise { + if (!state.selectedStoreId) return []; + return getPendingSinceApi({ + storeId: state.selectedStoreId, + since: since.toISOString(), + }); + } + + return { loadFullBoard, loadStats, catchUpSince }; +} diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/helpers.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/helpers.ts new file mode 100644 index 0000000..9fecfa0 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/helpers.ts @@ -0,0 +1,39 @@ +/** + * 文件职责:订单大厅纯工具函数(格式化、过滤等)。 + */ +import dayjs from 'dayjs'; + +import { CHANNEL_MAP, DELIVERY_TYPE_MAP } from './constants'; + +/** 格式化金额(保留两位小数)。 */ +export function formatAmount(value: number): string { + return `¥${value.toFixed(2)}`; +} + +/** 格式化时间为 HH:mm:ss。 */ +export function formatTime(value?: null | string): string { + if (!value) return '--'; + return dayjs(value).format('HH:mm:ss'); +} + +/** 格式化时间为 MM-DD HH:mm。 */ +export function formatDateTime(value?: null | string): string { + if (!value) return '--'; + return dayjs(value).format('MM-DD HH:mm'); +} + +/** 计算已用时间(分钟)。 */ +export function elapsedMinutes(since?: null | string): number { + if (!since) return 0; + return Math.floor(dayjs().diff(dayjs(since), 'minute')); +} + +/** 获取渠道文本。 */ +export function channelText(channel: number): string { + return CHANNEL_MAP[channel] ?? '未知'; +} + +/** 获取履约类型文本。 */ +export function deliveryTypeText(deliveryType: number): string { + return DELIVERY_TYPE_MAP[deliveryType] ?? '未知'; +} diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/notification-actions.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/notification-actions.ts new file mode 100644 index 0000000..f06ea41 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/notification-actions.ts @@ -0,0 +1,76 @@ +/** + * 文件职责:订单大厅声音播放与桌面通知。 + */ + +/** 通知动作选项。 */ +interface NotificationActionsOptions { + isSoundEnabled: { value: boolean }; +} + +/** 创建通知动作。 */ +export function createNotificationActions(options: NotificationActionsOptions) { + let newOrderAudio: HTMLAudioElement | null = null; + let urgeAudio: HTMLAudioElement | null = null; + let isPlaying = false; + + // 1. 懒初始化音频(需要用户交互后才能播放) + function initAudio(): void { + if (!newOrderAudio) { + newOrderAudio = new Audio('/sounds/new-order.mp3'); + newOrderAudio.loop = true; + } + if (!urgeAudio) { + urgeAudio = new Audio('/sounds/urge.mp3'); + } + } + + // 2. 播放新订单提示音(循环播放直到接单) + function playNewOrderSound(): void { + if (!options.isSoundEnabled.value || isPlaying) return; + initAudio(); + isPlaying = true; + newOrderAudio?.play().catch(() => { + // 浏览器可能阻止自动播放 + isPlaying = false; + }); + } + + // 3. 停止新订单提示音 + function stopNewOrderSound(): void { + if (newOrderAudio) { + newOrderAudio.pause(); + newOrderAudio.currentTime = 0; + } + isPlaying = false; + } + + // 4. 播放催单提示音(播放一次) + function playUrgeSound(): void { + if (!options.isSoundEnabled.value) return; + initAudio(); + urgeAudio?.play().catch(() => {}); + } + + // 5. 显示桌面通知 + function showDesktopNotification(title: string, body: string): void { + if (Notification.permission === 'granted') { + // eslint-disable-next-line no-new + new Notification(title, { body, icon: '/favicon.ico' }); + } + } + + // 6. 请求桌面通知权限 + function requestNotificationPermission(): void { + if ('Notification' in window && Notification.permission === 'default') { + Notification.requestPermission(); + } + } + + return { + playNewOrderSound, + stopNewOrderSound, + playUrgeSound, + showDesktopNotification, + requestNotificationPermission, + }; +} diff --git a/apps/web-antd/src/views/order/board/composables/order-board-page/signalr-actions.ts b/apps/web-antd/src/views/order/board/composables/order-board-page/signalr-actions.ts new file mode 100644 index 0000000..2c4dc72 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/order-board-page/signalr-actions.ts @@ -0,0 +1,88 @@ +import type { + NewOrderPayload, + OrderBoardPageState, + OrderStatusChangedPayload, + OrderUrgedPayload, +} from '../../types'; + +/** + * 文件职责:订单大厅 SignalR 连接与事件处理。 + */ +import type { OrderBoardCard } from '#/api/order-board'; + +import { useSignalR } from '#/hooks/useSignalR'; + +/** SignalR 动作选项。 */ +interface SignalRActionsOptions { + state: OrderBoardPageState; + onNewOrder?: () => void; + onUrge?: () => void; + catchUpSince: (since: Date) => Promise; + loadFullBoard: () => Promise; + loadStats: () => Promise; +} + +/** 创建 SignalR 动作。 */ +export function createSignalRActions(options: SignalRActionsOptions) { + const { state, onNewOrder, onUrge, catchUpSince, loadFullBoard, loadStats } = + options; + + const { isConnected, connect, disconnect, on, invoke } = useSignalR({ + onReconnected: async (lastDisconnectedAt) => { + // 1. 重连后补偿拉取 + const missed = await catchUpSince(lastDisconnectedAt); + if (missed.length > 0) { + await loadFullBoard(); + await loadStats(); + } + }, + }); + + // 2. 建立连接并订阅事件 + async function connectAndSubscribe(): Promise { + if (!state.selectedStoreId) return; + + await connect(state.selectedStoreId); + + // 3. 订阅新订单 + on('NewOrder', () => { + loadFullBoard(); + loadStats(); + onNewOrder?.(); + }); + + // 4. 订阅状态变更 + on('OrderStatusChanged', () => { + loadFullBoard(); + loadStats(); + }); + + // 5. 订阅催单 + on('OrderUrged', (data) => { + // 标记对应卡片为催单状态 + const allCards = [ + ...state.pendingCards, + ...state.makingCards, + ...state.deliveringCards, + ]; + const card = allCards.find((c) => c.orderNo === data.orderNo); + if (card) { + card.isUrged = true; + card.urgeCount = data.urgeCount; + } + onUrge?.(); + }); + } + + // 6. 切换门店 + async function switchStore(storeId: string): Promise { + await (isConnected.value ? invoke('JoinStore', storeId) : connect(storeId)); + } + + return { + isConnected, + connectAndSubscribe, + disconnect, + switchStore, + }; +} diff --git a/apps/web-antd/src/views/order/board/composables/useOrderBoardPage.ts b/apps/web-antd/src/views/order/board/composables/useOrderBoardPage.ts new file mode 100644 index 0000000..ebba419 --- /dev/null +++ b/apps/web-antd/src/views/order/board/composables/useOrderBoardPage.ts @@ -0,0 +1,147 @@ +import type { OrderBoardPageState } from '../types'; + +/** + * 文件职责:订单大厅页面编排器,组合所有子动作模块。 + */ +import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; + +import { createCardActions } from './order-board-page/card-actions'; +import { createDataActions } from './order-board-page/data-actions'; +import { createNotificationActions } from './order-board-page/notification-actions'; +import { createSignalRActions } from './order-board-page/signalr-actions'; + +/** 订单大厅页面 Hook。 */ +export function useOrderBoardPage() { + // 1. 页面状态 + const state = reactive({ + selectedStoreId: '', + channel: 'all', + isLoading: false, + isSoundEnabled: true, + pendingCards: [], + makingCards: [], + deliveringCards: [], + completedCards: [], + }); + + const stats = ref({ + todayTotal: 0, + pendingCount: 0, + makingCount: 0, + deliveringCount: 0, + completedCount: 0, + }); + + // 2. 拒单弹窗状态 + const isRejectModalOpen = ref(false); + const rejectingOrderId = ref(''); + + // 3. 创建通知动作 + const notificationActions = createNotificationActions({ + isSoundEnabled: { + get value() { + return state.isSoundEnabled; + }, + }, + }); + + // 4. 创建数据动作 + const dataActions = createDataActions({ state, statsRef: stats }); + + // 5. 创建 SignalR 动作 + const signalRActions = createSignalRActions({ + state, + onNewOrder: () => { + notificationActions.playNewOrderSound(); + notificationActions.showDesktopNotification( + '新订单', + '有新订单到达,请及时处理', + ); + }, + onUrge: () => { + notificationActions.playUrgeSound(); + }, + catchUpSince: dataActions.catchUpSince, + loadFullBoard: dataActions.loadFullBoard, + loadStats: dataActions.loadStats, + }); + + // 6. 刷新回调 + async function refresh(): Promise { + await Promise.all([dataActions.loadFullBoard(), dataActions.loadStats()]); + } + + // 7. 创建卡片动作 + const cardActions = createCardActions({ + state, + onRefresh: refresh, + onAccepted: () => { + // 接单后如果待接单列为空则停止声音 + if (state.pendingCards.length <= 1) { + notificationActions.stopNewOrderSound(); + } + }, + }); + + // 8. 拒单流程 + function openRejectModal(orderId: string): void { + rejectingOrderId.value = orderId; + isRejectModalOpen.value = true; + } + + async function confirmReject(reason: string): Promise { + await cardActions.handleReject(rejectingOrderId.value, reason); + isRejectModalOpen.value = false; + rejectingOrderId.value = ''; + } + + function cancelReject(): void { + isRejectModalOpen.value = false; + rejectingOrderId.value = ''; + } + + // 9. 监听门店切换 + watch( + () => state.selectedStoreId, + async (newStoreId) => { + if (!newStoreId) return; + await refresh(); + await signalRActions.switchStore(newStoreId); + }, + ); + + // 10. 监听渠道切换 + watch( + () => state.channel, + () => dataActions.loadFullBoard(), + ); + + // 11. 生命周期 + onMounted(() => { + notificationActions.requestNotificationPermission(); + }); + + onUnmounted(() => { + signalRActions.disconnect(); + notificationActions.stopNewOrderSound(); + }); + + return { + state, + stats, + isRejectModalOpen, + isConnected: signalRActions.isConnected, + // 数据 + loadFullBoard: dataActions.loadFullBoard, + loadStats: dataActions.loadStats, + // 卡片操作 + handleAccept: cardActions.handleAccept, + handleCompletePreparation: cardActions.handleCompletePreparation, + handleConfirmDelivery: cardActions.handleConfirmDelivery, + openRejectModal, + confirmReject, + cancelReject, + // SignalR + connectAndSubscribe: signalRActions.connectAndSubscribe, + }; +} diff --git a/apps/web-antd/src/views/order/board/index.vue b/apps/web-antd/src/views/order/board/index.vue new file mode 100644 index 0000000..65a9def --- /dev/null +++ b/apps/web-antd/src/views/order/board/index.vue @@ -0,0 +1,90 @@ + + + + diff --git a/apps/web-antd/src/views/order/board/styles/animations.less b/apps/web-antd/src/views/order/board/styles/animations.less new file mode 100644 index 0000000..eb65250 --- /dev/null +++ b/apps/web-antd/src/views/order/board/styles/animations.less @@ -0,0 +1,48 @@ +/* 文件职责:订单大厅动画效果。 */ + +/* 待接单卡片脉冲边框 */ +.ob-card--pulse { + animation: ob-pulse-border 2s ease-in-out infinite; +} + +@keyframes ob-pulse-border { + 0%, + 100% { + box-shadow: 0 1px 2px rgb(0 0 0 / 6%); + } + + 50% { + box-shadow: + 0 0 0 3px rgb(250 140 22 / 30%), + 0 2px 8px rgb(0 0 0 / 12%); + } +} + +/* 催单标记闪烁 */ +.ob-card--urged::after { + 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 { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} diff --git a/apps/web-antd/src/views/order/board/styles/card.less b/apps/web-antd/src/views/order/board/styles/card.less new file mode 100644 index 0000000..db724ff --- /dev/null +++ b/apps/web-antd/src/views/order/board/styles/card.less @@ -0,0 +1,92 @@ +/* 文件职责:订单大厅卡片样式。 */ +.ob-card { + position: relative; + padding: 12px; + margin-bottom: 8px; + background: #fff; + border-left: 4px solid #d9d9d9; + border-radius: 6px; + box-shadow: 0 1px 2px rgb(0 0 0 / 6%); + transition: box-shadow 0.2s; +} + +.ob-card:hover { + box-shadow: 0 2px 8px rgb(0 0 0 / 12%); +} + +/* 列颜色标识 */ +.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; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.ob-card__items { + 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; + gap: 8px; + padding-top: 8px; + margin-top: 8px; + border-top: 1px solid #f0f0f0; +} diff --git a/apps/web-antd/src/views/order/board/styles/index.less b/apps/web-antd/src/views/order/board/styles/index.less new file mode 100644 index 0000000..4b832f1 --- /dev/null +++ b/apps/web-antd/src/views/order/board/styles/index.less @@ -0,0 +1,5 @@ +/* 文件职责:订单大厅样式聚合。 */ +@import './layout.less'; +@import './toolbar.less'; +@import './card.less'; +@import './animations.less'; diff --git a/apps/web-antd/src/views/order/board/styles/layout.less b/apps/web-antd/src/views/order/board/styles/layout.less new file mode 100644 index 0000000..e938529 --- /dev/null +++ b/apps/web-antd/src/views/order/board/styles/layout.less @@ -0,0 +1,90 @@ +/* 文件职责:订单大厅看板布局。 */ +.ob-page { + display: flex; + flex-direction: column; + gap: 12px; + height: 100%; + padding: 12px; +} + +.ob-board { + display: flex; + flex: 1; + gap: 12px; + overflow-x: auto; +} + +.ob-column { + display: flex; + flex: 1; + flex-direction: column; + min-width: 280px; + background: #fafafa; + border-radius: 8px; +} + +.ob-column__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; +} + +.ob-column__title { + font-size: 15px; + font-weight: 600; +} + +.ob-column__body { + flex: 1; + padding: 8px; + overflow-y: auto; +} + +.ob-column__empty { + padding: 40px 0; + color: #bfbfbf; + text-align: center; +} + +/* 统计条 */ +.ob-stats { + display: flex; + gap: 16px; + padding: 8px 0; +} + +.ob-stats__item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 80px; +} + +.ob-stats__value { + font-size: 24px; + font-weight: 700; + line-height: 1.2; +} + +.ob-stats__label { + font-size: 12px; + color: #8c8c8c; +} + +.ob-stats__item--pending .ob-stats__value { + color: #fa8c16; +} + +.ob-stats__item--making .ob-stats__value { + color: #1890ff; +} + +.ob-stats__item--delivering .ob-stats__value { + color: #52c41a; +} + +.ob-stats__item--completed .ob-stats__value { + color: #8c8c8c; +} diff --git a/apps/web-antd/src/views/order/board/styles/toolbar.less b/apps/web-antd/src/views/order/board/styles/toolbar.less new file mode 100644 index 0000000..087b94a --- /dev/null +++ b/apps/web-antd/src/views/order/board/styles/toolbar.less @@ -0,0 +1,47 @@ +/* 文件职责:订单大厅工具栏样式。 */ +.ob-toolbar { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; + padding: 8px 0; +} + +.ob-toolbar__store { + display: flex; + gap: 8px; + align-items: center; +} + +.ob-toolbar__label { + font-size: 13px; + color: #595959; + white-space: nowrap; +} + +.ob-toolbar__channel { + flex: 1; +} + +.ob-toolbar__right { + display: flex; + gap: 12px; + align-items: center; + margin-left: auto; +} + +.ob-toolbar__status { + padding: 2px 8px; + font-size: 12px; + border-radius: 10px; +} + +.ob-toolbar__status--on { + color: #52c41a; + background: #f6ffed; +} + +.ob-toolbar__status--off { + color: #ff4d4f; + background: #fff2f0; +} diff --git a/apps/web-antd/src/views/order/board/types.ts b/apps/web-antd/src/views/order/board/types.ts new file mode 100644 index 0000000..2acb0c2 --- /dev/null +++ b/apps/web-antd/src/views/order/board/types.ts @@ -0,0 +1,64 @@ +/** + * 文件职责:订单大厅本地类型定义。 + */ +import type { OrderBoardCard } from '#/api/order-board'; + +/** 看板列键。 */ +export type BoardColumnKey = 'completed' | 'delivering' | 'making' | 'pending'; + +/** 渠道筛选值。 */ +export type BoardChannelFilter = + | 'all' + | 'miniprogram' + | 'phone' + | 'scan' + | 'staff' + | 'thirdparty'; + +/** SignalR 新订单推送载荷。 */ +export interface NewOrderPayload { + amount: number; + channel: number; + createdAt: string; + customerName?: null | string; + deliveryType: number; + itemsSummary?: null | string; + orderId: number; + orderNo: string; + storeId: number; + tableNo?: null | string; +} + +/** SignalR 状态变更推送载荷。 */ +export interface OrderStatusChangedPayload { + channel: number; + customerName?: null | string; + deliveryType: number; + itemsSummary?: null | string; + newStatus: number; + occurredAt: string; + oldStatus: number; + orderId: number; + orderNo: string; + paidAmount: number; +} + +/** SignalR 催单推送载荷。 */ +export interface OrderUrgedPayload { + occurredAt: string; + orderId: number; + orderNo: string; + urgeCount: number; +} + +/** 看板页面状态。 */ +export interface OrderBoardPageState { + channel: BoardChannelFilter; + completedCards: OrderBoardCard[]; + deliveringCards: OrderBoardCard[]; + isLoading: boolean; + isSoundEnabled: boolean; + makingCards: OrderBoardCard[]; + pendingCards: OrderBoardCard[]; + selectedStoreId: string; +}