完善供应商频次卡片展示
- 展示近3年异常次数和合作频次代理值 - 增加频次口径和无供应商数据提示 - 兼容旧历史风险卡片数据 - 保留历史会话加载超时和流式响应处理优化
This commit is contained in:
parent
f0a940eb10
commit
ab20424aa9
@ -12,6 +12,7 @@ export interface ConversationStreamEvent {
|
|||||||
export function fetchConversations(limit = 50, search?: string) {
|
export function fetchConversations(limit = 50, search?: string) {
|
||||||
return http.get<ApiResponse<ConversationSummary[]>>("/v1/conversations", {
|
return http.get<ApiResponse<ConversationSummary[]>>("/v1/conversations", {
|
||||||
params: { limit, search },
|
params: { limit, search },
|
||||||
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ export function createConversation(title?: string) {
|
|||||||
export function fetchConversation(id: number, offset = 0) {
|
export function fetchConversation(id: number, offset = 0) {
|
||||||
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
|
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
|
||||||
params: { limit: 20, offset },
|
params: { limit: 20, offset },
|
||||||
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +47,8 @@ export async function streamConversationMessage(
|
|||||||
messageType: "text" | "image" | "file" | "audio" = "text",
|
messageType: "text" | "image" | "file" | "audio" = "text",
|
||||||
) {
|
) {
|
||||||
const token = localStorage.getItem("risk_prediction_token");
|
const token = localStorage.getItem("risk_prediction_token");
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = window.setTimeout(() => controller.abort(), 180_000);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${import.meta.env.VITE_API_BASE_URL || "/api"}/v1/conversations/${id}/stream`,
|
`${import.meta.env.VITE_API_BASE_URL || "/api"}/v1/conversations/${id}/stream`,
|
||||||
{
|
{
|
||||||
@ -55,6 +59,7 @@ export async function streamConversationMessage(
|
|||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ content, messageType }),
|
body: JSON.stringify({ content, messageType }),
|
||||||
|
signal: controller.signal,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@ -64,32 +69,51 @@ export async function streamConversationMessage(
|
|||||||
if (!response.ok || !response.body)
|
if (!response.ok || !response.body)
|
||||||
throw new Error(`流式请求失败(HTTP ${response.status})`);
|
throw new Error(`流式请求失败(HTTP ${response.status})`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
try {
|
||||||
const decoder = new TextDecoder();
|
const reader = response.body.getReader();
|
||||||
let buffer = "";
|
const decoder = new TextDecoder();
|
||||||
let completed = false;
|
let buffer = "";
|
||||||
while (true) {
|
let completed = false;
|
||||||
const { value, done } = await reader.read();
|
while (true) {
|
||||||
if (done) break;
|
const { value, done } = await reader.read();
|
||||||
buffer += decoder.decode(value, { stream: true });
|
if (done) break;
|
||||||
const lines = buffer.split("\n");
|
buffer += decoder.decode(value, { stream: true });
|
||||||
buffer = lines.pop() ?? "";
|
const lines = buffer.split("\n");
|
||||||
for (const line of lines) {
|
buffer = lines.pop() ?? "";
|
||||||
if (!line.startsWith("data:")) continue;
|
for (const line of lines) {
|
||||||
const raw = line.slice(5).trim();
|
if (!line.startsWith("data:")) continue;
|
||||||
if (!raw) continue;
|
const raw = line.slice(5).trim();
|
||||||
const event = JSON.parse(raw) as ConversationStreamEvent;
|
if (!raw) continue;
|
||||||
onEvent(event);
|
let event: ConversationStreamEvent;
|
||||||
if (event.type === "done") completed = true;
|
try {
|
||||||
|
event = JSON.parse(raw) as ConversationStreamEvent;
|
||||||
|
} catch {
|
||||||
|
throw new Error("AI 流式响应格式错误,请重试");
|
||||||
|
}
|
||||||
|
onEvent(event);
|
||||||
|
if (event.type === "done") completed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (buffer.startsWith("data:")) {
|
||||||
if (buffer.startsWith("data:")) {
|
const raw = buffer.slice(5).trim();
|
||||||
const raw = buffer.slice(5).trim();
|
if (raw) {
|
||||||
if (raw) {
|
let event: ConversationStreamEvent;
|
||||||
const event = JSON.parse(raw) as ConversationStreamEvent;
|
try {
|
||||||
onEvent(event);
|
event = JSON.parse(raw) as ConversationStreamEvent;
|
||||||
if (event.type === "done") completed = true;
|
} catch {
|
||||||
|
throw new Error("AI 流式响应格式错误,请重试");
|
||||||
|
}
|
||||||
|
onEvent(event);
|
||||||
|
if (event.type === "done") completed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!completed) throw new Error("AI 流式连接中断,请重试");
|
||||||
|
} catch (reason) {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
throw new Error("AI 响应超时,请稍后重试", { cause: reason });
|
||||||
|
}
|
||||||
|
throw reason;
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
if (!completed) throw new Error("AI 流式连接中断,请重试");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ interface CardData extends AssistantResponseCard {
|
|||||||
anomalyCount?: number;
|
anomalyCount?: number;
|
||||||
cooperationCount?: number | null;
|
cooperationCount?: number | null;
|
||||||
frequencyCoefficient?: number;
|
frequencyCoefficient?: number;
|
||||||
|
frequencyBasis?: string;
|
||||||
|
dataAvailable?: boolean;
|
||||||
tier?: string;
|
tier?: string;
|
||||||
topProblems?: Array<{ name: string; count: number }>;
|
topProblems?: Array<{ name: string; count: number }>;
|
||||||
frequentProblems?: Array<{ name: string; count: number }>;
|
frequentProblems?: Array<{ name: string; count: number }>;
|
||||||
@ -94,14 +96,14 @@ function formatCount(value: unknown) {
|
|||||||
<div class="metric-grid">
|
<div class="metric-grid">
|
||||||
<div>
|
<div>
|
||||||
<b>{{ formatCount(data.anomalyCount) }}</b
|
<b>{{ formatCount(data.anomalyCount) }}</b
|
||||||
><span>历史异常</span>
|
><span>近3年异常</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<b>{{ formatCount(data.cooperationCount) }}</b
|
<b>{{ formatCount(data.cooperationCount) }}</b
|
||||||
><span>合作次数</span>
|
><span>合作频次(代理)</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<b>×{{ data.frequencyCoefficient }}</b
|
<b>×{{ data.frequencyCoefficient?.toFixed(2) ?? "1.00" }}</b
|
||||||
><span>频次系数</span>
|
><span>频次系数</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -141,26 +141,58 @@ const stars = computed(() => {
|
|||||||
|
|
||||||
<section class="supplier-analysis">
|
<section class="supplier-analysis">
|
||||||
<h3><Factory :size="14" /> 供应商合作频次分析</h3>
|
<h3><Factory :size="14" /> 供应商合作频次分析</h3>
|
||||||
<div v-if="result.supplierFreq?.supplierName">
|
<template
|
||||||
<span>{{ result.supplierFreq.supplierName }}</span>
|
v-if="
|
||||||
<strong>异常 {{ result.supplierFreq.anomalyCount ?? 0 }} 次</strong>
|
result.supplierFreq?.dataAvailable ||
|
||||||
</div>
|
result.supplierFreq?.supplierName
|
||||||
<div v-if="result.supplierFreq">
|
"
|
||||||
<span>频次风险系数</span
|
>
|
||||||
><strong>×{{ result.supplierFreq.alpha.toFixed(2) }}</strong>
|
<div v-if="result.supplierFreq.supplierName">
|
||||||
</div>
|
<span>{{ result.supplierFreq.supplierName }}</span>
|
||||||
<div v-if="result.supplierFreq?.tier">
|
<strong
|
||||||
<span>供应商层级</span><strong>{{ result.supplierFreq.tier }}</strong>
|
>近3年异常 {{ result.supplierFreq.anomalyCount ?? 0 }} 次</strong
|
||||||
</div>
|
>
|
||||||
<div>
|
</div>
|
||||||
<span>合作次数</span
|
<div v-if="result.supplierFreq.alpha != null">
|
||||||
><strong>{{
|
<span>频次风险系数</span
|
||||||
result.supplierFreq?.cooperationCount ?? "暂无数据"
|
><strong>×{{ result.supplierFreq.alpha.toFixed(2) }}</strong>
|
||||||
}}</strong>
|
</div>
|
||||||
</div>
|
<div v-if="result.supplierFreq.tier">
|
||||||
<div v-if="result.supplierFreq?.suggestion">
|
<span>供应商层级</span
|
||||||
<span>建议</span>{{ result.supplierFreq.suggestion }}
|
><strong>{{ result.supplierFreq.tier }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>合作频次(代理)</span>
|
||||||
|
<strong
|
||||||
|
>{{
|
||||||
|
result.supplierFreq.cooperationCount ??
|
||||||
|
result.supplierFreq.anomalyCount ??
|
||||||
|
0
|
||||||
|
}}
|
||||||
|
次</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="result.supplierFreq.suggestion">
|
||||||
|
<span>管理建议</span>{{ result.supplierFreq.suggestion }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="supplier-analysis__empty">
|
||||||
|
{{
|
||||||
|
result.supplierFreq?.note || "未指定供应商,暂无供应商频次分析。"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<small
|
||||||
|
v-if="
|
||||||
|
result.supplierFreq?.dataAvailable ||
|
||||||
|
result.supplierFreq?.supplierName
|
||||||
|
"
|
||||||
|
class="supplier-analysis__note"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
result.supplierFreq.note ||
|
||||||
|
"当前以近3年异常记录数作为合作频次代理,不代表真实采购订单合作次数。"
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="standards-card">
|
<section class="standards-card">
|
||||||
@ -507,6 +539,15 @@ h3 {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.supplier-analysis__empty,
|
||||||
|
.supplier-analysis__note {
|
||||||
|
display: block;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #6d5a91;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.standards-card {
|
.standards-card {
|
||||||
border: 1px solid #bfdbfe;
|
border: 1px solid #bfdbfe;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,13 @@ function startNewChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openConversation(id: number) {
|
async function openConversation(id: number) {
|
||||||
|
if (conversationStore.current?.id === id) {
|
||||||
|
await router.push({
|
||||||
|
name: "assessment",
|
||||||
|
query: { conversation: String(id) },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await conversationStore.open(id);
|
await conversationStore.open(id);
|
||||||
await router.push({
|
await router.push({
|
||||||
|
|||||||
@ -60,9 +60,12 @@ export interface RiskAssessmentResult {
|
|||||||
supplierName: string | null;
|
supplierName: string | null;
|
||||||
cooperationCount: number | null;
|
cooperationCount: number | null;
|
||||||
anomalyCount: number | null;
|
anomalyCount: number | null;
|
||||||
alpha: number;
|
alpha: number | null;
|
||||||
tier: string | null;
|
tier: string | null;
|
||||||
suggestion: string | null;
|
suggestion: string | null;
|
||||||
|
frequencyBasis?: string;
|
||||||
|
note?: string | null;
|
||||||
|
dataAvailable?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
standardJudgment?: Array<{
|
standardJudgment?: Array<{
|
||||||
category: string;
|
category: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user