feat: 重构订单大厅看板UI对齐设计稿
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 51s
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 51s
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user