修复报告会话关联与供应商搜索兼容性
This commit is contained in:
parent
763d8cc545
commit
c2a0756d2f
@ -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() });
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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