feat: 添加 SignalR 客户端与订单大厅 API 模块
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 1m59s

- 安装 @microsoft/signalr 依赖
- 新增 useSignalR Hook(自动重连 + 断线补偿回调)
- 新增订单大厅 API 模块(board/stats/pending-since + 接单/拒单/出餐/确认)
This commit is contained in:
2026-02-27 13:12:04 +08:00
parent 2e510a8fa7
commit 42bf54a52c
4 changed files with 315 additions and 0 deletions

View File

@@ -26,6 +26,7 @@
"#/*": "./src/*"
},
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",

View File

@@ -0,0 +1,100 @@
/**
* 文件职责:订单大厅(看板)接口与类型定义。
*/
import { requestClient } from '#/api/request';
/** 订单看板卡片。 */
export interface OrderBoardCard {
acceptedAt?: null | string;
channel: number;
createdAt: string;
customerName?: null | string;
customerPhone?: null | string;
deliveryType: number;
id: string;
isUrged: boolean;
itemsSummary?: null | string;
orderNo: string;
paidAmount: number;
queueNumber?: null | string;
readyAt?: null | string;
status: number;
storeId: string;
tableNo?: null | string;
urgeCount: number;
}
/** 订单看板结果(四列)。 */
export interface OrderBoardResult {
completed: OrderBoardCard[];
delivering: OrderBoardCard[];
making: OrderBoardCard[];
pending: OrderBoardCard[];
}
/** 订单看板统计。 */
export interface OrderBoardStats {
completedCount: number;
deliveringCount: number;
makingCount: number;
pendingCount: number;
todayTotal: number;
}
/** 获取完整看板数据。 */
export async function getOrderBoardApi(params: {
channel?: string;
storeId: string;
}) {
return requestClient.get<OrderBoardResult>('/order-board/board', { params });
}
/** 获取看板统计。 */
export async function getOrderBoardStatsApi(params: { storeId: string }) {
return requestClient.get<OrderBoardStats>('/order-board/stats', { params });
}
/** 重连补偿拉取。 */
export async function getPendingSinceApi(params: {
since: string;
storeId: string;
}) {
return requestClient.get<OrderBoardCard[]>('/order-board/pending-since', {
params,
});
}
/** 接单。 */
export async function acceptOrderApi(orderId: string) {
return requestClient.post<OrderBoardCard>(
`/order-board/${orderId}/accept`,
{},
);
}
/** 拒单。 */
export async function rejectOrderApi(
orderId: string,
data: { reason: string },
) {
return requestClient.post<OrderBoardCard>(
`/order-board/${orderId}/reject`,
data,
);
}
/** 出餐完成。 */
export async function completePreparationApi(orderId: string) {
return requestClient.post<OrderBoardCard>(
`/order-board/${orderId}/complete-preparation`,
{},
);
}
/** 确认送达/取餐。 */
export async function confirmDeliveryApi(orderId: string) {
return requestClient.post<OrderBoardCard>(
`/order-board/${orderId}/confirm-delivery`,
{},
);
}

View File

@@ -0,0 +1,121 @@
/**
* 文件职责SignalR 连接管理 Hook提供自动重连、事件订阅、断线补偿能力。
*/
import type { HubConnection } from '@microsoft/signalr';
import { ref } from 'vue';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
import {
HubConnectionBuilder,
HubConnectionState,
LogLevel,
} from '@microsoft/signalr';
/** SignalR Hook 配置。 */
export interface UseSignalROptions {
/** 重连后的补偿回调(传入断线时间戳)。 */
onReconnected?: (lastDisconnectedAt: Date) => void;
/** 连接关闭回调。 */
onClose?: (error?: Error) => void;
}
/** SignalR 连接管理 Hook。 */
export function useSignalR(options?: UseSignalROptions) {
const isConnected = ref(false);
let connection: HubConnection | null = null;
let lastDisconnectedAt: Date | null = null;
// 1. 构建 Hub URL从 apiURL 去掉 /api/tenant/v1 后缀)
function buildHubUrl(): string {
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const base = apiURL.replace(/\/api\/tenant\/v\d+\/?$/, '');
return `${base}/hubs/order-board`;
}
// 2. 获取 JWT Token
function getAccessToken(): string {
const accessStore = useAccessStore();
return accessStore.accessToken || '';
}
// 3. 建立连接
async function connect(storeId?: string): Promise<void> {
if (connection?.state === HubConnectionState.Connected) {
return;
}
const hubUrl = storeId
? `${buildHubUrl()}?storeId=${storeId}`
: buildHubUrl();
connection = new HubConnectionBuilder()
.withUrl(hubUrl, {
accessTokenFactory: () => getAccessToken(),
})
.withAutomaticReconnect([0, 2000, 5000, 10_000, 30_000])
.configureLogging(LogLevel.Warning)
.build();
// 4. 重连成功回调
connection.onreconnecting(() => {
isConnected.value = false;
lastDisconnectedAt = new Date();
});
connection.onreconnected(() => {
isConnected.value = true;
if (lastDisconnectedAt && options?.onReconnected) {
options.onReconnected(lastDisconnectedAt);
}
lastDisconnectedAt = null;
});
// 5. 连接关闭回调
connection.onclose((error) => {
isConnected.value = false;
lastDisconnectedAt = new Date();
options?.onClose?.(error ?? undefined);
});
await connection.start();
isConnected.value = true;
}
// 6. 断开连接
async function disconnect(): Promise<void> {
if (connection) {
await connection.stop();
connection = null;
isConnected.value = false;
}
}
// 7. 订阅事件
function on<T = unknown>(event: string, callback: (data: T) => void): void {
connection?.on(event, callback);
}
// 8. 取消订阅
function off(event: string): void {
connection?.off(event);
}
// 9. 调用 Hub 方法
async function invoke(method: string, ...args: unknown[]): Promise<void> {
if (connection?.state === HubConnectionState.Connected) {
await connection.invoke(method, ...args);
}
}
return {
isConnected,
connect,
disconnect,
on,
off,
invoke,
};
}