dieya_risk_prediction_frontend/src/stores/conversation.store.ts
kx_guo 74a1c9d4bc 统一供应商画像展示并优化页面加载速度
供应商画像直接展示后端统一计算的基础分、频次风险系数和供应商得分;对齐对话卡片数据;未归类品类固定灰色展示并保留待补标记录;隐藏无推荐数据的代工厂模块;增加页面缓存、资源预加载、接口缓存和重复请求去重。
2026-09-09 10:46:14 +08:00

283 lines
8.6 KiB
TypeScript

import { defineStore } from "pinia";
import { ref } from "vue";
import {
createConversation,
fetchConversation,
fetchConversations,
streamConversationMessage,
sendConversationMessage,
} from "@/api/conversations";
import type { Conversation, ConversationSummary } from "@/types/conversation";
type ConversationMessageType = "text" | "image" | "file" | "audio";
export const useConversationStore = defineStore("conversation", () => {
const items = ref<ConversationSummary[]>([]);
const current = ref<Conversation | null>(null);
const loading = ref(false);
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;
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) {
loading.value = true;
error.value = null;
try {
const response = await fetchConversation(id);
if (!response.data.data) throw new Error("对话响应为空");
current.value = response.data.data;
} catch (reason) {
error.value = reason instanceof Error ? reason.message : "对话加载失败";
throw reason;
} finally {
loading.value = false;
}
}
async function loadOlder() {
if (
loadingOlder.value ||
!current.value ||
!current.value.messages.length ||
!current.value.messagesTotal
) {
return false;
}
const offset = current.value.messagesOffset + current.value.messages.length;
if (!current.value.hasMoreMessages) return false;
loadingOlder.value = true;
try {
const response = await fetchConversation(current.value.id, offset);
if (!response.data.data) throw new Error("历史消息响应为空");
const older = response.data.data;
current.value = {
...current.value,
messages: [...older.messages, ...current.value.messages],
messagesOffset: 0,
hasMoreMessages: older.hasMoreMessages,
messagesTotal: older.messagesTotal,
};
return true;
} finally {
loadingOlder.value = false;
}
}
async function create(title?: string) {
const response = await createConversation(title);
if (!response.data.data) throw new Error("新建对话响应为空");
current.value = response.data.data;
items.value = [toSummary(response.data.data), ...items.value];
return current.value;
}
async function send(
content: string,
messageType: ConversationMessageType = "text",
) {
if (!current.value) await create();
if (!current.value) throw new Error("当前对话不存在");
sending.value = true;
error.value = null;
try {
const response = await sendConversationMessage(
current.value.id,
content,
messageType,
);
if (!response.data.data) throw new Error("消息响应为空");
current.value = response.data.data;
const index = items.value.findIndex(
(item) => item.id === current.value?.id,
);
const summary = toSummary(current.value);
if (index >= 0) items.value.splice(index, 1, summary);
else items.value.unshift(summary);
items.value.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
return current.value;
} catch (reason) {
error.value = reason instanceof Error ? reason.message : "消息发送失败";
throw reason;
} finally {
sending.value = false;
}
}
async function streamSend(
content: string,
onDelta: (delta: string) => void,
messageType: ConversationMessageType = "text",
) {
if (!current.value) await create();
if (!current.value) throw new Error("当前对话不存在");
sending.value = true;
error.value = null;
const startedAt = new Date().toISOString();
const userMessage = {
id: -Date.now(),
role: "user" as const,
messageType,
content,
riskResult: null,
responseCard: null,
semantic: null,
createdAt: startedAt,
};
const assistantMessage = {
id: userMessage.id - 1,
role: "assistant" as const,
messageType: "text" as const,
content: "",
riskResult: null,
responseCard: null,
semantic: null as Record<string, unknown> | null,
createdAt: startedAt,
};
current.value = {
...current.value,
messages: [...current.value.messages, userMessage, assistantMessage],
messageCount: current.value.messageCount + 2,
messagesTotal: current.value.messagesTotal + 2,
};
try {
const conversationId = current.value.id;
await streamConversationMessage(
conversationId,
content,
(event) => {
if (event.type === "delta" && event.content) onDelta(event.content);
if (event.type === "semantic" && event.data && current.value) {
const messages = [...current.value.messages];
const last = messages[messages.length - 1];
if (last?.role === "assistant") {
messages[messages.length - 1] = {
...last,
semantic: event.data as Record<string, unknown>,
};
current.value = { ...current.value, messages };
}
}
if (event.type === "delta" && event.content && current.value) {
const messages = [...current.value.messages];
const last = messages[messages.length - 1];
if (last?.role === "assistant") {
messages[messages.length - 1] = {
...last,
content: last.content + event.content,
};
current.value = { ...current.value, messages };
}
}
if (event.type === "error")
throw new Error(event.message || "AI 回复失败");
if (event.type === "done" && event.data)
current.value = event.data as Conversation;
},
messageType,
);
if (
!current.value ||
!current.value.messages.some(
(message) => message.role === "assistant" && message.content.trim(),
)
) {
throw new Error("AI 回复为空");
}
const summary = toSummary(current.value);
const index = items.value.findIndex((item) => item.id === summary.id);
if (index >= 0) items.value.splice(index, 1, summary);
else items.value.unshift(summary);
return current.value;
} catch (reason) {
const conversationId = current.value?.id;
if (current.value) {
const messages = current.value.messages.filter(
(message) => message.id !== assistantMessage.id,
);
current.value = {
...current.value,
messages,
messageCount: Math.max(0, current.value.messageCount - 1),
messagesTotal: Math.max(0, current.value.messagesTotal - 1),
};
}
if (conversationId) {
try {
const response = await fetchConversation(conversationId);
if (response.data.data) current.value = response.data.data;
} catch {
// Keep the local state when recovery is unavailable.
}
}
error.value = reason instanceof Error ? reason.message : "AI 回复失败";
throw reason;
} finally {
sending.value = false;
}
}
function reset() {
current.value = null;
error.value = null;
}
function toSummary(conversation: Conversation): ConversationSummary {
return {
id: conversation.id,
title: conversation.title,
messageCount: conversation.messageCount,
lastRiskLevel: conversation.lastRiskLevel,
updatedAt: conversation.updatedAt,
};
}
return {
items,
current,
loading,
sending,
loadingOlder,
error,
loadList,
open,
loadOlder,
create,
send,
streamSend,
reset,
};
});