kx_guo 32739944c7 优化风险卡片与历史会话加载
- 展示数据库七维风险维度、默认值和数据说明\n- 增加标准兜底校验卡片及无历史案例提示\n- 支持 AI 风险建议和案例推理卡片的新数据结构\n- 进入会话默认定位最新内容\n- 上划到顶部自动加载更早的 20 条消息并保持阅读位置\n- 移除手动加载更早消息按钮\n- 更新风险评估表单和前端类型定义
2026-09-04 18:02:39 +08:00

565 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {
AlertTriangle,
BarChart3,
BookOpenCheck,
Bot,
Building2,
CheckCircle2,
ClipboardCheck,
PackageSearch,
Ruler,
Sparkles,
} from "@lucide/vue";
import { computed, ref } from "vue";
import type { AssistantResponseCard } from "@/types/conversation";
const props = defineProps<{ card: AssistantResponseCard }>();
const feedback = ref("");
interface CardData extends AssistantResponseCard {
name?: string;
partyType?: string;
material?: string;
anomalyCount?: number;
cooperationCount?: number | null;
frequencyCoefficient?: number;
tier?: string;
topProblems?: Array<{ name: string; count: number }>;
frequentProblems?: Array<{ name: string; count: number }>;
suggestions?: string[];
note?: string;
detectedValues?: Array<{ name: string; value: string }>;
matchedCount?: number;
standards?: Array<{
category: string;
riskPoint: string;
rule: string;
source?: string | null;
status: string;
}>;
confidence?: string;
analysisBase?: string;
risks?: Array<{ level: string; title: string; detail: string }>;
warning?: string;
}
const data = computed(() => props.card as CardData);
function notify(label: string) {
feedback.value = `${label}功能将在对应模块接入后执行`;
window.setTimeout(() => (feedback.value = ""), 2400);
}
function formatCount(value: unknown) {
return typeof value === "number" ? value.toLocaleString("zh-CN") : "暂无数据";
}
</script>
<template>
<article class="response-card" :class="`response-card--${card.type}`">
<header class="response-card__header">
<span class="response-card__icon">
<Building2 v-if="card.type === 'supplier'" :size="16" />
<PackageSearch v-else-if="card.type === 'material'" :size="16" />
<BookOpenCheck v-else-if="card.type === 'standard'" :size="16" />
<ClipboardCheck v-else-if="card.type === 'compliance'" :size="16" />
<Bot v-else :size="16" />
</span>
<div>
<h3>{{ card.title }}</h3>
<small v-if="card.type === 'supplier'"
>{{ data.name }} · {{ data.partyType }}</small
>
<small v-else-if="card.type === 'material'"
>{{ data.material }} · 历史异常分析</small
>
<small v-else-if="card.type === 'standard'"
>{{ data.category }} · 规则引擎</small
>
<small v-else-if="card.type === 'solution'"
>AI 推理生成 · 置信度:{{ data.confidence }}</small
>
<small v-else>标准库规则匹配 · 待品控确认</small>
</div>
</header>
<template v-if="card.type === 'supplier'">
<div class="metric-grid">
<div>
<b>{{ formatCount(data.anomalyCount) }}</b
><span>历史异常</span>
</div>
<div>
<b>{{ formatCount(data.cooperationCount) }}</b
><span>合作次数</span>
</div>
<div>
<b>×{{ data.frequencyCoefficient }}</b
><span>频次系数</span>
</div>
</div>
<div class="profile-line">
<span>供应商层级</span><strong>{{ data.tier }}</strong>
</div>
<section class="result-section">
<h4><BarChart3 :size="14" /> 高频异常</h4>
<div v-if="data.topProblems?.length" class="problem-list">
<div v-for="problem in data.topProblems" :key="problem.name">
<span>{{ problem.name }}</span
><b>{{ problem.count }} 次</b>
</div>
</div>
<p v-else class="empty">暂无匹配的异常类型</p>
</section>
<ul class="suggestion-list">
<li v-for="suggestion in data.suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ul>
<p class="card-note">{{ data.note }}</p>
</template>
<template v-else-if="card.type === 'material'">
<div class="material-summary">
<b>{{ formatCount(data.anomalyCount) }}</b
><span>条历史异常涉及该物料</span>
</div>
<section class="result-section">
<h4><BarChart3 :size="14" /> 常见问题</h4>
<div v-if="data.frequentProblems?.length" class="problem-list">
<div v-for="problem in data.frequentProblems" :key="problem.name">
<span>{{ problem.name }}</span
><b>{{ problem.count }} 次</b>
</div>
</div>
<p v-else class="empty">暂无匹配历史异常</p>
</section>
<section v-if="data.standards?.length" class="result-section">
<h4><BookOpenCheck :size="14" /> 关联标准</h4>
<div
v-for="standard in data.standards"
:key="standard.riskPoint"
class="standard-line"
>
<span>{{ standard.category }}</span>
<p>
{{ standard.riskPoint }}<small>{{ standard.source }}</small>
</p>
</div>
</section>
<ul class="suggestion-list">
<li v-for="suggestion in data.suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ul>
</template>
<template
v-else-if="card.type === 'standard' || card.type === 'compliance'"
>
<div
v-if="card.type === 'standard' && data.detectedValues?.length"
class="detected-values"
>
<span
v-for="item in data.detectedValues"
:key="`${item.name}-${item.value}`"
>{{ item.name }} <b>{{ item.value }}</b></span
>
</div>
<div class="standard-count">
<b>{{ data.matchedCount }}</b> 条规则已匹配
</div>
<section class="standard-results">
<template v-if="data.standards?.length">
<div
v-for="standard in data.standards"
:key="`${standard.category}-${standard.riskPoint}`"
class="standard-row"
>
<span>{{ standard.category }}</span>
<div>
<b>{{ standard.riskPoint }}</b
><small>{{ standard.rule }} · {{ standard.source }}</small>
</div>
<strong>{{ standard.status }}</strong>
</div>
</template>
<p v-else class="empty">暂无直接匹配规则</p>
</section>
<ul v-if="data.suggestions" class="suggestion-list">
<li v-for="suggestion in data.suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ul>
<p v-if="data.note" class="card-note">{{ data.note }}</p>
</template>
<template v-else>
<div class="solution-base"><b>分析基础:</b>{{ data.analysisBase }}</div>
<section
v-if="data.standardFallbackEnabled || data.standards?.length"
class="solution-section"
>
<h4>
<Ruler :size="14" />
<span v-if="data.standardFallbackEnabled"
>标准兜底校验(无历史案例时启用)</span
>
<span v-else>关联标准校验</span>
</h4>
<div
v-for="standard in data.standards"
:key="`${standard.category}-${standard.riskPoint}`"
class="standard-row"
>
<span>{{ standard.category }}</span>
<div>
<b>{{ standard.riskPoint }}</b
><small>{{ standard.rule }} · {{ standard.source }}</small>
</div>
<strong>{{ standard.status }}</strong>
</div>
<p v-if="!data.standards?.length" class="empty">
当前标准库暂无可匹配规则,请补充产品品类、包材或检测指标。
</p>
</section>
<section v-if="data.risks?.length" class="solution-section risk-list">
<div v-for="risk in data.risks" :key="risk.title" class="solution-risk">
<span>{{ risk.level }}</span>
<p>
<b>{{ risk.title }}:</b>{{ risk.detail }}
</p>
</div>
</section>
<section class="solution-suggest">
<h4><Sparkles :size="14" /> 建议方案(AI 生成,需品控确认)</h4>
<ol>
<li v-for="suggestion in data.suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ol>
</section>
<div class="solution-warning">
<AlertTriangle :size="14" />{{ data.warning }}
</div>
</template>
<div class="response-actions">
<button
type="button"
class="response-actions__primary"
@click="notify('生成报告')"
>
<CheckCircle2 :size="14" />生成报告
</button>
<button type="button" @click="notify('查看详情')">查看详情</button>
<button type="button" @click="notify('追问详情')">追问详情</button>
</div>
<p v-if="feedback" class="response-feedback">{{ feedback }}</p>
</article>
</template>
<style scoped>
.response-card {
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 18px;
background: var(--color-surface);
box-shadow: var(--shadow-md);
}
.response-card--supplier {
border-left: 3px solid var(--color-success);
}
.response-card--material {
border-left: 3px solid var(--color-warning);
}
.response-card--standard,
.response-card--compliance {
border-left: 3px solid var(--color-accent);
}
.response-card--solution {
border: 1px solid #bfdbfe;
border-left: 3px solid var(--color-purple);
}
.response-card__header {
display: flex;
align-items: center;
gap: 9px;
border-bottom: 1px solid var(--color-border-light);
padding-bottom: 12px;
}
.response-card__icon {
display: grid;
width: 30px;
height: 30px;
place-items: center;
border-radius: 6px;
background: var(--color-bg-warm);
color: var(--color-accent);
}
.response-card--supplier .response-card__icon {
color: var(--color-success);
}
.response-card--material .response-card__icon {
color: var(--color-warning);
}
.response-card--solution .response-card__icon {
color: var(--color-purple);
}
.response-card__header h3 {
margin: 0;
font-size: 14px;
}
.response-card__header small {
color: var(--color-text-muted);
font-size: 10px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 7px;
margin: 14px 0 10px;
}
.metric-grid > div {
border-radius: 6px;
padding: 10px;
background: var(--color-bg-warm);
}
.metric-grid b,
.metric-grid span {
display: block;
}
.metric-grid b {
color: var(--color-text);
font-size: 17px;
}
.metric-grid span {
margin-top: 3px;
color: var(--color-text-faint);
font-size: 10px;
}
.profile-line,
.material-summary,
.standard-count {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 9px 10px;
background: var(--color-success-soft);
color: var(--color-text-secondary);
font-size: 11px;
}
.profile-line strong {
color: var(--color-success);
}
.material-summary {
justify-content: flex-start;
margin-top: 14px;
background: var(--color-warning-soft);
}
.material-summary b {
color: var(--color-warning);
font-size: 17px;
}
.result-section,
.solution-section {
margin-top: 14px;
}
.result-section h4,
.solution-section h4,
.solution-suggest h4 {
display: flex;
align-items: center;
gap: 5px;
margin: 0 0 7px;
color: var(--color-text);
font-size: 12px;
}
.problem-list > div,
.standard-line,
.standard-row {
display: flex;
align-items: flex-start;
gap: 8px;
border-top: 1px dashed var(--color-border-light);
padding: 8px 0;
font-size: 11px;
}
.problem-list > div {
justify-content: space-between;
}
.problem-list b {
color: var(--color-danger);
}
.empty {
margin: 7px 0;
color: var(--color-text-faint);
font-size: 11px;
}
.standard-line > span,
.standard-row > span {
flex: 0 0 auto;
border-radius: 999px;
padding: 2px 6px;
background: var(--color-accent-subtle);
color: var(--color-accent);
font-size: 9px;
}
.standard-line p,
.standard-row div {
min-width: 0;
flex: 1;
margin: 0;
color: var(--color-text-secondary);
}
.standard-line small,
.standard-row small {
display: block;
margin-top: 3px;
color: var(--color-text-faint);
font-size: 9px;
}
.standard-row > strong {
flex: 0 0 auto;
color: var(--color-warning);
font-size: 10px;
}
.suggestion-list {
margin: 12px 0 0;
padding-left: 18px;
color: var(--color-text-secondary);
}
.suggestion-list li {
margin: 3px 0;
font-size: 11px;
line-height: 1.55;
}
.card-note {
margin: 10px 0 0;
color: var(--color-text-faint);
font-size: 10px;
line-height: 1.55;
}
.detected-values {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 14px 0 10px;
}
.detected-values span {
border: 1px solid var(--color-accent-soft);
border-radius: 5px;
padding: 5px 8px;
background: var(--color-accent-subtle);
color: var(--color-accent);
font-size: 11px;
}
.standard-count {
justify-content: flex-start;
margin-top: 14px;
background: var(--color-accent-subtle);
}
.standard-count b {
color: var(--color-accent);
font-size: 17px;
}
.standard-results {
margin-top: 5px;
}
.solution-base {
margin-top: 14px;
border-radius: 6px;
padding: 10px;
background: #f3f7fb;
color: var(--color-text-secondary);
font-size: 11px;
line-height: 1.6;
}
.solution-suggest {
margin-top: 14px;
border-radius: 6px;
padding: 11px;
background: #eff6ff;
}
.solution-suggest h4 {
color: var(--color-accent);
}
.solution-suggest ol {
margin: 0;
padding-left: 18px;
color: var(--color-text-secondary);
}
.solution-suggest li {
margin: 4px 0;
font-size: 11px;
line-height: 1.55;
}
.solution-risk {
display: flex;
gap: 7px;
border-top: 1px solid var(--color-border-light);
padding: 8px 0;
}
.solution-risk > span {
flex: 0 0 auto;
border-radius: 4px;
padding: 3px 5px;
background: var(--color-warning-soft);
color: var(--color-warning);
font-size: 9px;
}
.solution-risk p {
margin: 0;
color: var(--color-text-secondary);
font-size: 11px;
line-height: 1.5;
}
.solution-warning {
display: flex;
gap: 6px;
margin-top: 10px;
border-radius: 5px;
padding: 8px 10px;
background: var(--color-warning-soft);
color: #92400e;
font-size: 10px;
line-height: 1.5;
}
.response-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 14px;
border-top: 1px solid var(--color-border-light);
padding-top: 12px;
}
.response-actions button {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 30px;
border: 1px solid var(--color-border);
border-radius: 5px;
padding: 0 10px;
background: var(--color-surface);
color: var(--color-text-secondary);
font-size: 10px;
}
.response-actions__primary {
border-color: var(--color-text) !important;
background: var(--color-text) !important;
color: white !important;
}
.response-feedback {
margin: 8px 0 0;
color: var(--color-accent);
font-size: 10px;
}
@media (max-width: 600px) {
.metric-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>