595 lines
16 KiB
Vue
595 lines
16 KiB
Vue
<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;
|
||
frequencyBasis?: string;
|
||
dataAvailable?: boolean;
|
||
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 }>;
|
||
similarCases?: Array<{
|
||
date?: string | null;
|
||
product?: string | null;
|
||
problem?: string | null;
|
||
solution?: string | null;
|
||
similarity?: number | null;
|
||
}>;
|
||
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>近3年异常</span>
|
||
</div>
|
||
<div v-if="data.cooperationCount != null">
|
||
<b>{{ formatCount(data.cooperationCount) }}</b
|
||
><span>合作频次(采购订单)</span>
|
||
</div>
|
||
<div>
|
||
<b>×{{ data.frequencyCoefficient?.toFixed(2) ?? "1.00" }}</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.similarCases?.length" class="solution-section">
|
||
<h4>相似历史案例</h4>
|
||
<div
|
||
v-for="(item, index) in data.similarCases"
|
||
:key="`${item.date}-${index}`"
|
||
class="standard-row"
|
||
>
|
||
<span>TOP {{ index + 1 }}</span>
|
||
<div>
|
||
<b>{{ item.product || "未命名产品" }}</b>
|
||
<small
|
||
>{{ item.problem || "异常问题未填写" }} →
|
||
{{ item.solution || "解决方案未填写" }}
|
||
<em v-if="item.similarity != null"
|
||
>相似度 {{ Math.round(item.similarity * 100) }}%</em
|
||
></small
|
||
>
|
||
</div>
|
||
</div>
|
||
</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>
|