1247 lines
30 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,
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>