修复报告会话关联与供应商搜索兼容性

This commit is contained in:
kx_guo 2026-09-10 16:15:46 +08:00
parent 763d8cc545
commit c2a0756d2f
5 changed files with 153 additions and 33 deletions

View File

@ -18,7 +18,8 @@ export async function fetchSupplierProfile(
if (running) return { data: { data: await running } }; if (running) return { data: { data: await running } };
const request = (async () => { const request = (async () => {
const response = await http.get<ApiResponse<SupplierProfile>>( const response = await http.get<ApiResponse<SupplierProfile>>(
`/v1/suppliers/${encodeURIComponent(key)}`, "/v1/suppliers/profile",
{ params: { supplier_name: key } },
); );
if (!response.data.data) throw new Error("供应商画像响应为空"); if (!response.data.data) throw new Error("供应商画像响应为空");
cache.set(key, { data: response.data.data, at: Date.now() }); cache.set(key, { data: response.data.data, at: Date.now() });

View File

@ -1,21 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowLeft, Download, FileText } from "@lucide/vue"; import { ArrowLeft, Download, FileText } from "@lucide/vue";
import { computed } from "vue"; import { computed, onActivated, onMounted, shallowRef, watch } from "vue";
import { useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { downloadReportPdf, downloadReportWord } from "@/api/reports"; import { downloadReportPdf, downloadReportWord } from "@/api/reports";
import { useAuthStore } from "@/stores/auth.store";
import { useConversationStore } from "@/stores/conversation.store";
import type { ReportCard, RiskReportResult } from "@/types/risk"; import type { ReportCard, RiskReportResult } from "@/types/risk";
import { formatRiskLevel } from "@/utils/risk"; import { formatRiskLevel } from "@/utils/risk";
import { readConversationReport } from "@/utils/conversationReports";
const router = useRouter(); const router = useRouter();
let parsed: RiskReportResult | null = null; const route = useRoute();
try { const auth = useAuthStore();
const raw = sessionStorage.getItem("dieya-risk-report"); const conversationStore = useConversationStore();
parsed = raw ? (JSON.parse(raw) as RiskReportResult) : null; const report = shallowRef<RiskReportResult | null>(null);
} catch {
parsed = null; function loadReport() {
const conversationId =
route.query.conversation ?? conversationStore.current?.id;
report.value = readConversationReport(
typeof conversationId === "string" || typeof conversationId === "number"
? conversationId
: null,
auth.user?.id,
);
} }
const report = computed(() => parsed);
// ReportPage is kept alive by MainLayout. Reload the session payload whenever
// the page becomes active so a newly generated report is immediately visible.
onMounted(loadReport);
onActivated(loadReport);
watch(
[() => route.query.conversation, () => conversationStore.current?.id],
loadReport,
);
const reportCards = computed<ReportCard[]>( const reportCards = computed<ReportCard[]>(
() => { () => {
if (!report.value) return []; if (!report.value) return [];
@ -124,6 +143,17 @@ async function downloadPdf() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
function returnToConversation() {
const conversationId =
route.query.conversation ?? conversationStore.current?.id;
void router.push({
name: "assessment",
...(typeof conversationId === "string" || typeof conversationId === "number"
? { query: { conversation: conversationId } }
: {}),
});
}
function notify(message: string) { function notify(message: string) {
window.alert(`${message}功能待接入服务端`); window.alert(`${message}功能待接入服务端`);
} }
@ -154,10 +184,20 @@ function dimensionConclusion(item: {
<template> <template>
<div class="report-page"> <div class="report-page">
<header class="report-actions"> <header class="report-actions">
<button type="button" class="secondary-button" @click="downloadPdf"> <button
type="button"
class="secondary-button"
:disabled="!report"
@click="downloadPdf"
>
<Download :size="15" />导出 PDF <Download :size="15" />导出 PDF
</button> </button>
<button type="button" class="secondary-button" @click="downloadWord"> <button
type="button"
class="secondary-button"
:disabled="!report"
@click="downloadWord"
>
<Download :size="15" />导出 Word <Download :size="15" />导出 Word
</button> </button>
<button <button
@ -167,12 +207,12 @@ function dimensionConclusion(item: {
> >
<FileText :size="15" />钉钉发送 <FileText :size="15" />钉钉发送
</button> </button>
<button type="button" class="primary-button" @click="router.back()"> <button type="button" class="primary-button" @click="returnToConversation">
<ArrowLeft :size="15" />返回对话 <ArrowLeft :size="15" />返回对话
</button> </button>
</header> </header>
<p v-if="!report" class="report-empty"> <p v-if="!report" class="report-empty">
暂无报告数据,请从风险评估结果卡片点击“生成报告”。 当前会话暂无报告,请返回对话后从风险评估结果卡片点击“生成报告”。
</p> </p>
<article v-else class="report-sheet"> <article v-else class="report-sheet">
<h1>新品风险预估报告</h1> <h1>新品风险预估报告</h1>

View File

@ -12,6 +12,10 @@ import { useConversationStore } from "@/stores/conversation.store";
import { useAuthStore } from "@/stores/auth.store"; import { useAuthStore } from "@/stores/auth.store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import type { ReportCard, RiskAssessmentResult } from "@/types/risk"; import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
import {
readConversationReport,
writeConversationReport,
} from "@/utils/conversationReports";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -287,13 +291,14 @@ async function openFromRoute() {
} }
} }
function readStoredReport() { /**
try { * Vue/Pinia exposes conversation results as reactive proxies. Those proxies
const raw = sessionStorage.getItem("dieya-risk-report"); * cannot be passed to structuredClone (the browser reports DataCloneError),
return raw ? (JSON.parse(raw) as RiskAssessmentResult) : null; * while the report payload only contains JSON data. Serialising here also
} catch { * makes the value safe to put into sessionStorage.
return null; */
} function cloneReport<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
} }
function legacySolutionCards(result: RiskAssessmentResult): ReportCard[] { function legacySolutionCards(result: RiskAssessmentResult): ReportCard[] {
@ -312,7 +317,9 @@ function legacySolutionCards(result: RiskAssessmentResult): ReportCard[] {
} }
function openReport(result: RiskAssessmentResult) { function openReport(result: RiskAssessmentResult) {
const stored = readStoredReport(); const conversationId = conversation.value?.id;
if (!conversationId) return;
const stored = readConversationReport(conversationId, auth.user?.id);
const storedCards = stored const storedCards = stored
? stored.reportCards?.length ? stored.reportCards?.length
? stored.reportCards ? stored.reportCards
@ -321,31 +328,36 @@ function openReport(result: RiskAssessmentResult) {
const merged: RiskAssessmentResult = const merged: RiskAssessmentResult =
stored?.subjectId === result.subjectId stored?.subjectId === result.subjectId
? { ? {
...structuredClone(result), ...cloneReport(result),
reportCards: storedCards.length ? storedCards : result.reportCards ?? [], reportCards: storedCards.length ? storedCards : result.reportCards ?? [],
aiSolutionCards: stored.aiSolutionCards?.length aiSolutionCards: stored.aiSolutionCards?.length
? stored.aiSolutionCards ? stored.aiSolutionCards
: result.aiSolutionCards ?? [], : result.aiSolutionCards ?? [],
} }
: structuredClone(result); : cloneReport(result);
sessionStorage.setItem("dieya-risk-report", JSON.stringify(merged)); writeConversationReport(conversationId, merged, auth.user?.id);
void router.push({ name: "report" }); void router.push({
name: "report",
query: { conversation: String(conversationId) },
});
} }
function openSolutionReport(card: ReportCard) { function openSolutionReport(card: ReportCard) {
const conversationId = conversation.value?.id;
if (!conversationId) return;
const current = conversation.value?.messages const current = conversation.value?.messages
.slice() .slice()
.reverse() .reverse()
.find((message) => message.riskResult)?.riskResult; .find((message) => message.riskResult)?.riskResult;
const stored = readStoredReport(); const stored = readConversationReport(conversationId, auth.user?.id);
const sameReport = Boolean( const sameReport = Boolean(
current && stored && current.subjectId === stored.subjectId, current && stored && current.subjectId === stored.subjectId,
); );
const base: RiskAssessmentResult = const base: RiskAssessmentResult =
sameReport && stored sameReport && stored
? structuredClone(stored) ? cloneReport(stored)
: current : current
? structuredClone(current) ? cloneReport(current)
: stored || { : stored || {
subjectId: "AI 案例推理方案", subjectId: "AI 案例推理方案",
score: 50, score: 50,
@ -365,8 +377,11 @@ function openSolutionReport(card: ReportCard) {
card, card,
], ],
}; };
sessionStorage.setItem("dieya-risk-report", JSON.stringify(merged)); writeConversationReport(conversationId, merged, auth.user?.id);
void router.push({ name: "report" }); void router.push({
name: "report",
query: { conversation: String(conversationId) },
});
} }
async function submit( async function submit(

View File

@ -117,7 +117,7 @@ function searchSuppliers(value: string) {
supplierSearching.value = false; supplierSearching.value = false;
return; return;
} }
const local = supplierNames.value.filter((name) => name.includes(keyword)); const local = supplierNames.value.filter((name) => matchesSupplier(name, keyword));
supplierResults.value = local; supplierResults.value = local;
supplierSearching.value = true; supplierSearching.value = true;
supplierSearchTimer = setTimeout(async () => { supplierSearchTimer = setTimeout(async () => {
@ -133,6 +133,28 @@ function searchSuppliers(value: string) {
}, 180); }, 180);
} }
function compactSupplierText(value: string) {
return value.toLocaleLowerCase().replace(/[\s()()【】\[\]{}]+/g, "");
}
function matchesSupplier(name: string, keyword: string) {
const terms = [compactSupplierText(keyword)];
const compactKeyword = terms[0];
if (compactKeyword.endsWith("市") && compactKeyword.length > 2) {
terms.push(compactKeyword.slice(0, -1));
}
if (compactKeyword.endsWith("有限公司") && compactKeyword.length > 5) {
terms.push(compactKeyword.slice(0, -5));
}
const segments = name
.split(/[\//、,,;;|\\]+/)
.map(compactSupplierText)
.filter(Boolean);
return terms.some((term) =>
segments.some((segment) => segment.includes(term) || term.includes(segment)),
);
}
function onSupplierInput(event: Event) { function onSupplierInput(event: Event) {
supplierQuery.value = (event.target as HTMLInputElement).value; supplierQuery.value = (event.target as HTMLInputElement).value;
supplierDropdownOpen.value = true; supplierDropdownOpen.value = true;
@ -230,7 +252,7 @@ watch(
<span <span
v-else-if="!supplierResults.length" v-else-if="!supplierResults.length"
class="supplier-option-state" class="supplier-option-state"
>未找到匹配供应商</span >未找到匹配供应商,可尝试输入公司简称、城市名或完整名称</span
> >
</div> </div>
</div> </div>

View File

@ -0,0 +1,42 @@
import type { RiskReportResult } from "@/types/risk";
const STORAGE_PREFIX = "dieya-risk-reports";
type ReportMap = Record<string, RiskReportResult>;
function storageKey(userId: number | string | null | undefined) {
return `${STORAGE_PREFIX}:${userId ?? "current-user"}`;
}
function readMap(userId: number | string | null | undefined): ReportMap {
try {
const raw = sessionStorage.getItem(storageKey(userId));
if (!raw) return {};
const parsed = JSON.parse(raw) as unknown;
return parsed && typeof parsed === "object" ? (parsed as ReportMap) : {};
} catch {
return {};
}
}
export function readConversationReport(
conversationId: number | string | null | undefined,
userId?: number | string | null,
): RiskReportResult | null {
if (conversationId == null || String(conversationId).trim() === "") return null;
return readMap(userId)[String(conversationId)] ?? null;
}
export function writeConversationReport(
conversationId: number | string,
report: RiskReportResult,
userId?: number | string | null,
) {
if (conversationId == null || String(conversationId).trim() === "") return;
const reports = readMap(userId);
reports[String(conversationId)] = report;
try {
sessionStorage.setItem(storageKey(userId), JSON.stringify(reports));
} catch {
// Storage failure should not prevent opening the report page.
}
}