From 42bf54a52cf3406f17b077af6016ad0cece2bffd Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 27 Feb 2026 13:12:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20SignalR=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E4=B8=8E=E8=AE=A2=E5=8D=95=E5=A4=A7?= =?UTF-8?q?=E5=8E=85=20API=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 安装 @microsoft/signalr 依赖 - 新增 useSignalR Hook(自动重连 + 断线补偿回调) - 新增订单大厅 API 模块(board/stats/pending-since + 接单/拒单/出餐/确认) --- apps/web-antd/package.json | 1 + apps/web-antd/src/api/order-board/index.ts | 100 +++++++++++++++++ apps/web-antd/src/hooks/useSignalR.ts | 121 +++++++++++++++++++++ pnpm-lock.yaml | 93 ++++++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 apps/web-antd/src/api/order-board/index.ts create mode 100644 apps/web-antd/src/hooks/useSignalR.ts diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 8294eb9..e948a16 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -26,6 +26,7 @@ "#/*": "./src/*" }, "dependencies": { + "@microsoft/signalr": "^8.0.7", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", "@vben/constants": "workspace:*", diff --git a/apps/web-antd/src/api/order-board/index.ts b/apps/web-antd/src/api/order-board/index.ts new file mode 100644 index 0000000..de6c97e --- /dev/null +++ b/apps/web-antd/src/api/order-board/index.ts @@ -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('/order-board/board', { params }); +} + +/** 获取看板统计。 */ +export async function getOrderBoardStatsApi(params: { storeId: string }) { + return requestClient.get('/order-board/stats', { params }); +} + +/** 重连补偿拉取。 */ +export async function getPendingSinceApi(params: { + since: string; + storeId: string; +}) { + return requestClient.get('/order-board/pending-since', { + params, + }); +} + +/** 接单。 */ +export async function acceptOrderApi(orderId: string) { + return requestClient.post( + `/order-board/${orderId}/accept`, + {}, + ); +} + +/** 拒单。 */ +export async function rejectOrderApi( + orderId: string, + data: { reason: string }, +) { + return requestClient.post( + `/order-board/${orderId}/reject`, + data, + ); +} + +/** 出餐完成。 */ +export async function completePreparationApi(orderId: string) { + return requestClient.post( + `/order-board/${orderId}/complete-preparation`, + {}, + ); +} + +/** 确认送达/取餐。 */ +export async function confirmDeliveryApi(orderId: string) { + return requestClient.post( + `/order-board/${orderId}/confirm-delivery`, + {}, + ); +} diff --git a/apps/web-antd/src/hooks/useSignalR.ts b/apps/web-antd/src/hooks/useSignalR.ts new file mode 100644 index 0000000..77a5ed0 --- /dev/null +++ b/apps/web-antd/src/hooks/useSignalR.ts @@ -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 { + 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 { + if (connection) { + await connection.stop(); + connection = null; + isConnected.value = false; + } + } + + // 7. 订阅事件 + function on(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 { + if (connection?.state === HubConnectionState.Connected) { + await connection.invoke(method, ...args); + } + } + + return { + isConnected, + connect, + disconnect, + on, + off, + invoke, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3eef56..2d68a78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -614,6 +614,9 @@ importers: apps/web-antd: dependencies: + '@microsoft/signalr': + specifier: ^8.0.7 + version: 8.0.17 '@vben/access': specifier: workspace:* version: link:../../packages/effects/access @@ -3613,6 +3616,9 @@ packages: resolution: {integrity: sha512-1jlWO4qmgqYoVUcyh+oXYRztZde/pAi7cSVzBz/rc+S7CoVzDasy8QE13dx6sLG4VRo8SfkkLbFORR6tBw4uGQ==} hasBin: true + '@microsoft/signalr@8.0.17': + resolution: {integrity: sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==} + '@microsoft/tsdoc-config@0.18.0': resolution: {integrity: sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==} @@ -6349,6 +6355,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -6425,6 +6435,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -8697,6 +8710,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + publint@0.3.17: resolution: {integrity: sha512-Q3NLegA9XM6usW+dYQRG1g9uEHiYUzcCVBJDJ7yMcWRqVU9LYZUWdqbwMZfmTCFC5PZLQpLAmhvRcQRl3exqkw==} engines: {node: '>=18'} @@ -8726,6 +8742,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8868,6 +8887,9 @@ packages: require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -9065,6 +9087,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -9591,6 +9616,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -9803,6 +9832,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -9915,6 +9948,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -10345,6 +10381,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -12641,6 +12689,18 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/signalr@8.0.17': + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + '@microsoft/tsdoc-config@0.18.0': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -15672,6 +15732,8 @@ snapshots: events@3.3.0: {} + eventsource@2.0.2: {} + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -15752,6 +15814,11 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fetch-cookie@2.2.0: + dependencies: + set-cookie-parser: 2.7.2 + tough-cookie: 4.1.4 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -18034,6 +18101,10 @@ snapshots: prr@1.0.1: optional: true + psl@1.15.0: + dependencies: + punycode: 2.3.1 + publint@0.3.17: dependencies: '@publint/pack': 0.1.3 @@ -18063,6 +18134,8 @@ snapshots: quansync@0.2.11: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} radix3@1.1.2: {} @@ -18236,6 +18309,8 @@ snapshots: require-package-name@2.0.1: {} + requires-port@1.0.0: {} + reserved-identifiers@1.2.0: {} resize-observer-polyfill@1.5.1: {} @@ -18455,6 +18530,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -19066,6 +19143,13 @@ snapshots: totalist@3.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} tr46@1.0.1: @@ -19304,6 +19388,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unplugin-utils@0.3.1: @@ -19411,6 +19497,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} vee-validate@4.15.1(vue@3.5.27(typescript@5.9.3)): @@ -20105,6 +20196,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@7.5.10: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0