1247 lines
30 KiB
Vue
1247 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
AlertTriangle,
|
||
BookOpenCheck,
|
||
Copy,
|
||
ChevronDown,
|
||
ChevronUp,
|
||
Factory,
|
||
FileDown,
|
||
History,
|
||
Sparkles,
|
||
} from "@lucide/vue";
|
||
import { computed, ref } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
|
||
import type { RiskAssessmentResult } from "@/types/risk";
|
||
import { formatRiskLevel } from "@/utils/risk";
|
||
|
||
const props = defineProps<{
|
||
result: RiskAssessmentResult;
|
||
canMonitor?: boolean;
|
||
}>();
|
||
const router = useRouter();
|
||
type SimilarCase = NonNullable<RiskAssessmentResult["similarCases"]>[number];
|
||
const emit = defineEmits<{ generateReport: [] }>();
|
||
const expanded = ref(true);
|
||
const evidenceExpanded = ref(true);
|
||
const feedback = ref("");
|
||
const selectedCase = ref<SimilarCase | null>(null);
|
||
|
||
interface DimensionView {
|
||
name: string;
|
||
score: number;
|
||
weight: string;
|
||
tone: string;
|
||
status?: string;
|
||
reason?: string | null;
|
||
}
|
||
|
||
function notify(label: string) {
|
||
feedback.value = `${label}功能将在对应模块接入后执行`;
|
||
window.setTimeout(() => (feedback.value = ""), 2600);
|
||
}
|
||
|
||
function copyScore() {
|
||
void navigator.clipboard?.writeText(
|
||
`综合安全分:${props.result.score.toFixed(1)},${formatRiskLevel(props.result.level)}`,
|
||
);
|
||
notify("评分已复制");
|
||
}
|
||
|
||
function openCase(item?: SimilarCase) {
|
||
const target = item ?? props.result.similarCases?.[0];
|
||
if (target) selectedCase.value = target;
|
||
else notify("案例详情");
|
||
}
|
||
|
||
const dimensions = computed<DimensionView[]>(() =>
|
||
(props.result.dimensionScores?.length
|
||
? props.result.dimensionScores.map((item) => ({
|
||
dimension: item.dimension,
|
||
score: item.score,
|
||
weight: item.weight,
|
||
status: item.status,
|
||
reason: item.reason,
|
||
}))
|
||
: props.result.factors.map((factor) => ({
|
||
dimension: factor.name,
|
||
score: Math.max(0, Math.round(100 - factor.value)),
|
||
weight: factor.weight,
|
||
status: undefined,
|
||
reason: undefined,
|
||
}))
|
||
).map((item) => ({
|
||
name: item.dimension,
|
||
score: Math.round(item.score),
|
||
weight: `${item.weight}%`,
|
||
tone: item.score < 40 ? "danger" : item.score < 60 ? "warning" : "success",
|
||
status: item.status,
|
||
reason: item.reason,
|
||
})),
|
||
);
|
||
const suggestions = computed(() =>
|
||
props.result.aiSuggestions?.length
|
||
? props.result.aiSuggestions
|
||
: ["高风险贡献指标优先安排专项验证,并明确责任人与完成节点。"],
|
||
);
|
||
const stars = computed(() => {
|
||
const filled = Math.max(1, Math.min(5, Math.round(props.result.score / 20)));
|
||
return "★".repeat(filled) + "☆".repeat(5 - filled);
|
||
});
|
||
const supplierDimension = computed(() =>
|
||
props.result.dimensionScores?.find((item) => item.dimension === "供应商表现"),
|
||
);
|
||
const formulaText = computed(() => {
|
||
const source = props.result.dimensionScores?.length
|
||
? props.result.dimensionScores
|
||
: props.result.factors.map((factor) => ({
|
||
dimension: factor.name,
|
||
score: 100 - factor.value,
|
||
weight: factor.weight,
|
||
}));
|
||
if (!source.length) return "暂无七维评分明细";
|
||
return `${source.map((item) => `${(item.weight / 100).toFixed(2)}×${Math.round(item.score)}`).join(" + ")} ≈ ${Math.round(props.result.score)}`;
|
||
});
|
||
const standardCategories = computed(() => [
|
||
...new Set(
|
||
(props.result.standardJudgment ?? []).map((item) => item.category),
|
||
),
|
||
]);
|
||
function openStandards(category = "all", keyword?: string) {
|
||
void router.push({
|
||
name: "standards",
|
||
query: {
|
||
category,
|
||
...(keyword ? { keyword } : {}),
|
||
},
|
||
});
|
||
}
|
||
|
||
function openSupplier() {
|
||
const supplier = props.result.supplierFreq?.supplierName;
|
||
void router.push({
|
||
name: "supplier",
|
||
query: supplier ? { supplier } : undefined,
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<article class="risk-card" :class="{ 'risk-card--collapsed': !expanded }">
|
||
<header class="risk-summary">
|
||
<span
|
||
class="risk-summary__signal"
|
||
:class="`signal-${result.level}`"
|
||
></span>
|
||
<div>
|
||
<strong :class="`risk-summary__title--${result.level}`">
|
||
{{ stars }}
|
||
{{ formatRiskLevel(result.level) }}
|
||
</strong>
|
||
<small
|
||
>综合评分 <b>{{ Math.round(result.score) }}</b> / 100</small
|
||
>
|
||
</div>
|
||
<span class="risk-action-badge">{{ result.riskBadge || "需关注" }}</span>
|
||
<button
|
||
class="collapse-button"
|
||
type="button"
|
||
@click="expanded = !expanded"
|
||
>
|
||
<ChevronUp v-if="expanded" :size="13" />
|
||
<ChevronDown v-else :size="13" />
|
||
{{ expanded ? "收起" : "展开" }}
|
||
</button>
|
||
<button
|
||
class="collapse-button"
|
||
type="button"
|
||
title="复制评分"
|
||
aria-label="复制评分"
|
||
@click="copyScore"
|
||
>
|
||
<Copy :size="13" />
|
||
</button>
|
||
</header>
|
||
|
||
<template v-if="expanded">
|
||
<section class="alert-strip">
|
||
<AlertTriangle :size="16" />
|
||
<p>
|
||
{{
|
||
result.alertText ||
|
||
"当前暂无足够历史数据,建议按标准库完成基础验证。"
|
||
}}
|
||
</p>
|
||
</section>
|
||
|
||
<section class="category-warning">
|
||
<h3><History :size="14" /> 品类维度风险预警</h3>
|
||
<div v-if="result.categoryWarning">
|
||
<span>{{ result.categoryWarning.category }}</span>
|
||
历史异常 {{ result.categoryWarning.anomalyCount }} 条,
|
||
{{ result.categoryWarning.riskLevel }},案例用于当前风险参考
|
||
</div>
|
||
<div v-if="result.categoryWarning?.suggestion">
|
||
<span>建议</span>{{ result.categoryWarning.suggestion }}
|
||
</div>
|
||
<div v-if="result.categoryWarning?.similarCategory">
|
||
<span>相似: {{ result.categoryWarning.similarCategory }}</span>
|
||
历史异常
|
||
{{ result.categoryWarning.similarCount ?? 0 }}
|
||
条,用于当前品类案例不足时辅助预判
|
||
</div>
|
||
</section>
|
||
|
||
<section class="supplier-analysis">
|
||
<h3><Factory :size="14" /> 供应商合作频次分析</h3>
|
||
<template
|
||
v-if="
|
||
result.supplierFreq?.dataAvailable ||
|
||
result.supplierFreq?.supplierName
|
||
"
|
||
>
|
||
<div v-if="result.supplierFreq.supplierName">
|
||
<span>{{ result.supplierFreq.supplierName }}</span>
|
||
<strong
|
||
>近3个自然年度异常 {{ result.supplierFreq.anomalyCount ?? 0 }} 次</strong
|
||
>
|
||
</div>
|
||
<div v-if="result.supplierFreq.alpha != null">
|
||
<span>频次风险系数</span
|
||
><strong>×{{ result.supplierFreq.alpha.toFixed(2) }}</strong>
|
||
</div>
|
||
<div v-if="supplierDimension">
|
||
<span>供应商得分 S供</span
|
||
><strong>{{ Math.round(supplierDimension.score) }} 分</strong>
|
||
</div>
|
||
<div v-if="result.supplierFreq.tier">
|
||
<span>供应商层级</span
|
||
><strong>{{ result.supplierFreq.tier }}</strong>
|
||
</div>
|
||
<div v-if="result.supplierFreq.cooperationCount != null">
|
||
<span>合作频次(采购订单)</span>
|
||
<strong>{{ result.supplierFreq.cooperationCount }} 次</strong>
|
||
</div>
|
||
<div v-if="result.supplierFreq.suggestion">
|
||
<span>管理建议</span>{{ result.supplierFreq.suggestion }}
|
||
</div>
|
||
<div v-if="supplierDimension?.reason" class="supplier-calculation">
|
||
<span>计算依据</span>{{ supplierDimension.reason }}
|
||
</div>
|
||
</template>
|
||
<p v-else class="supplier-analysis__empty">
|
||
{{
|
||
result.supplierFreq?.note || "未指定供应商,暂无供应商频次分析。"
|
||
}}
|
||
</p>
|
||
<small
|
||
v-if="
|
||
result.supplierFreq?.dataAvailable ||
|
||
result.supplierFreq?.supplierName
|
||
"
|
||
class="supplier-analysis__note"
|
||
>
|
||
{{
|
||
result.supplierFreq.note ||
|
||
"合作频次按自然年度边界采购订单去重单据编号统计;异常数按自然年度边界异常案例库统计。"
|
||
}}
|
||
</small>
|
||
</section>
|
||
|
||
<section class="standards-card">
|
||
<h3><BookOpenCheck :size="14" /> 标准判定 · 双引擎</h3>
|
||
<p v-if="result.standardJudgment?.length" class="standards-card__intro">
|
||
命中判定标准库 {{ result.standardJudgment.length }} 条 · 覆盖
|
||
{{ standardCategories.join(" / ") }} · 来源:《标准文件.xlsx》219 条
|
||
</p>
|
||
<p
|
||
v-if="!result.standardJudgment?.length"
|
||
class="standards-card__empty"
|
||
>
|
||
暂无相关标准
|
||
</p>
|
||
<button
|
||
v-for="standard in result.standardJudgment"
|
||
:key="`${standard.category}-${standard.riskPoint}`"
|
||
class="standard-row"
|
||
type="button"
|
||
@click="openStandards(standard.category, standard.riskPoint)"
|
||
>
|
||
<span
|
||
class="standard-tag"
|
||
:class="{ 'standard-tag--pack': standard.category === '包材' }"
|
||
>{{ standard.category }}</span
|
||
>
|
||
<div>
|
||
{{ standard.riskPoint
|
||
}}<small
|
||
>{{ standard.rule }} ·
|
||
{{ standard.source || "标准文件.xlsx" }}</small
|
||
>
|
||
</div>
|
||
</button>
|
||
<footer>
|
||
<span class="standards-card__engine-icon">↻</span>
|
||
<span
|
||
><b>双引擎融合:</b
|
||
>历史数据(经验引擎)提供风险优先级与相似案例;标准库(规则引擎)给出量化阈值与检测依据。</span
|
||
>
|
||
<span class="standards-card__links">
|
||
<button
|
||
v-for="category in standardCategories"
|
||
:key="category"
|
||
type="button"
|
||
@click="openStandards(category)"
|
||
>
|
||
查看{{ category }}标准
|
||
</button>
|
||
</span>
|
||
</footer>
|
||
</section>
|
||
|
||
<section class="analysis-section">
|
||
<h3>七维风险分析</h3>
|
||
<div class="dimension-grid">
|
||
<div
|
||
v-for="item in dimensions"
|
||
:key="item.name"
|
||
class="dimension-cell"
|
||
>
|
||
<span>{{ item.name }}</span>
|
||
<div class="dimension-meter">
|
||
<i
|
||
:class="`tone-${item.tone}`"
|
||
:style="{ width: `${item.score}%` }"
|
||
></i>
|
||
</div>
|
||
<small
|
||
><b>{{ item.score }}</b
|
||
>分 · {{ item.weight }}
|
||
<em v-if="item.status === 'default'">默认</em></small
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="formula-strip">
|
||
R = {{ formulaText }}({{ formatRiskLevel(result.level) }})
|
||
</div>
|
||
</section>
|
||
|
||
<section class="evidence-panel">
|
||
<button
|
||
type="button"
|
||
class="evidence-panel__head"
|
||
@click="evidenceExpanded = !evidenceExpanded"
|
||
>
|
||
<span>🧠 推理过程(思维链)</span><small>检索→匹配→评分→生成</small>
|
||
<ChevronUp v-if="evidenceExpanded" :size="14" /><ChevronDown
|
||
v-else
|
||
:size="14"
|
||
/>
|
||
</button>
|
||
<div v-if="evidenceExpanded" class="evidence-panel__body">
|
||
<div>
|
||
<b>① 向量检索</b
|
||
><span
|
||
>新品参数嵌入 → 召回历史案例 → 余弦相似度与业务字段混合排序,返回
|
||
Top {{ result.similarCases?.length ?? 0 }} 条。</span
|
||
>
|
||
</div>
|
||
<div>
|
||
<b>② 相似度加权</b
|
||
><span
|
||
>按相似度和时间衰减因子 D(t)
|
||
对候选案例排序,优先展示高相关案例。</span
|
||
>
|
||
</div>
|
||
<div>
|
||
<b>③ 七维评分</b
|
||
><span
|
||
>{{ formulaText }},映射为{{
|
||
formatRiskLevel(result.level)
|
||
}}。</span
|
||
>
|
||
</div>
|
||
<div>
|
||
<b>④ 建议生成</b
|
||
><span
|
||
>聚合相似案例方案,结合标准规则生成
|
||
{{ suggestions.length }} 条可执行建议。</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="analysis-section">
|
||
<h3>
|
||
{{
|
||
result.similarCases?.length
|
||
? `匹配到 ${result.similarCases.length} 条相似案例`
|
||
: "相似案例"
|
||
}}
|
||
</h3>
|
||
<div v-if="!result.similarCases?.length" class="case-empty">
|
||
暂无相关案例,RAG 相似案例检索尚未接入。
|
||
</div>
|
||
<button
|
||
v-for="(item, index) in result.similarCases"
|
||
:key="`${item.date}-${index}`"
|
||
class="case-row"
|
||
type="button"
|
||
@click="openCase(item)"
|
||
>
|
||
<span class="case-rank">TOP {{ index + 1 }}</span>
|
||
<div class="case-content">
|
||
<small>
|
||
{{ item.date || "日期未知" }} ·
|
||
{{ item.product || "未命名产品" }} ·
|
||
{{ item.problem || "异常问题未填写" }} →
|
||
{{ item.solution || "解决方案未填写" }}
|
||
<em v-if="item.similarity != null"
|
||
>相似度 {{ Math.round(item.similarity * 100) }}%</em
|
||
>
|
||
</small>
|
||
</div>
|
||
</button>
|
||
</section>
|
||
|
||
<section class="tips-card">
|
||
<h3><Sparkles :size="14" /> AI 预防建议</h3>
|
||
<ol>
|
||
<li v-for="suggestion in suggestions" :key="suggestion">
|
||
{{ suggestion }}
|
||
</li>
|
||
</ol>
|
||
</section>
|
||
|
||
<div class="risk-actions">
|
||
<button
|
||
class="primary-action"
|
||
type="button"
|
||
@click="emit('generateReport')"
|
||
>
|
||
<FileDown :size="14" /> 生成报告
|
||
</button>
|
||
<button type="button" @click="openCase()">案例详情</button>
|
||
<button type="button" @click="openSupplier">供应商对比</button>
|
||
<button
|
||
v-if="canMonitor"
|
||
type="button"
|
||
disabled
|
||
title="加入监控功能开发中"
|
||
>
|
||
加入监控
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<p v-if="feedback" class="risk-feedback">{{ feedback }}</p>
|
||
<div
|
||
v-if="selectedCase"
|
||
class="case-modal-backdrop"
|
||
@click.self="selectedCase = null"
|
||
>
|
||
<section
|
||
class="case-modal"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="case-detail-title"
|
||
>
|
||
<header class="case-modal__header">
|
||
<div>
|
||
<span class="case-modal__eyebrow">SIMILAR CASE DETAIL</span>
|
||
<h3 id="case-detail-title">案例详情</h3>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="case-modal__close"
|
||
aria-label="关闭案例详情"
|
||
@click="selectedCase = null"
|
||
>
|
||
×
|
||
</button>
|
||
</header>
|
||
<div class="case-modal__summary">
|
||
<strong>{{ selectedCase.product || "未命名产品" }}</strong>
|
||
<span v-if="selectedCase.similarity != null"
|
||
>相似度 {{ Math.round(selectedCase.similarity * 100) }}%</span
|
||
>
|
||
</div>
|
||
<dl class="case-modal__grid">
|
||
<div>
|
||
<dt>发生日期</dt>
|
||
<dd>{{ selectedCase.date || "暂无数据" }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>产品品类</dt>
|
||
<dd>{{ selectedCase.category || "暂无数据" }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>物料类型</dt>
|
||
<dd>{{ selectedCase.material || "暂无数据" }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>缺陷等级</dt>
|
||
<dd>{{ selectedCase.defectLevel || "暂无数据" }}</dd>
|
||
</div>
|
||
<div class="case-modal__field--wide">
|
||
<dt>异常问题</dt>
|
||
<dd>
|
||
{{
|
||
selectedCase.problem || selectedCase.description || "暂无数据"
|
||
}}
|
||
</dd>
|
||
</div>
|
||
<div class="case-modal__field--wide">
|
||
<dt>根因分析</dt>
|
||
<dd>{{ selectedCase.rootCause || "暂无数据" }}</dd>
|
||
</div>
|
||
<div class="case-modal__field--wide">
|
||
<dt>解决方案</dt>
|
||
<dd>{{ selectedCase.solution || "暂无数据" }}</dd>
|
||
</div>
|
||
<div class="case-modal__field--wide">
|
||
<dt>处理结果</dt>
|
||
<dd>{{ selectedCase.result || "暂无数据" }}</dd>
|
||
</div>
|
||
</dl>
|
||
<footer class="case-modal__footer">
|
||
来源:历史异常案例库 · 案例编号:{{
|
||
selectedCase.caseId || "暂无数据"
|
||
}}
|
||
</footer>
|
||
</section>
|
||
</div>
|
||
</article>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.risk-card {
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 14px;
|
||
padding: 20px;
|
||
background: var(--color-surface);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.risk-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 11px;
|
||
border-bottom: 1px solid var(--color-border-light);
|
||
margin-bottom: 14px;
|
||
padding-bottom: 14px;
|
||
}
|
||
|
||
.risk-summary__signal {
|
||
width: 13px;
|
||
height: 13px;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 5px var(--color-warning-soft);
|
||
}
|
||
|
||
.signal-low {
|
||
background: var(--color-success);
|
||
box-shadow: 0 0 0 5px var(--color-success-soft);
|
||
}
|
||
.signal-moderately_low {
|
||
background: var(--color-accent);
|
||
box-shadow: 0 0 0 5px var(--color-accent-subtle);
|
||
}
|
||
.signal-medium {
|
||
background: var(--color-warning);
|
||
}
|
||
.signal-high {
|
||
background: var(--color-danger);
|
||
box-shadow: 0 0 0 5px var(--color-danger-soft);
|
||
}
|
||
.signal-very_high {
|
||
background: #991b1b;
|
||
box-shadow: 0 0 0 5px #fee2e2;
|
||
}
|
||
|
||
.risk-summary > div {
|
||
display: grid;
|
||
flex: 1;
|
||
gap: 1px;
|
||
}
|
||
|
||
.risk-summary strong {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.risk-summary__title--low,
|
||
.risk-summary__title--moderately_low,
|
||
.risk-summary__title--medium,
|
||
.risk-summary__title--high,
|
||
.risk-summary__title--very_high {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.risk-action-badge {
|
||
flex: 0 0 auto;
|
||
border: 1px solid var(--color-warning-border);
|
||
border-radius: 6px;
|
||
padding: 4px 9px;
|
||
background: var(--color-warning-soft);
|
||
color: var(--color-warning);
|
||
font-size: 11px;
|
||
font-weight: 650;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.collapse-button {
|
||
display: inline-flex;
|
||
min-height: 30px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
border: 1px solid var(--color-border-strong);
|
||
border-radius: 6px;
|
||
padding: 0 9px;
|
||
background: var(--color-bg-warm);
|
||
color: var(--color-text-secondary);
|
||
font-size: 11px;
|
||
font-weight: 550;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.collapse-button:hover {
|
||
border-color: var(--color-text-muted);
|
||
background: var(--color-surface);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.collapse-button svg {
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.collapse-button[title="复制评分"] {
|
||
width: 30px;
|
||
padding: 0;
|
||
background: var(--color-surface);
|
||
}
|
||
|
||
.collapse-button[title="复制评分"]:hover {
|
||
border-color: var(--color-accent-soft);
|
||
background: var(--color-accent-subtle);
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.collapse-button[title="复制评分"]:hover svg {
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.risk-summary__title--low,
|
||
.risk-summary__title--moderately_low {
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.risk-summary__title--medium {
|
||
color: var(--color-warning);
|
||
}
|
||
|
||
.risk-summary__title--high,
|
||
.risk-summary__title--very_high {
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
.risk-summary small {
|
||
color: var(--color-text-muted);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.risk-summary small b {
|
||
color: var(--color-warning);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.alert-strip,
|
||
.category-warning,
|
||
.supplier-analysis,
|
||
.standards-card,
|
||
.tips-card {
|
||
border-radius: 10px;
|
||
margin-bottom: 14px;
|
||
padding: 12px 13px;
|
||
}
|
||
|
||
.alert-strip {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 9px;
|
||
border: 1px solid var(--color-danger-border);
|
||
background: var(--color-danger-soft);
|
||
color: #991b1b;
|
||
}
|
||
|
||
.alert-strip svg {
|
||
flex: 0 0 auto;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.alert-strip p {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.category-warning {
|
||
border: 1px solid var(--color-warning-border);
|
||
background: var(--color-warning-soft);
|
||
color: #92400e;
|
||
}
|
||
|
||
.supplier-analysis {
|
||
border: 1px solid #ddd6fe;
|
||
background: var(--color-purple-soft);
|
||
color: #5b21b6;
|
||
}
|
||
|
||
h3 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin: 0 0 7px;
|
||
color: inherit;
|
||
font-size: 12px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.category-warning > div,
|
||
.supplier-analysis > div {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 3px 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.category-warning > div {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.category-warning > div span {
|
||
flex: 0 0 auto;
|
||
border-radius: 999px;
|
||
padding: 1px 7px;
|
||
background: var(--color-warning);
|
||
color: white;
|
||
font-size: 9px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.supplier-analysis strong {
|
||
overflow-wrap: anywhere;
|
||
color: #4c1d95;
|
||
text-align: right;
|
||
}
|
||
.supplier-calculation {
|
||
align-items: flex-start !important;
|
||
margin-top: 5px;
|
||
border-top: 1px dashed #ddd6fe;
|
||
padding-top: 7px !important;
|
||
color: var(--color-text-muted) !important;
|
||
font-size: 10px !important;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.supplier-analysis__empty,
|
||
.supplier-analysis__note {
|
||
display: block;
|
||
margin: 4px 0 0;
|
||
color: #6d5a91;
|
||
font-size: 10px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.standards-card {
|
||
border: 1px solid #bfdbfe;
|
||
}
|
||
|
||
.standards-card h3 {
|
||
color: var(--color-accent-hover);
|
||
}
|
||
|
||
.standards-card__intro {
|
||
margin: 0 0 8px;
|
||
color: var(--color-text-faint);
|
||
font-size: 10px;
|
||
}
|
||
.formula-strip {
|
||
margin-top: 10px;
|
||
border: 1px dashed #bfdbfe;
|
||
border-radius: 5px;
|
||
padding: 8px 10px;
|
||
background: #eff6ff;
|
||
color: #2563eb;
|
||
font-size: 10px;
|
||
line-height: 1.55;
|
||
text-align: center;
|
||
}
|
||
.evidence-panel {
|
||
margin-top: 12px;
|
||
border: 1px solid var(--color-border-light);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.evidence-panel__head {
|
||
display: flex;
|
||
width: 100%;
|
||
align-items: center;
|
||
gap: 8px;
|
||
border: 0;
|
||
padding: 10px 12px;
|
||
background: white;
|
||
color: #111827;
|
||
font: inherit;
|
||
font-size: 11px;
|
||
font-weight: 650;
|
||
border-bottom: 1px solid var(--color-border-light);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
}
|
||
.evidence-panel__head small {
|
||
margin-left: auto;
|
||
color: var(--color-text-muted);
|
||
font-size: 9px;
|
||
font-weight: 400;
|
||
}
|
||
.evidence-panel__body {
|
||
display: grid;
|
||
gap: 0;
|
||
padding: 4px 12px 8px;
|
||
background: white;
|
||
}
|
||
.evidence-panel__body div {
|
||
display: grid;
|
||
grid-template-columns: 112px minmax(0, 1fr);
|
||
gap: 8px;
|
||
border-top: 1px dashed var(--color-border-light);
|
||
padding: 8px 0;
|
||
color: var(--color-text-muted);
|
||
font-size: 10px;
|
||
line-height: 1.55;
|
||
}
|
||
.evidence-panel__body div:first-child {
|
||
border-top: 0;
|
||
}
|
||
.evidence-panel__body b {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.standard-row {
|
||
display: flex;
|
||
width: 100%;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
border-top: 1px dashed var(--color-border-light);
|
||
border-right: 0;
|
||
border-bottom: 0;
|
||
border-left: 0;
|
||
padding: 7px 0;
|
||
background: transparent;
|
||
color: var(--color-text-secondary);
|
||
font-size: 12px;
|
||
font-family: inherit;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
.standard-row:hover {
|
||
background: var(--color-bg-warm);
|
||
}
|
||
|
||
.standard-tag {
|
||
flex: 0 0 auto;
|
||
border: 1px solid var(--color-accent-soft);
|
||
border-radius: 999px;
|
||
padding: 1px 7px;
|
||
background: var(--color-accent-subtle);
|
||
color: var(--color-accent);
|
||
font-size: 9px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.standard-tag--pack {
|
||
border-color: var(--color-success-border);
|
||
background: var(--color-success-soft);
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.standard-row > div {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.standard-row small {
|
||
display: block;
|
||
color: var(--color-text-faint);
|
||
font-size: 9px;
|
||
}
|
||
|
||
.standards-card__empty,
|
||
.case-empty {
|
||
margin: 8px 0;
|
||
color: var(--color-text-faint);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.standards-card footer {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 7px;
|
||
border-radius: 6px;
|
||
margin-top: 7px;
|
||
padding: 7px 9px;
|
||
background: var(--color-bg-warm);
|
||
color: var(--color-text-muted);
|
||
font-size: 10px;
|
||
line-height: 1.6;
|
||
}
|
||
.standards-card footer > span:nth-child(2) {
|
||
flex: 1;
|
||
}
|
||
.standards-card footer b {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
.standards-card footer button {
|
||
flex: 0 0 auto;
|
||
border: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
color: var(--color-accent);
|
||
font: inherit;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
.standards-card footer button:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.standards-card__links {
|
||
display: inline-flex;
|
||
flex: 0 0 auto !important;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 4px 8px;
|
||
}
|
||
.standards-card__engine-icon {
|
||
color: var(--color-accent);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.analysis-section {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.analysis-section > h3 {
|
||
color: var(--color-text);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dimension-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 7px;
|
||
}
|
||
|
||
.dimension-cell {
|
||
display: grid;
|
||
gap: 5px;
|
||
border-radius: 9px;
|
||
padding: 10px;
|
||
background: var(--color-bg-warm);
|
||
text-align: center;
|
||
}
|
||
.dimension-cell:nth-child(1) .dimension-meter i,
|
||
.dimension-cell:nth-child(5) .dimension-meter i,
|
||
.dimension-cell:nth-child(6) .dimension-meter i {
|
||
background: #d97706;
|
||
}
|
||
.dimension-cell:nth-child(2) .dimension-meter i {
|
||
background: #16a34a;
|
||
}
|
||
.dimension-cell:nth-child(3) .dimension-meter i {
|
||
background: #dc2626;
|
||
}
|
||
|
||
.dimension-cell > span {
|
||
color: var(--color-text-muted);
|
||
font-size: 9px;
|
||
font-weight: 550;
|
||
}
|
||
|
||
.dimension-cell small {
|
||
color: var(--color-text-faint);
|
||
font-size: 9px;
|
||
}
|
||
|
||
.dimension-cell small b {
|
||
color: var(--color-text-secondary);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.dimension-meter {
|
||
height: 5px;
|
||
overflow: hidden;
|
||
border-radius: 3px;
|
||
background: var(--color-border);
|
||
}
|
||
|
||
.dimension-meter i {
|
||
display: block;
|
||
height: 100%;
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.tone-warning {
|
||
background: var(--color-warning);
|
||
}
|
||
.tone-danger {
|
||
background: var(--color-danger);
|
||
}
|
||
.tone-success {
|
||
background: var(--color-success);
|
||
}
|
||
|
||
.case-row {
|
||
display: grid;
|
||
width: 100%;
|
||
grid-template-columns: 58px minmax(0, 1fr);
|
||
align-items: start;
|
||
gap: 8px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
margin-bottom: 4px;
|
||
padding: 7px 9px;
|
||
background: var(--color-bg-warm);
|
||
color: var(--color-text-secondary);
|
||
text-align: left;
|
||
}
|
||
|
||
.case-rank {
|
||
display: inline-flex;
|
||
width: 58px;
|
||
min-width: 58px;
|
||
justify-content: flex-start;
|
||
padding-top: 1px;
|
||
color: var(--color-accent);
|
||
font-weight: 650;
|
||
white-space: nowrap;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.case-content {
|
||
min-width: 0;
|
||
}
|
||
|
||
.case-content small {
|
||
display: block;
|
||
color: var(--color-text-faint);
|
||
font-size: 9px;
|
||
line-height: 1.45;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.case-content em {
|
||
margin-left: 6px;
|
||
color: var(--color-accent);
|
||
font-style: normal;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tips-card {
|
||
border: 1px solid var(--color-success-border);
|
||
background: var(--color-success-soft);
|
||
color: #166534;
|
||
}
|
||
|
||
.tips-card ol {
|
||
margin: 0;
|
||
padding-left: 18px;
|
||
}
|
||
|
||
.tips-card li {
|
||
margin: 2px 0;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.risk-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
border-top: 1px solid var(--color-border-light);
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.risk-actions button {
|
||
display: inline-flex;
|
||
min-height: 32px;
|
||
align-items: center;
|
||
gap: 5px;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 999px;
|
||
padding: 0 12px;
|
||
background: var(--color-surface);
|
||
color: var(--color-text-secondary);
|
||
font-size: 11px;
|
||
font-weight: 550;
|
||
}
|
||
|
||
.risk-actions .primary-action {
|
||
border-color: var(--color-text);
|
||
background: var(--color-text);
|
||
color: white;
|
||
}
|
||
.risk-actions button:disabled {
|
||
border-color: var(--color-border-light);
|
||
background: var(--color-bg-warm);
|
||
color: var(--color-text-faint);
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.risk-feedback {
|
||
margin: 8px 0 0;
|
||
color: var(--color-accent);
|
||
font-size: 11px;
|
||
}
|
||
.case-modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 100;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 24px;
|
||
background: rgb(15 23 42 / 48%);
|
||
}
|
||
.case-modal {
|
||
width: min(680px, 100%);
|
||
max-height: min(720px, calc(100vh - 48px));
|
||
overflow-y: auto;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 10px;
|
||
background: var(--color-surface);
|
||
box-shadow: 0 22px 54px rgb(15 23 42 / 22%);
|
||
}
|
||
.case-modal__header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--color-border-light);
|
||
padding: 18px 20px;
|
||
}
|
||
.case-modal__eyebrow {
|
||
color: var(--color-accent);
|
||
font-size: 9px;
|
||
font-weight: 750;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.case-modal__header h3 {
|
||
margin: 4px 0 0;
|
||
font-size: 17px;
|
||
}
|
||
.case-modal__close {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--color-text-muted);
|
||
font-size: 22px;
|
||
cursor: pointer;
|
||
}
|
||
.case-modal__summary {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px 10px;
|
||
}
|
||
.case-modal__summary strong {
|
||
font-size: 15px;
|
||
}
|
||
.case-modal__summary span {
|
||
border-radius: 4px;
|
||
padding: 4px 7px;
|
||
background: var(--color-accent-subtle);
|
||
color: var(--color-accent);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
.case-modal__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin: 0;
|
||
padding: 10px 20px 20px;
|
||
}
|
||
.case-modal__grid div {
|
||
min-width: 0;
|
||
border: 1px solid var(--color-border-light);
|
||
border-radius: 5px;
|
||
padding: 9px 10px;
|
||
}
|
||
.case-modal__grid dt {
|
||
color: var(--color-text-muted);
|
||
font-size: 10px;
|
||
}
|
||
.case-modal__grid dd {
|
||
margin: 4px 0 0;
|
||
color: var(--color-text-secondary);
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
white-space: pre-wrap;
|
||
}
|
||
.case-modal__field--wide {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.case-modal__footer {
|
||
border-top: 1px solid var(--color-border-light);
|
||
padding: 10px 20px 14px;
|
||
color: var(--color-text-faint);
|
||
font-size: 10px;
|
||
}
|
||
@media (max-width: 560px) {
|
||
.case-modal-backdrop {
|
||
align-items: end;
|
||
padding: 0;
|
||
}
|
||
.case-modal {
|
||
max-height: 90vh;
|
||
border-radius: 10px 10px 0 0;
|
||
}
|
||
.case-modal__grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.case-modal__field--wide {
|
||
grid-column: auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.risk-card {
|
||
padding: 14px;
|
||
}
|
||
|
||
.dimension-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.case-row {
|
||
grid-template-columns: 50px minmax(0, 1fr);
|
||
}
|
||
|
||
.case-rank {
|
||
width: 50px;
|
||
min-width: 50px;
|
||
}
|
||
|
||
.case-content small {
|
||
white-space: normal;
|
||
}
|
||
}
|
||
</style>
|