dev #1

Merged
kx_guo merged 31 commits from dev into main 2026-09-21 10:50:45 +08:00
4 changed files with 238 additions and 70 deletions
Showing only changes of commit ca9fc9ee9d - Show all commits

View File

@ -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" />生成报告

View File

@ -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;

View File

@ -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>

View File

@ -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 {