From 32739944c74d87ffd742616ead074e33cf02e3c1 Mon Sep 17 00:00:00 2001
From: kx_guo
Date: Fri, 4 Sep 2026 18:02:39 +0800
Subject: [PATCH 01/31] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A3=8E=E9=99=A9?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E4=B8=8E=E5=8E=86=E5=8F=B2=E4=BC=9A=E8=AF=9D?=
=?UTF-8?q?=E5=8A=A0=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 展示数据库七维风险维度、默认值和数据说明\n- 增加标准兜底校验卡片及无历史案例提示\n- 支持 AI 风险建议和案例推理卡片的新数据结构\n- 进入会话默认定位最新内容\n- 上划到顶部自动加载更早的 20 条消息并保持阅读位置\n- 移除手动加载更早消息按钮\n- 更新风险评估表单和前端类型定义
---
.../assistant/AssistantResponseCard.vue | 17 +-
src/components/assistant/RiskAnalysisCard.vue | 26 +-
src/components/risk/RiskAssessmentForm.vue | 315 ++++++++----------
src/components/risk/RiskResultPanel.vue | 49 ++-
src/pages/RiskAssistantPage.vue | 29 +-
src/types/risk.ts | 24 +-
6 files changed, 253 insertions(+), 207 deletions(-)
diff --git a/src/components/assistant/AssistantResponseCard.vue b/src/components/assistant/AssistantResponseCard.vue
index d08bf81..c2a7e14 100644
--- a/src/components/assistant/AssistantResponseCard.vue
+++ b/src/components/assistant/AssistantResponseCard.vue
@@ -8,6 +8,7 @@ import {
CheckCircle2,
ClipboardCheck,
PackageSearch,
+ Ruler,
Sparkles,
} from "@lucide/vue";
import { computed, ref } from "vue";
@@ -196,8 +197,17 @@ function formatCount(value: unknown) {
分析基础:{{ data.analysisBase }}
-
- 标准兜底校验
+
+
+
+ 标准兜底校验(无历史案例时启用)
+ 关联标准校验
+
{{ standard.status }}
+
+ 当前标准库暂无可匹配规则,请补充产品品类、包材或检测指标。
+
diff --git a/src/components/assistant/RiskAnalysisCard.vue b/src/components/assistant/RiskAnalysisCard.vue
index f6a6c19..474bac6 100644
--- a/src/components/assistant/RiskAnalysisCard.vue
+++ b/src/components/assistant/RiskAnalysisCard.vue
@@ -22,6 +22,15 @@ const props = defineProps<{
const expanded = ref(true);
const feedback = ref("");
+interface DimensionView {
+ name: string;
+ score: number;
+ weight: string;
+ tone: string;
+ status?: string;
+ reason?: string | null;
+}
+
function notify(label: string) {
feedback.value = `${label}功能将在对应模块接入后执行`;
window.setTimeout(() => (feedback.value = ""), 2600);
@@ -34,19 +43,29 @@ function copyScore() {
notify("评分已复制");
}
-const dimensions = computed(() =>
+const dimensions = computed(() =>
(props.result.dimensionScores?.length
- ? props.result.dimensionScores
+ ? props.result.dimensionScores.map((item) => ({
+ dimension: item.dimension,
+ score: item.score,
+ weight: item.weight,
+ status: item.status,
+ reason: item.reason,
+ }))
: props.result.factors.map((factor) => ({
dimension: factor.name,
score: Math.max(0, Math.round(100 - factor.value)),
weight: factor.weight,
+ status: undefined,
+ reason: undefined,
}))
).map((item) => ({
name: item.dimension,
score: Math.round(item.score),
weight: `${item.weight}%`,
tone: item.score < 40 ? "danger" : item.score < 60 ? "warning" : "success",
+ status: item.status,
+ reason: item.reason,
})),
);
const suggestions = computed(() =>
@@ -195,7 +214,8 @@ const stars = computed(() => {
{{ item.score }}分 · {{ item.weight }}分 · {{ item.weight }}
+ 默认
diff --git a/src/components/risk/RiskAssessmentForm.vue b/src/components/risk/RiskAssessmentForm.vue
index beab4b8..86c7f2f 100644
--- a/src/components/risk/RiskAssessmentForm.vue
+++ b/src/components/risk/RiskAssessmentForm.vue
@@ -1,8 +1,8 @@
+
+ AI 案例推理方案明细(已追加)
+
+
+ {{ solution.title || "AI 案例推理生成" }}
+ 置信度:{{ solution.confidence }}
+
+
+ 分析基础:{{ solution.analysisBase }}
+
+
+ 风险项
+
+ -
+ {{ risk.level }} · {{ risk.title }}:{{ risk.detail }}
+
+
+
+
+ 建议方案
+
+ -
+ {{ suggestion }}
+
+
+
+
+ {{ solution.warning }}
+
+
+
+
七、标准判定结果
@@ -440,6 +481,61 @@ ol {
font-size: 13px;
line-height: 1.8;
}
+.ai-solution-report {
+ margin: 0 0 16px;
+ border: 1px solid #bfdbfe;
+ border-left: 3px solid #7c3aed;
+ border-radius: 7px;
+ padding: 14px;
+ background: #fafbff;
+}
+.report-subsection-title {
+ margin: 14px 0 8px;
+ color: #334155;
+ font-size: 13px;
+}
+.ai-solution-report header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
+ color: #1d4ed8;
+ font-size: 13px;
+}
+.ai-solution-report header span {
+ border: 1px solid #fde68a;
+ border-radius: 999px;
+ padding: 2px 8px;
+ background: #fffbeb;
+ color: #d97706;
+ font-size: 10px;
+}
+.ai-solution-report p,
+.ai-solution-report li {
+ color: #334155;
+ font-size: 12px;
+ line-height: 1.7;
+}
+.ai-solution-report p {
+ margin: 6px 0;
+}
+.ai-solution-report h3 {
+ margin: 10px 0 4px;
+ font-size: 12px;
+}
+.ai-solution-report ul,
+.ai-solution-report ol {
+ margin: 4px 0;
+ padding-left: 20px;
+}
+.ai-solution-report__warning {
+ border-radius: 4px;
+ padding: 7px 9px;
+ background: #fffbeb;
+ color: #92400e !important;
+ font-size: 11px !important;
+}
@media (max-width: 700px) {
.report-page {
padding: 12px;
diff --git a/src/pages/RiskAssistantPage.vue b/src/pages/RiskAssistantPage.vue
index b4cae0c..dc030db 100644
--- a/src/pages/RiskAssistantPage.vue
+++ b/src/pages/RiskAssistantPage.vue
@@ -11,7 +11,7 @@ import OcrRecognitionDialog from "@/components/ocr/OcrRecognitionDialog.vue";
import { useConversationStore } from "@/stores/conversation.store";
import { useAuthStore } from "@/stores/auth.store";
import { storeToRefs } from "pinia";
-import type { RiskAssessmentResult } from "@/types/risk";
+import type { AiSolutionReport, RiskAssessmentResult } from "@/types/risk";
const route = useRoute();
const router = useRouter();
@@ -20,8 +20,10 @@ const conversationStore = useConversationStore();
const {
current: conversation,
sending: loading,
+ generating,
error,
} = storeToRefs(conversationStore);
+const withdrawingId = ref(null);
const prompt = ref("");
const ocrOpen = ref(false);
const scrollArea = ref(null);
@@ -204,6 +206,25 @@ function handleScroll() {
if (scrollArea.value && scrollArea.value.scrollTop <= 80) loadOlder();
}
+function hasAssistantPair(index: number) {
+ return conversation.value?.messages[index + 1]?.role === "assistant";
+}
+
+function stopGeneration() {
+ conversationStore.stopGeneration();
+}
+
+async function withdrawMessage(messageId: number) {
+ if (!conversation.value || withdrawingId.value !== null) return;
+ if (!window.confirm("撤回后将同时删除该问题和对应回答,确认撤回吗?")) return;
+ withdrawingId.value = messageId;
+ try {
+ await conversationStore.withdraw(messageId);
+ } finally {
+ withdrawingId.value = null;
+ }
+}
+
async function openFromRoute() {
const rawId = route.query.conversation;
if (typeof rawId !== "string") return;
@@ -227,6 +248,53 @@ function openReport(result: RiskAssessmentResult) {
void router.push({ name: "report" });
}
+function openSolutionReport(card: Record) {
+ const solution = card as AiSolutionReport;
+ const current = conversation.value?.messages
+ .slice()
+ .reverse()
+ .find((message) => message.riskResult)?.riskResult;
+ let stored: RiskAssessmentResult | null = null;
+ try {
+ const raw = sessionStorage.getItem("dieya-risk-report");
+ stored = raw ? (JSON.parse(raw) as RiskAssessmentResult) : null;
+ } catch {
+ stored = null;
+ }
+ const sameReport = Boolean(
+ current && stored && current.subjectId === stored.subjectId,
+ );
+ const base: RiskAssessmentResult =
+ sameReport && stored
+ ? structuredClone(stored)
+ : current
+ ? structuredClone(current)
+ : stored || {
+ subjectId: "AI 案例推理方案",
+ score: 50,
+ level: "medium",
+ modelVersion: "ai-solution-card",
+ factors: [],
+ };
+ const existing = base.aiSolutionCards ?? [];
+ const merged: RiskAssessmentResult = {
+ ...base,
+ aiSolutionCards: [...existing, solution],
+ aiSuggestions: [
+ ...(base.aiSuggestions ?? []),
+ ...(solution.suggestions ?? []),
+ ],
+ similarCases: solution.similarCases?.length
+ ? [...(base.similarCases ?? []), ...solution.similarCases]
+ : base.similarCases,
+ standardJudgment: solution.standards?.length
+ ? [...(base.standardJudgment ?? []), ...solution.standards]
+ : base.standardJudgment,
+ };
+ sessionStorage.setItem("dieya-risk-report", JSON.stringify(merged));
+ void router.push({ name: "report" });
+}
+
async function submit(
messageType: "text" | "image" | "file" | "audio" = "text",
) {
@@ -293,6 +361,22 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
{{ message.content }}
+
+
+
@@ -517,6 +609,45 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
text-align: right;
}
+.user-message-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 7px;
+}
+
+.user-message-actions button {
+ border-color: rgb(255 255 255 / 35%);
+ color: rgb(255 255 255 / 82%);
+}
+
+.user-message-actions button,
+.stop-generation {
+ border: 1px solid rgb(255 255 255 / 35%);
+ border-radius: 999px;
+ padding: 3px 9px;
+ background: transparent;
+ color: rgb(255 255 255 / 82%);
+ font-size: 10px;
+}
+
+.user-message-actions button:hover,
+.stop-generation:hover {
+ border-color: rgb(255 255 255 / 65%);
+ color: white;
+}
+
+.user-message-actions button:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
+.stop-generation {
+ margin-top: 8px;
+ border-color: var(--color-border-strong);
+ background: var(--color-surface);
+ color: var(--color-text-secondary);
+}
+
.assistant-bubble {
width: fit-content;
max-width: 82%;
diff --git a/src/stores/conversation.store.ts b/src/stores/conversation.store.ts
index 9def815..0b6eeda 100644
--- a/src/stores/conversation.store.ts
+++ b/src/stores/conversation.store.ts
@@ -5,6 +5,7 @@ import {
createConversation,
fetchConversation,
fetchConversations,
+ withdrawConversationMessage,
streamConversationMessage,
sendConversationMessage,
} from "@/api/conversations";
@@ -19,6 +20,9 @@ export const useConversationStore = defineStore("conversation", () => {
const sending = ref(false);
const loadingOlder = ref(false);
const error = ref(null);
+ const generating = ref(false);
+ let activeController: AbortController | null = null;
+ let activeUserMessageId: number | null = null;
let listRequest: Promise | null = null;
let listRequestSearch: string | undefined;
let listLoadedAt = 0;
@@ -144,6 +148,8 @@ export const useConversationStore = defineStore("conversation", () => {
if (!current.value) await create();
if (!current.value) throw new Error("当前对话不存在");
sending.value = true;
+ generating.value = true;
+ activeController = new AbortController();
error.value = null;
const startedAt = new Date().toISOString();
const userMessage = {
@@ -156,6 +162,7 @@ export const useConversationStore = defineStore("conversation", () => {
semantic: null,
createdAt: startedAt,
};
+ activeUserMessageId = userMessage.id;
const assistantMessage = {
id: userMessage.id - 1,
role: "assistant" as const,
@@ -178,6 +185,23 @@ export const useConversationStore = defineStore("conversation", () => {
conversationId,
content,
(event) => {
+ if (
+ event.type === "start" &&
+ event.data &&
+ typeof event.data === "object" &&
+ "messageId" in event.data &&
+ typeof event.data.messageId === "number" &&
+ current.value
+ ) {
+ const serverMessageId = event.data.messageId;
+ const messages = current.value.messages.map((message) =>
+ message.id === userMessage.id
+ ? { ...message, id: serverMessageId }
+ : message,
+ );
+ activeUserMessageId = serverMessageId;
+ current.value = { ...current.value, messages };
+ }
if (event.type === "delta" && event.content) onDelta(event.content);
if (event.type === "semantic" && event.data && current.value) {
const messages = [...current.value.messages];
@@ -207,6 +231,7 @@ export const useConversationStore = defineStore("conversation", () => {
current.value = event.data as Conversation;
},
messageType,
+ activeController.signal,
);
if (
!current.value ||
@@ -223,15 +248,49 @@ export const useConversationStore = defineStore("conversation", () => {
return current.value;
} catch (reason) {
const conversationId = current.value?.id;
+ if (activeController?.signal.aborted) {
+ if (conversationId) {
+ try {
+ const response = await fetchConversation(conversationId);
+ if (response.data.data) current.value = response.data.data;
+ } catch {
+ // If the request was cancelled before persistence, remove both
+ // temporary messages from the local conversation.
+ if (current.value) {
+ const messages = current.value.messages.filter(
+ (message) =>
+ message.id !== assistantMessage.id &&
+ message.id !== userMessage.id,
+ );
+ current.value = {
+ ...current.value,
+ messages,
+ messageCount: Math.max(0, current.value.messageCount - 2),
+ messagesTotal: Math.max(0, current.value.messagesTotal - 2),
+ };
+ }
+ }
+ const summary = current.value ? toSummary(current.value) : null;
+ if (summary) {
+ 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;
+ }
if (current.value) {
const messages = current.value.messages.filter(
- (message) => message.id !== assistantMessage.id,
+ (message) =>
+ message.id !== assistantMessage.id && message.id !== userMessage.id,
);
current.value = {
...current.value,
messages,
- messageCount: Math.max(0, current.value.messageCount - 1),
- messagesTotal: Math.max(0, current.value.messagesTotal - 1),
+ messageCount: Math.max(0, current.value.messageCount - 2),
+ messagesTotal: Math.max(0, current.value.messagesTotal - 2),
};
}
if (conversationId) {
@@ -246,6 +305,43 @@ export const useConversationStore = defineStore("conversation", () => {
throw reason;
} finally {
sending.value = false;
+ generating.value = false;
+ activeController = null;
+ activeUserMessageId = null;
+ }
+ }
+
+ function stopGeneration() {
+ activeController?.abort();
+ }
+
+ function isGeneratingMessage(messageId: number) {
+ return activeUserMessageId === messageId && generating.value;
+ }
+
+ async function withdraw(messageId: number) {
+ if (!current.value) return;
+ loading.value = true;
+ error.value = null;
+ try {
+ const response = await withdrawConversationMessage(
+ current.value.id,
+ messageId,
+ );
+ 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);
+ return current.value;
+ } catch (reason) {
+ error.value = reason instanceof Error ? reason.message : "撤回失败";
+ throw reason;
+ } finally {
+ loading.value = false;
}
}
@@ -269,6 +365,7 @@ export const useConversationStore = defineStore("conversation", () => {
current,
loading,
sending,
+ generating,
loadingOlder,
error,
loadList,
@@ -277,6 +374,9 @@ export const useConversationStore = defineStore("conversation", () => {
create,
send,
streamSend,
+ stopGeneration,
+ isGeneratingMessage,
+ withdraw,
reset,
};
});
diff --git a/src/types/risk.ts b/src/types/risk.ts
index 13d9681..31e06cd 100644
--- a/src/types/risk.ts
+++ b/src/types/risk.ts
@@ -53,6 +53,7 @@ export interface RiskAssessmentResult {
similarity?: number | null;
}>;
aiSuggestions?: string[];
+ aiSolutionCards?: AiSolutionReport[];
riskBadge?: string | null;
alertText?: string | null;
categoryWarning?: {
@@ -84,6 +85,24 @@ export interface RiskAssessmentResult {
dimensionScores?: RiskDimensionScore[];
}
+export interface AiSolutionReport {
+ title?: string;
+ confidence?: string | null;
+ analysisBase?: string | null;
+ standardFallbackEnabled?: boolean;
+ standards?: Array<{
+ category: string;
+ riskPoint: string;
+ rule: string;
+ source: string | null;
+ status: string;
+ }>;
+ similarCases?: NonNullable;
+ risks?: Array<{ level: string; title: string; detail: string }>;
+ suggestions?: string[];
+ warning?: string | null;
+}
+
export type RiskReportResult = RiskAssessmentResult;
export interface ModelInfo {
--
2.47.2
From d05be62e024e9c884aeaf735fab3fab74c2b6fe4 Mon Sep 17 00:00:00 2001
From: kx_guo
Date: Wed, 9 Sep 2026 15:51:43 +0800
Subject: [PATCH 09/31] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BE=9B=E5=BA=94?=
=?UTF-8?q?=E5=95=86=E6=90=9C=E7=B4=A2=E4=B8=8B=E6=8B=89=E4=B8=8E=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E6=93=8D=E4=BD=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
供应商画像选择器改为支持输入筛选的动态下拉框,输入关键词实时匹配并刷新画像;删除暂未实现的收藏方案按钮,保留已接入操作。
---
.../assistant/AssistantResponseCard.vue | 8 -
src/pages/SupplierProfilePage.vue | 164 ++++++++++++++++--
2 files changed, 153 insertions(+), 19 deletions(-)
diff --git a/src/components/assistant/AssistantResponseCard.vue b/src/components/assistant/AssistantResponseCard.vue
index fc4b033..401f1ad 100644
--- a/src/components/assistant/AssistantResponseCard.vue
+++ b/src/components/assistant/AssistantResponseCard.vue
@@ -403,14 +403,6 @@ function generateReport() {
>
查看标准详情
-