From 74a1c9d4bc000db58c7db4a42e6efcdfb7b34909 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Wed, 9 Sep 2026 10:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E5=B1=95=E7=A4=BA=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 供应商画像直接展示后端统一计算的基础分、频次风险系数和供应商得分;对齐对话卡片数据;未归类品类固定灰色展示并保留待补标记录;隐藏无推荐数据的代工厂模块;增加页面缓存、资源预加载、接口缓存和重复请求去重。 --- src/api/supplierProfile.ts | 37 +- src/api/suppliers.ts | 45 +- .../assistant/AssistantResponseCard.vue | 6 + src/components/layout/AppHeader.vue | 18 + src/components/layout/AppSidebar.vue | 18 +- src/layouts/MainLayout.vue | 6 +- src/pages/SupplierProfilePage.vue | 892 +++++++++++------- src/services/dashboard.service.ts | 27 +- src/services/standards.service.ts | 30 +- src/stores/conversation.store.ts | 35 +- src/types/supplierProfile.ts | 3 + 11 files changed, 773 insertions(+), 344 deletions(-) diff --git a/src/api/supplierProfile.ts b/src/api/supplierProfile.ts index b367ea6..cb255e6 100644 --- a/src/api/supplierProfile.ts +++ b/src/api/supplierProfile.ts @@ -2,8 +2,37 @@ import { http } from "@/api/http"; import type { ApiResponse } from "@/types/api"; import type { SupplierProfile } from "@/types/supplierProfile"; -export function fetchSupplierProfile(name: string) { - return http.get>( - `/v1/suppliers/${encodeURIComponent(name)}`, - ); +const CACHE_TTL_MS = 30_000; +const cache = new Map(); +const pending = new Map>(); + +export async function fetchSupplierProfile( + name: string, +): Promise<{ data: { data: SupplierProfile | null } }> { + const key = name.trim(); + const cached = cache.get(key); + if (cached && Date.now() - cached.at < CACHE_TTL_MS) { + return { data: { data: cached.data } }; + } + const running = pending.get(key); + if (running) return { data: { data: await running } }; + const request = (async () => { + const response = await http.get>( + `/v1/suppliers/${encodeURIComponent(key)}`, + ); + if (!response.data.data) throw new Error("供应商画像响应为空"); + cache.set(key, { data: response.data.data, at: Date.now() }); + return response.data.data; + })(); + pending.set(key, request); + try { + return { data: { data: await request } }; + } finally { + pending.delete(key); + } +} + +export function clearSupplierProfileCache(name?: string) { + if (name) cache.delete(name.trim()); + else cache.clear(); } diff --git a/src/api/suppliers.ts b/src/api/suppliers.ts index 5d707a5..1fe4f44 100644 --- a/src/api/suppliers.ts +++ b/src/api/suppliers.ts @@ -1,9 +1,44 @@ import { http } from "@/api/http"; import type { ApiResponse } from "@/types/api"; -export interface SupplierOption { name: string; anomalyCount: number; tier: string } -export interface SupplierListData { items: SupplierOption[]; defaultSupplier: string | null } - -export function fetchSupplierOptions(keyword?: string) { - return http.get>("/v1/suppliers", { params: { keyword, limit: 8 } }); +export interface SupplierOption { + name: string; + anomalyCount: number; + tier: string; +} +export interface SupplierListData { + items: SupplierOption[]; + defaultSupplier: string | null; +} + +const CACHE_TTL_MS = 30_000; +let cached: { data: SupplierListData; at: number } | null = null; +let pending: Promise | null = null; + +export async function fetchSupplierOptions( + keyword?: string, +): Promise<{ data: { data: SupplierListData } }> { + const normalized = keyword?.trim() || ""; + if (!normalized && cached && Date.now() - cached.at < CACHE_TTL_MS) + return { data: { data: cached.data } }; + if (!normalized && pending) return { data: { data: await pending } }; + const request = (async () => { + const response = await http.get>( + "/v1/suppliers", + { params: { keyword: normalized || undefined, limit: 8 } }, + ); + if (!response.data.data) throw new Error("供应商列表响应为空"); + if (!normalized) cached = { data: response.data.data, at: Date.now() }; + return response.data.data; + })(); + if (!normalized) pending = request; + try { + return { data: { data: await request } }; + } finally { + if (!normalized) pending = null; + } +} + +export function clearSupplierOptionsCache() { + cached = null; } diff --git a/src/components/assistant/AssistantResponseCard.vue b/src/components/assistant/AssistantResponseCard.vue index e1be85c..1e2ffa7 100644 --- a/src/components/assistant/AssistantResponseCard.vue +++ b/src/components/assistant/AssistantResponseCard.vue @@ -26,6 +26,8 @@ interface CardData extends AssistantResponseCard { anomalyCount?: number; cooperationCount?: number | null; frequencyCoefficient?: number; + supplierBaseScore?: number | null; + supplierScore?: number | null; frequencyBasis?: string; dataAvailable?: boolean; tier?: string; @@ -119,6 +121,10 @@ function formatCount(value: unknown) { ×{{ data.frequencyCoefficient?.toFixed(2) ?? "1.00" }}频次系数 +
+ {{ Math.round(data.supplierScore) }} 分供应商得分 S供 +
供应商层级{{ data.tier }} diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index ed1aac2..09d1c6a 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -20,6 +20,22 @@ const visibleNavigation = computed(() => (item) => item !== "管理" || auth.hasPermission("risk_item.review"), ), ); + +const routeChunks: Record Promise> = { + 看板: () => import("@/pages/OverviewPage.vue"), + 供应商: () => import("@/pages/SupplierProfilePage.vue"), + 标准库: () => import("@/pages/StandardsPage.vue"), + 报告: () => import("@/pages/ReportPage.vue"), + 管理: () => import("@/pages/ReviewPage.vue"), +}; +const prefetched = new Set(); +function prefetch(item: string) { + if (prefetched.has(item)) return; + const loader = routeChunks[item]; + if (!loader) return; + prefetched.add(item); + void loader(); +}