dev #1
@ -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<ApiResponse<SupplierProfile>>(
|
||||
`/v1/suppliers/${encodeURIComponent(name)}`,
|
||||
);
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
const cache = new Map<string, { data: SupplierProfile; at: number }>();
|
||||
const pending = new Map<string, Promise<SupplierProfile>>();
|
||||
|
||||
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<ApiResponse<SupplierProfile>>(
|
||||
`/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();
|
||||
}
|
||||
|
||||
@ -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<ApiResponse<SupplierListData>>("/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<SupplierListData> | 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<ApiResponse<SupplierListData>>(
|
||||
"/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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
<b>×{{ data.frequencyCoefficient?.toFixed(2) ?? "1.00" }}</b
|
||||
><span>频次系数</span>
|
||||
</div>
|
||||
<div v-if="data.supplierScore != null">
|
||||
<b>{{ Math.round(data.supplierScore) }} 分</b
|
||||
><span>供应商得分 S供</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-line">
|
||||
<span>供应商层级</span><strong>{{ data.tier }}</strong>
|
||||
|
||||
@ -20,6 +20,22 @@ const visibleNavigation = computed(() =>
|
||||
(item) => item !== "管理" || auth.hasPermission("risk_item.review"),
|
||||
),
|
||||
);
|
||||
|
||||
const routeChunks: Record<string, () => Promise<unknown>> = {
|
||||
看板: () => import("@/pages/OverviewPage.vue"),
|
||||
供应商: () => import("@/pages/SupplierProfilePage.vue"),
|
||||
标准库: () => import("@/pages/StandardsPage.vue"),
|
||||
报告: () => import("@/pages/ReportPage.vue"),
|
||||
管理: () => import("@/pages/ReviewPage.vue"),
|
||||
};
|
||||
const prefetched = new Set<string>();
|
||||
function prefetch(item: string) {
|
||||
if (prefetched.has(item)) return;
|
||||
const loader = routeChunks[item];
|
||||
if (!loader) return;
|
||||
prefetched.add(item);
|
||||
void loader();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -52,6 +68,8 @@ const visibleNavigation = computed(() =>
|
||||
(item === '管理' && route.name === 'review') ||
|
||||
(item === '对话' && route.name === 'assessment'),
|
||||
}"
|
||||
@mouseenter="prefetch(item)"
|
||||
@focus="prefetch(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</RouterLink>
|
||||
|
||||
@ -53,7 +53,23 @@ function canReview() {
|
||||
return auth.hasPermission("risk_item.review");
|
||||
}
|
||||
|
||||
onMounted(() => conversationStore.loadList());
|
||||
onMounted(() => {
|
||||
// Keep the first non-chat navigation fast by warming the small route chunks
|
||||
// after the browser has finished the initial render. This does not block the
|
||||
// chat page and makes subsequent clicks resolve from the module cache.
|
||||
const warmRoutes = () => {
|
||||
void import("@/pages/OverviewPage.vue");
|
||||
void import("@/pages/SupplierProfilePage.vue");
|
||||
void import("@/pages/StandardsPage.vue");
|
||||
void import("@/pages/ReportPage.vue");
|
||||
void import("@/pages/ReviewPage.vue");
|
||||
void import("@/pages/RiskItemEntryPage.vue");
|
||||
};
|
||||
const idle = window.requestIdleCallback;
|
||||
if (idle) idle(warmRoutes, { timeout: 1500 });
|
||||
else window.setTimeout(warmRoutes, 500);
|
||||
void conversationStore.loadList();
|
||||
});
|
||||
|
||||
function startNewChat() {
|
||||
conversationStore.reset();
|
||||
|
||||
@ -17,7 +17,11 @@ onMounted(() => appStore.refreshHealth());
|
||||
<div class="app-shell__body">
|
||||
<AppSidebar />
|
||||
<main class="app-content">
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive :max="8">
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,31 @@
|
||||
import { fetchDashboard } from "@/api/dashboard";
|
||||
import type { DashboardData, DashboardYear } from "@/types/dashboard";
|
||||
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
const cache = new Map<DashboardYear, { data: DashboardData; at: number }>();
|
||||
const pending = new Map<DashboardYear, Promise<DashboardData>>();
|
||||
|
||||
export async function getDashboard(
|
||||
year: DashboardYear,
|
||||
): Promise<DashboardData> {
|
||||
const response = await fetchDashboard(year);
|
||||
if (!response.data.data) throw new Error("Dashboard response is empty");
|
||||
return response.data.data;
|
||||
const cached = cache.get(year);
|
||||
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data;
|
||||
const running = pending.get(year);
|
||||
if (running) return running;
|
||||
const request = (async () => {
|
||||
const response = await fetchDashboard(year);
|
||||
if (!response.data.data) throw new Error("Dashboard response is empty");
|
||||
cache.set(year, { data: response.data.data, at: Date.now() });
|
||||
return response.data.data;
|
||||
})();
|
||||
pending.set(year, request);
|
||||
try {
|
||||
return await request;
|
||||
} finally {
|
||||
pending.delete(year);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDashboardCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
@ -11,6 +11,10 @@ import type {
|
||||
StandardRuleUpdate,
|
||||
} from "@/types/standards";
|
||||
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
const cache = new Map<string, { data: StandardRuleListData; at: number }>();
|
||||
const pending = new Map<string, Promise<StandardRuleListData>>();
|
||||
|
||||
export async function getStandards(params: {
|
||||
keyword?: string;
|
||||
category?: string;
|
||||
@ -18,9 +22,27 @@ export async function getStandards(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<StandardRuleListData> {
|
||||
const response = await fetchStandards(params);
|
||||
if (!response.data.data) throw new Error("标准库响应为空");
|
||||
return response.data.data;
|
||||
const key = JSON.stringify(params);
|
||||
const cached = cache.get(key);
|
||||
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data;
|
||||
const running = pending.get(key);
|
||||
if (running) return running;
|
||||
const request = (async () => {
|
||||
const response = await fetchStandards(params);
|
||||
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 await request;
|
||||
} finally {
|
||||
pending.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStandardsCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export async function getStandard(ruleId: number): Promise<StandardRuleDetail> {
|
||||
@ -34,6 +56,7 @@ export async function addStandard(
|
||||
): Promise<StandardRuleDetail> {
|
||||
const response = await createStandard(payload);
|
||||
if (!response.data.data) throw new Error("新增标准响应为空");
|
||||
clearStandardsCache();
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@ -43,5 +66,6 @@ export async function editStandard(
|
||||
): Promise<StandardRuleDetail> {
|
||||
const response = await updateStandard(ruleId, payload);
|
||||
if (!response.data.data) throw new Error("修改标准响应为空");
|
||||
clearStandardsCache();
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@ -19,18 +19,37 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
const sending = ref(false);
|
||||
const loadingOlder = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
let listRequest: Promise<void> | null = null;
|
||||
let listRequestSearch: string | undefined;
|
||||
let listLoadedAt = 0;
|
||||
|
||||
async function loadList(search?: string) {
|
||||
const normalizedSearch = search?.trim() || undefined;
|
||||
if (listRequest && listRequestSearch === normalizedSearch)
|
||||
return listRequest;
|
||||
if (
|
||||
!normalizedSearch &&
|
||||
items.value.length &&
|
||||
Date.now() - listLoadedAt < 30_000
|
||||
)
|
||||
return;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await fetchConversations(50, search);
|
||||
items.value = response.data.data ?? [];
|
||||
} catch {
|
||||
error.value = "历史对话加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
listRequestSearch = normalizedSearch;
|
||||
listRequest = (async () => {
|
||||
try {
|
||||
const response = await fetchConversations(50, normalizedSearch);
|
||||
items.value = response.data.data ?? [];
|
||||
if (!normalizedSearch) listLoadedAt = Date.now();
|
||||
} catch {
|
||||
error.value = "历史对话加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
listRequest = null;
|
||||
listRequestSearch = undefined;
|
||||
}
|
||||
})();
|
||||
return listRequest;
|
||||
}
|
||||
|
||||
async function open(id: number) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export interface SupplierCategoryBreakdown {
|
||||
category: string;
|
||||
isUnclassified: boolean;
|
||||
anomalyCount: number;
|
||||
anomalyRate: number | null;
|
||||
topMaterial: string | null;
|
||||
@ -23,6 +24,8 @@ export interface SupplierProfile {
|
||||
anomalyTotal: number;
|
||||
anomalyRate: number | null;
|
||||
frequencyAlpha: number;
|
||||
supplierBaseScore: number | null;
|
||||
supplierScore: number | null;
|
||||
riskScore: number | null;
|
||||
mainCategoryCount: number;
|
||||
cooperationCountBasis: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user