修复报告会话关联与供应商搜索兼容性
This commit is contained in:
parent
763d8cc545
commit
c2a0756d2f
@ -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() });
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
42
src/utils/conversationReports.ts
Normal file
42
src/utils/conversationReports.ts
Normal 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.
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user