dev #1
@ -15,11 +15,12 @@ import { computed, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import type { AssistantResponseCard } from "@/types/conversation";
|
||||
import type { ReportCard } from "@/types/risk";
|
||||
|
||||
const props = defineProps<{ card: AssistantResponseCard }>();
|
||||
const router = useRouter();
|
||||
const emit = defineEmits<{
|
||||
generateReport: [card: AssistantResponseCard];
|
||||
generateReport: [card: ReportCard];
|
||||
}>();
|
||||
interface CardData extends AssistantResponseCard {
|
||||
name?: string;
|
||||
@ -119,7 +120,7 @@ function formatCount(value: unknown) {
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
emit("generateReport", props.card);
|
||||
emit("generateReport", JSON.parse(JSON.stringify(props.card)) as ReportCard);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -389,8 +390,7 @@ function generateReport() {
|
||||
<button
|
||||
type="button"
|
||||
class="response-actions__primary"
|
||||
:disabled="card.type !== 'solution'"
|
||||
:title="card.type === 'solution' ? '追加到当前风险报告' : '功能开发中'"
|
||||
title="追加到当前风险报告"
|
||||
@click="generateReport"
|
||||
>
|
||||
<CheckCircle2 :size="14" />生成报告
|
||||
|
||||
@ -4,7 +4,7 @@ import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { downloadReportPdf, downloadReportWord } from "@/api/reports";
|
||||
import type { RiskReportResult } from "@/types/risk";
|
||||
import type { ReportCard, RiskReportResult } from "@/types/risk";
|
||||
import { formatRiskLevel } from "@/utils/risk";
|
||||
|
||||
const router = useRouter();
|
||||
@ -16,6 +16,24 @@ try {
|
||||
parsed = null;
|
||||
}
|
||||
const report = computed(() => parsed);
|
||||
const reportCards = computed<ReportCard[]>(
|
||||
() => {
|
||||
if (!report.value) return [];
|
||||
if (report.value.reportCards?.length) return report.value.reportCards;
|
||||
return (report.value.aiSolutionCards ?? []).map((solution) => ({
|
||||
type: "solution" as const,
|
||||
title: solution.title || "AI 案例推理生成",
|
||||
confidence: solution.confidence,
|
||||
analysisBase: solution.analysisBase,
|
||||
standardFallbackEnabled: solution.standardFallbackEnabled,
|
||||
standards: solution.standards,
|
||||
similarCases: solution.similarCases,
|
||||
risks: solution.risks,
|
||||
suggestions: solution.suggestions,
|
||||
warning: solution.warning,
|
||||
}));
|
||||
},
|
||||
);
|
||||
const dimensionRows = computed(() => {
|
||||
if (!report.value) return [];
|
||||
if (report.value.dimensionScores?.length) {
|
||||
@ -277,47 +295,6 @@ function dimensionConclusion(item: {
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<template v-if="report.aiSolutionCards?.length">
|
||||
<h3 class="report-subsection-title">AI 案例推理方案明细(已追加)</h3>
|
||||
<section
|
||||
v-for="(solution, index) in report.aiSolutionCards"
|
||||
:key="`${solution.title || 'solution'}-${index}`"
|
||||
class="ai-solution-report"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ solution.title || "AI 案例推理生成" }}</strong>
|
||||
<span v-if="solution.confidence"
|
||||
>置信度:{{ solution.confidence }}</span
|
||||
>
|
||||
</header>
|
||||
<p v-if="solution.analysisBase">
|
||||
<b>分析基础:</b>{{ solution.analysisBase }}
|
||||
</p>
|
||||
<template v-if="solution.risks?.length">
|
||||
<h3>风险项</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="risk in solution.risks"
|
||||
:key="`${risk.title}-${risk.detail}`"
|
||||
>
|
||||
<b>{{ risk.level }} · {{ risk.title }}:</b>{{ risk.detail }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-if="solution.suggestions?.length">
|
||||
<h3>建议方案</h3>
|
||||
<ol>
|
||||
<li v-for="suggestion in solution.suggestions" :key="suggestion">
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
<p v-if="solution.warning" class="ai-solution-report__warning">
|
||||
{{ solution.warning }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<h2>七、标准判定结果</h2>
|
||||
<template v-if="report.standardJudgment?.length">
|
||||
<table>
|
||||
@ -347,6 +324,41 @@ function dimensionConclusion(item: {
|
||||
双引擎融合:历史异常经验引擎提供风险优先级,标准库提供可执行的检测阈值与验证任务。
|
||||
</p>
|
||||
|
||||
<template v-if="reportCards.length">
|
||||
<h2>八、追加卡片分析</h2>
|
||||
<section
|
||||
v-for="(card, index) in reportCards"
|
||||
:key="`${card.type}-${card.title}-${index}`"
|
||||
class="report-card-detail"
|
||||
>
|
||||
<header><strong>{{ card.title }}</strong><span>{{ card.type }}</span></header>
|
||||
<p v-if="card.name || card.material || card.category"><b>分析对象:</b>{{ card.name || card.material || card.category }}</p>
|
||||
<p v-if="card.partyType"><b>对象类型:</b>{{ card.partyType }}</p>
|
||||
<p v-if="card.question"><b>原始问题:</b>{{ card.question }}</p>
|
||||
<p v-if="card.anomalyCount != null"><b>历史异常:</b>{{ card.anomalyCount }} 条</p>
|
||||
<p v-if="card.cooperationCount != null"><b>合作次数:</b>{{ card.cooperationCount }} 次</p>
|
||||
<p v-if="card.frequencyBasis"><b>数据口径:</b>{{ card.frequencyBasis }}</p>
|
||||
<p v-if="card.dataAvailable != null"><b>数据状态:</b>{{ card.dataAvailable ? "有匹配数据" : "暂无匹配数据" }}</p>
|
||||
<p v-if="card.frequencyCoefficient != null"><b>频次系数:</b>×{{ card.frequencyCoefficient.toFixed(2) }}</p>
|
||||
<p v-if="card.supplierBaseScore != null"><b>供应商基础分:</b>{{ card.supplierBaseScore.toFixed(2) }} 分</p>
|
||||
<p v-if="card.supplierScore != null"><b>供应商得分:</b>{{ card.supplierScore.toFixed(2) }} 分</p>
|
||||
<p v-if="card.tier"><b>层级:</b>{{ card.tier }}</p>
|
||||
<p v-if="card.matchedCount != null"><b>匹配数量:</b>{{ card.matchedCount }} 条</p>
|
||||
<p v-if="card.analysisBase"><b>分析基础:</b>{{ card.analysisBase }}</p>
|
||||
<p v-if="card.confidence"><b>置信度:</b>{{ card.confidence }}</p>
|
||||
<p v-if="card.standardFallbackEnabled"><b>标准兜底校验:</b>已启用(无直接历史案例)</p>
|
||||
<p v-if="card.detectedValues?.length"><b>检测值:</b>{{ card.detectedValues.map((item) => `${item.name}:${item.value}`).join("、") }}</p>
|
||||
<p v-if="card.topProblems?.length"><b>高频问题:</b>{{ card.topProblems.map((item) => `${item.name}(${item.count}次)`).join("、") }}</p>
|
||||
<p v-if="card.frequentProblems?.length"><b>常见问题:</b>{{ card.frequentProblems.map((item) => `${item.name}(${item.count}次)`).join("、") }}</p>
|
||||
<template v-if="card.standards?.length"><p><b>关联标准:</b></p><ul class="report-card-detail__standards"><li v-for="standard in card.standards" :key="`${standard.category}-${standard.riskPoint}`">{{ standard.category }} · {{ standard.riskPoint }}:{{ standard.rule }}({{ standard.source || "标准文件.xlsx" }})· {{ standard.status }}</li></ul></template>
|
||||
<template v-if="card.similarCases?.length"><p><b>相似案例:</b></p><ul class="report-card-detail__standards"><li v-for="(item, caseIndex) in card.similarCases" :key="`${item.date || 'case'}-${caseIndex}`">{{ item.date || "日期未知" }} · {{ item.product || "未命名产品" }} · {{ item.problem || item.description || "异常问题未填写" }} → {{ item.solution || "解决方案未填写" }}<template v-if="item.similarity != null">(相似度 {{ Math.round(item.similarity * 100) }}%)</template></li></ul></template>
|
||||
<ul v-if="card.risks?.length"><li v-for="risk in card.risks" :key="`${risk.title}-${risk.detail}`"><b>{{ risk.level }} · {{ risk.title }}:</b>{{ risk.detail }}</li></ul>
|
||||
<ol v-if="card.suggestions?.length"><li v-for="suggestion in card.suggestions" :key="suggestion">{{ suggestion }}</li></ol>
|
||||
<p v-if="card.note"><b>备注:</b>{{ card.note }}</p>
|
||||
<p v-if="card.warning" class="report-card-detail__warning">{{ card.warning }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<footer class="report-footer">
|
||||
<span>编制:蝶雅风险预估系统</span
|
||||
><span>生成时间:{{ generatedAt }}</span>
|
||||
@ -494,6 +506,45 @@ ol {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
.report-card-detail {
|
||||
margin: 0 0 14px;
|
||||
border: 1px solid #dbe3ef;
|
||||
border-left: 3px solid #2563eb;
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px;
|
||||
background: #fafcff;
|
||||
}
|
||||
.report-card-detail header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 7px;
|
||||
color: #1d4ed8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.report-card-detail header span {
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
}
|
||||
.report-card-detail p,
|
||||
.report-card-detail li {
|
||||
margin: 4px 0;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.report-card-detail ul {
|
||||
margin: 4px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.report-card-detail__standards {
|
||||
color: #475569;
|
||||
}
|
||||
.report-card-detail__warning {
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
background: #fffbeb;
|
||||
color: #92400e !important;
|
||||
}
|
||||
.ai-solution-report header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -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 { AiSolutionReport, RiskAssessmentResult } from "@/types/risk";
|
||||
import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -69,6 +69,29 @@ const toolLabel = computed(() => {
|
||||
if (tool.value === "cases") return "案例关键词";
|
||||
return "方案主题";
|
||||
});
|
||||
const toolDescription = computed(() => {
|
||||
if (tool.value === "assessment")
|
||||
return "填写品类、包装材质(必填),可补充规格、供应商、加工厂和开发阶段。";
|
||||
if (tool.value === "material")
|
||||
return "输入物料名称或组合,例如:软管、PET瓶、铝瓶+喷头、泵头。";
|
||||
if (tool.value === "cases")
|
||||
return "描述要检索的异常现象,例如:软管印刷遇水脱落、泵头漏液。";
|
||||
if (tool.value === "solution")
|
||||
return "描述需要生成方案的新品、品类、材质或风险问题,例如:铝瓶+喷头的密封验证方案。";
|
||||
return "填写必要信息后,将生成一条带上下文的对话请求。";
|
||||
});
|
||||
const toolPlaceholder = computed(() => {
|
||||
if (tool.value === "material") return "如:软管 / PET瓶 / 铝瓶+喷头";
|
||||
if (tool.value === "cases") return "如:软管印刷遇水脱落,如何处理";
|
||||
if (tool.value === "solution")
|
||||
return "如:次氯酸消毒喷雾,铝瓶+喷头,生成验证方案";
|
||||
return "请输入内容";
|
||||
});
|
||||
const toolSubmitLabel = computed(() =>
|
||||
["assessment", "material", "cases", "solution"].includes(tool.value ?? "")
|
||||
? "开始分析"
|
||||
: "带入对话",
|
||||
);
|
||||
|
||||
const loadingText = computed(() =>
|
||||
loading.value ? "正在调用风险评分引擎…" : "",
|
||||
@ -155,6 +178,27 @@ function submitTool() {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
} else if (selectedTool === "material") {
|
||||
const value = toolInput.value.trim();
|
||||
if (!value) {
|
||||
toolError.value = "请输入要查询的物料名称";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `查物料风险:${value}`;
|
||||
} else if (selectedTool === "cases") {
|
||||
const value = toolInput.value.trim();
|
||||
if (!value) {
|
||||
toolError.value = "请输入要检索的异常现象或关键词";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `搜索相似案例:${value}`;
|
||||
} else if (selectedTool === "solution") {
|
||||
const value = toolInput.value.trim();
|
||||
if (!value) {
|
||||
toolError.value = "请输入需要生成方案的产品或风险问题";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `生成 AI 案例推理方案:${value}`;
|
||||
}
|
||||
const finalValue = toolInput.value.trim();
|
||||
if (!finalValue && !toolFile.value) {
|
||||
@ -243,24 +287,54 @@ async function openFromRoute() {
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredReport() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem("dieya-risk-report");
|
||||
return raw ? (JSON.parse(raw) as RiskAssessmentResult) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function legacySolutionCards(result: RiskAssessmentResult): ReportCard[] {
|
||||
return (result.aiSolutionCards ?? []).map((item) => ({
|
||||
type: "solution" as const,
|
||||
title: item.title || "AI 案例推理生成",
|
||||
confidence: item.confidence,
|
||||
analysisBase: item.analysisBase,
|
||||
standardFallbackEnabled: item.standardFallbackEnabled,
|
||||
standards: item.standards,
|
||||
similarCases: item.similarCases,
|
||||
risks: item.risks,
|
||||
suggestions: item.suggestions,
|
||||
warning: item.warning,
|
||||
}));
|
||||
}
|
||||
|
||||
function openReport(result: RiskAssessmentResult) {
|
||||
sessionStorage.setItem("dieya-risk-report", JSON.stringify(result));
|
||||
const stored = readStoredReport();
|
||||
const merged: RiskAssessmentResult =
|
||||
stored?.subjectId === result.subjectId
|
||||
? {
|
||||
...structuredClone(result),
|
||||
reportCards: stored.reportCards?.length
|
||||
? stored.reportCards
|
||||
: 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" });
|
||||
}
|
||||
|
||||
function openSolutionReport(card: Record<string, unknown>) {
|
||||
const solution = card as AiSolutionReport;
|
||||
function openSolutionReport(card: ReportCard) {
|
||||
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 stored = readStoredReport();
|
||||
const sameReport = Boolean(
|
||||
current && stored && current.subjectId === stored.subjectId,
|
||||
);
|
||||
@ -276,19 +350,23 @@ function openSolutionReport(card: Record<string, unknown>) {
|
||||
modelVersion: "ai-solution-card",
|
||||
factors: [],
|
||||
};
|
||||
const existing = base.aiSolutionCards ?? [];
|
||||
const existingCards = base.reportCards ?? [];
|
||||
const merged: RiskAssessmentResult = {
|
||||
...base,
|
||||
aiSolutionCards: [...existing, solution],
|
||||
aiSuggestions: [
|
||||
...(base.aiSuggestions ?? []),
|
||||
...(solution.suggestions ?? []),
|
||||
// All newly appended cards use one canonical collection. Migrate old
|
||||
// AI-only entries once so they are not lost when the first generic card
|
||||
// is appended to an existing session.
|
||||
aiSolutionCards: base.aiSolutionCards,
|
||||
reportCards: [
|
||||
...(existingCards.length ? existingCards : legacySolutionCards(base)),
|
||||
card,
|
||||
],
|
||||
similarCases: solution.similarCases?.length
|
||||
? [...(base.similarCases ?? []), ...solution.similarCases]
|
||||
aiSuggestions: [...(base.aiSuggestions ?? []), ...(card.suggestions ?? [])],
|
||||
similarCases: card.similarCases?.length
|
||||
? [...(base.similarCases ?? []), ...card.similarCases]
|
||||
: base.similarCases,
|
||||
standardJudgment: solution.standards?.length
|
||||
? [...(base.standardJudgment ?? []), ...solution.standards]
|
||||
standardJudgment: card.standards?.length
|
||||
? [...(base.standardJudgment ?? []), ...card.standards]
|
||||
: base.standardJudgment,
|
||||
};
|
||||
sessionStorage.setItem("dieya-risk-report", JSON.stringify(merged));
|
||||
@ -464,9 +542,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
</button>
|
||||
<p class="eyebrow">CHAT TOOL</p>
|
||||
<h2>{{ toolTitle }}</h2>
|
||||
<p class="dialog-description">
|
||||
填写必要信息后,将生成一条带上下文的对话请求。
|
||||
</p>
|
||||
<p class="dialog-description">{{ toolDescription }}</p>
|
||||
<div v-if="tool === 'assessment'" class="assessment-fields">
|
||||
<label
|
||||
>产品品类<input v-model="assessmentFields.category" autofocus
|
||||
@ -499,6 +575,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
v-if="tool !== 'ocr' && tool !== 'voice'"
|
||||
v-model="toolInput"
|
||||
:maxlength="tool === 'cases' ? 50 : 2000"
|
||||
:placeholder="toolPlaceholder"
|
||||
autofocus
|
||||
/>
|
||||
<input
|
||||
@ -517,7 +594,9 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
<button type="button" class="dialog-secondary" @click="closeTool">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" class="dialog-primary">带入对话</button>
|
||||
<button type="submit" class="dialog-primary">
|
||||
{{ toolSubmitLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -54,6 +54,7 @@ export interface RiskAssessmentResult {
|
||||
}>;
|
||||
aiSuggestions?: string[];
|
||||
aiSolutionCards?: AiSolutionReport[];
|
||||
reportCards?: ReportCard[];
|
||||
riskBadge?: string | null;
|
||||
alertText?: string | null;
|
||||
categoryWarning?: {
|
||||
@ -103,6 +104,43 @@ export interface AiSolutionReport {
|
||||
warning?: string | null;
|
||||
}
|
||||
|
||||
export interface ReportCard {
|
||||
type: "supplier" | "material" | "standard" | "compliance" | "solution";
|
||||
title: string;
|
||||
name?: string;
|
||||
partyType?: string;
|
||||
material?: string;
|
||||
category?: string;
|
||||
anomalyCount?: number | null;
|
||||
cooperationCount?: number | null;
|
||||
frequencyCoefficient?: number | null;
|
||||
supplierBaseScore?: number | null;
|
||||
supplierScore?: number | null;
|
||||
tier?: string | null;
|
||||
matchedCount?: number | null;
|
||||
topProblems?: Array<{ name: string; count: number }>;
|
||||
frequentProblems?: Array<{ name: string; count: number }>;
|
||||
standards?: Array<{
|
||||
category: string;
|
||||
riskPoint: string;
|
||||
rule: string;
|
||||
source: string | null;
|
||||
status: string;
|
||||
}>;
|
||||
similarCases?: NonNullable<RiskAssessmentResult["similarCases"]>;
|
||||
confidence?: string | null;
|
||||
analysisBase?: string | null;
|
||||
risks?: Array<{ level: string; title: string; detail: string }>;
|
||||
suggestions?: string[];
|
||||
note?: string | null;
|
||||
warning?: string | null;
|
||||
question?: string | null;
|
||||
detectedValues?: Array<{ name: string; value: string }>;
|
||||
standardFallbackEnabled?: boolean;
|
||||
frequencyBasis?: string | null;
|
||||
dataAvailable?: boolean;
|
||||
}
|
||||
|
||||
export type RiskReportResult = RiskAssessmentResult;
|
||||
|
||||
export interface ModelInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user