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

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 } };
const request = (async () => {
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("供应商画像响应为空");
cache.set(key, { data: response.data.data, at: Date.now() });

View File

@ -1,21 +1,40 @@
<script setup lang="ts">
import { ArrowLeft, Download, FileText } from "@lucide/vue";
import { computed } from "vue";
import { useRouter } from "vue-router";
import { computed, onActivated, onMounted, shallowRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
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 { formatRiskLevel } from "@/utils/risk";
import { readConversationReport } from "@/utils/conversationReports";
const router = useRouter();
let parsed: RiskReportResult | null = null;
try {
const raw = sessionStorage.getItem("dieya-risk-report");
parsed = raw ? (JSON.parse(raw) as RiskReportResult) : null;
} catch {
parsed = null;
const route = useRoute();
const auth = useAuthStore();
const conversationStore = useConversationStore();
const report = shallowRef<RiskReportResult | null>(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[]>(
() => {
if (!report.value) return [];
@ -124,6 +143,17 @@ async function downloadPdf() {
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) {
window.alert(`${message}功能待接入服务端`);
}
@ -154,10 +184,20 @@ function dimensionConclusion(item: {
<template>
<div class="report-page">
<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
</button>
<button type="button" class="secondary-button" @click="downloadWord">
<button
type="button"
class="secondary-button"
:disabled="!report"
@click="downloadWord"
>
<Download :size="15" />导出 Word
</button>
<button
@ -167,12 +207,12 @@ function dimensionConclusion(item: {
>
<FileText :size="15" />钉钉发送
</button>
<button type="button" class="primary-button" @click="router.back()">
<button type="button" class="primary-button" @click="returnToConversation">
<ArrowLeft :size="15" />返回对话
</button>
</header>
<p v-if="!report" class="report-empty">
暂无报告数据,请从风险评估结果卡片点击“生成报告”。
当前会话暂无报告,请返回对话后从风险评估结果卡片点击“生成报告”。
</p>
<article v-else class="report-sheet">
<h1>新品风险预估报告</h1>

View File

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

View File

@ -117,7 +117,7 @@ function searchSuppliers(value: string) {
supplierSearching.value = false;
return;
}
const local = supplierNames.value.filter((name) => name.includes(keyword));
const local = supplierNames.value.filter((name) => matchesSupplier(name, keyword));
supplierResults.value = local;
supplierSearching.value = true;
supplierSearchTimer = setTimeout(async () => {
@ -133,6 +133,28 @@ function searchSuppliers(value: string) {
}, 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) {
supplierQuery.value = (event.target as HTMLInputElement).value;
supplierDropdownOpen.value = true;
@ -230,7 +252,7 @@ watch(
<span
v-else-if="!supplierResults.length"
class="supplier-option-state"
>未找到匹配供应商</span
>未找到匹配供应商,可尝试输入公司简称、城市名或完整名称</span
>
</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.
}
}