feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
708
web/src/components/dashboard/RiskObservationDashboard.vue
Normal file
708
web/src/components/dashboard/RiskObservationDashboard.vue
Normal file
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<section class="risk-observation-dashboard">
|
||||
<article class="panel dashboard-card risk-trend-panel">
|
||||
<div class="card-head">
|
||||
<h3>风险观察趋势 <i class="mdi mdi-information-outline"></i></h3>
|
||||
<div class="risk-window-controls">
|
||||
<span class="risk-window-label">近 {{ dashboard.windowDays }} 天</span>
|
||||
<EnterpriseSelect
|
||||
class="risk-window-select"
|
||||
:model-value="activeWindowDays"
|
||||
:options="windowOptions"
|
||||
size="small"
|
||||
aria-label="风险看板时间窗口"
|
||||
@update:model-value="emit('update:windowDays', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="risk-dashboard-state">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>正在加载风险看板数据</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="risk-dashboard-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
<RiskDailyTrendChart v-else :rows="dailyRows" />
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-level-panel">
|
||||
<div class="card-head">
|
||||
<h3>风险等级分布 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<DonutChart
|
||||
:items="levelLegend"
|
||||
:center-value="String(dashboard.totalObservations)"
|
||||
center-label="风险观察"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-source-panel">
|
||||
<div class="card-head">
|
||||
<h3>来源分布 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<DonutChart
|
||||
:items="sourceLegend"
|
||||
:center-value="String(dashboard.totalObservations)"
|
||||
center-label="归集来源"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-dimension-panel">
|
||||
<div class="card-head">
|
||||
<h3>业务维度分布 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div class="risk-dimension-grid">
|
||||
<section
|
||||
v-for="group in dimensionGroups"
|
||||
:key="group.label"
|
||||
class="risk-dimension-group"
|
||||
>
|
||||
<span class="risk-dimension-title">{{ group.label }}</span>
|
||||
<div v-if="group.rows.length" class="risk-dimension-list">
|
||||
<div v-for="row in group.rows" :key="row.name" class="risk-dimension-row">
|
||||
<span>{{ row.name }}</span>
|
||||
<i><b :style="{ width: row.width }"></b></i>
|
||||
<strong>{{ row.count }}项</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="risk-dashboard-inline-empty">暂无数据</p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-signal-panel">
|
||||
<div class="card-head">
|
||||
<h3>风险信号排行 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<BarChart
|
||||
v-if="signalRanking.length"
|
||||
:items="signalRanking"
|
||||
value-prefix=""
|
||||
value-suffix="项"
|
||||
/>
|
||||
<div v-else class="risk-dashboard-empty">
|
||||
<i class="mdi mdi-shield-check-outline"></i>
|
||||
<span>当前周期暂无风险信号</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-ranking-panel">
|
||||
<div class="card-head">
|
||||
<h3>异常排行 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div class="risk-ranking-grid">
|
||||
<section
|
||||
v-for="group in rankingGroups"
|
||||
:key="group.label"
|
||||
class="risk-ranking-group"
|
||||
>
|
||||
<span class="risk-ranking-title">{{ group.label }}</span>
|
||||
<ol v-if="group.rows.length" class="risk-ranking-list">
|
||||
<li v-for="(row, index) in group.rows" :key="`${group.label}-${row.name}`">
|
||||
<em>{{ index + 1 }}</em>
|
||||
<div>
|
||||
<strong>{{ row.name }}</strong>
|
||||
<small>{{ row.amountLabel }}</small>
|
||||
</div>
|
||||
<span>{{ row.count }}项</span>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="risk-dashboard-inline-empty">暂无数据</p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-effect-panel">
|
||||
<div class="card-head">
|
||||
<h3>算法闭环效果 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div class="risk-effect-grid">
|
||||
<div v-for="item in effectItems" :key="item.label" class="risk-effect-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card risk-recent-panel">
|
||||
<div class="card-head">
|
||||
<h3>近期高风险观察 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div v-if="recentHighObservations.length" class="risk-recent-list">
|
||||
<button
|
||||
v-for="item in recentHighObservations"
|
||||
:key="item.observationKey || item.id"
|
||||
class="risk-recent-row"
|
||||
type="button"
|
||||
:disabled="!item.claimId"
|
||||
@click="openClaim(item)"
|
||||
>
|
||||
<span class="risk-recent-level" :class="item.riskLevel">
|
||||
{{ formatRiskLevel(item.riskLevel) }}
|
||||
</span>
|
||||
<span class="risk-recent-main">
|
||||
<strong>{{ item.title || formatSignal(item.riskSignal) }}</strong>
|
||||
<small>{{ item.claimNo || item.claimId || '未关联单据' }}</small>
|
||||
</span>
|
||||
<span class="risk-recent-score">{{ item.riskScore }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="risk-dashboard-empty">
|
||||
<i class="mdi mdi-check-circle-outline"></i>
|
||||
<span>暂无高风险观察</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import BarChart from '../charts/BarChart.vue'
|
||||
import DonutChart from '../charts/DonutChart.vue'
|
||||
import RiskDailyTrendChart from '../charts/RiskDailyTrendChart.vue'
|
||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||
|
||||
const props = defineProps({
|
||||
dashboard: { type: Object, required: true },
|
||||
loading: { type: Boolean, default: false },
|
||||
error: { type: Object, default: () => null },
|
||||
levelLegend: { type: Array, default: () => [] },
|
||||
sourceLegend: { type: Array, default: () => [] },
|
||||
signalRanking: { type: Array, default: () => [] },
|
||||
dailyRows: { type: Array, default: () => [] },
|
||||
windowOptions: { type: Array, default: () => [] },
|
||||
activeWindowDays: { type: Number, default: 30 }
|
||||
})
|
||||
const emit = defineEmits(['update:windowDays'])
|
||||
|
||||
const router = useRouter()
|
||||
const errorMessage = computed(() => props.error?.message || '风险看板数据加载失败')
|
||||
const recentHighObservations = computed(() => props.dashboard.recentHighObservations || [])
|
||||
const dimensionGroups = computed(() => [
|
||||
buildDimensionGroup('部门', props.dashboard.departmentDistribution, 'department'),
|
||||
buildDimensionGroup('费用类型', props.dashboard.expenseTypeDistribution, 'expense_type'),
|
||||
buildDimensionGroup('风险类型', props.dashboard.riskTypeDistribution, 'risk_type'),
|
||||
buildDimensionGroup('供应商', props.dashboard.supplierDistribution, 'supplier'),
|
||||
buildDimensionGroup('员工职级', props.dashboard.employeeGradeDistribution, 'grade')
|
||||
])
|
||||
const rankingGroups = computed(() => [
|
||||
buildRankingGroup('部门', props.dashboard.topDepartments, 'department'),
|
||||
buildRankingGroup('员工', props.dashboard.topEmployees, 'employee'),
|
||||
buildRankingGroup('供应商', props.dashboard.topSuppliers, 'supplier'),
|
||||
buildRankingGroup('规则', props.dashboard.topRules, 'rule'),
|
||||
buildRankingGroup('费用类型', props.dashboard.topExpenseTypes, 'expense_type')
|
||||
])
|
||||
const effectItems = computed(() => {
|
||||
const sourceDistribution = props.dashboard.sourceDistribution || {}
|
||||
const total = Number(props.dashboard.totalObservations || 0)
|
||||
const pending = Number(props.dashboard.pendingCount || 0)
|
||||
const processedRate = total > 0 ? Math.max(0, (total - pending) / total) : 0
|
||||
|
||||
return [
|
||||
{ label: '规则命中', value: sourceDistribution.rule_center || 0 },
|
||||
{ label: '图谱异常', value: sourceDistribution.financial_risk_graph || 0 },
|
||||
{ label: '确认率', value: formatPercent(props.dashboard.confirmationRate) },
|
||||
{ label: '误报率', value: formatPercent(props.dashboard.falsePositiveRate) },
|
||||
{ label: '候选规则', value: props.dashboard.candidateRuleCount || 0 },
|
||||
{ label: '完成率', value: formatPercent(processedRate) }
|
||||
]
|
||||
})
|
||||
|
||||
function buildDimensionGroup(label, distribution = {}, kind = '') {
|
||||
const rows = Object.entries(distribution || {})
|
||||
.map(([name, count]) => ({
|
||||
name: formatDimensionName(name, kind),
|
||||
count: Number(count || 0)
|
||||
}))
|
||||
.filter((item) => item.count > 0)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 4)
|
||||
const max = Math.max(...rows.map((item) => item.count), 1)
|
||||
|
||||
return {
|
||||
label,
|
||||
rows: rows.map((item) => ({
|
||||
...item,
|
||||
width: `${Math.max((item.count / max) * 100, 10)}%`
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function buildRankingGroup(label, rows = [], kind = '') {
|
||||
return {
|
||||
label,
|
||||
rows: (Array.isArray(rows) ? rows : [])
|
||||
.map((item) => ({
|
||||
name: formatDimensionName(item.name, kind),
|
||||
count: Number(item.count || 0),
|
||||
amount: Number(item.amount || 0),
|
||||
amountLabel: formatAmount(item.amount)
|
||||
}))
|
||||
.filter((item) => item.count > 0)
|
||||
.slice(0, 5)
|
||||
}
|
||||
}
|
||||
|
||||
function formatAmount(value) {
|
||||
const amount = Number(value || 0)
|
||||
if (amount >= 1_000_000) return `¥${(amount / 1_000_000).toFixed(1)}M`
|
||||
if (amount >= 1_000) return `¥${(amount / 1_000).toFixed(1)}K`
|
||||
return `¥${Math.round(amount)}`
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
return `${Math.round(Number(value || 0) * 100)}%`
|
||||
}
|
||||
|
||||
function formatRiskLevel(value) {
|
||||
const labels = {
|
||||
critical: '重大',
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '未知'
|
||||
}
|
||||
|
||||
function formatDimensionName(value, kind = '') {
|
||||
const text = String(value || '').trim()
|
||||
if (!text || text === 'unknown') {
|
||||
const unknownLabels = {
|
||||
department: '未归集部门',
|
||||
expense_type: '未归集费用',
|
||||
risk_type: '未知风险类型',
|
||||
supplier: '未归集供应商',
|
||||
grade: '未归集职级',
|
||||
employee: '未归集员工',
|
||||
rule: '未关联规则'
|
||||
}
|
||||
return unknownLabels[kind] || '未知'
|
||||
}
|
||||
if (kind === 'risk_type' || kind === 'expense_type' || kind === 'rule') {
|
||||
return formatSignal(text)
|
||||
}
|
||||
return text.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
function formatSignal(value) {
|
||||
const text = String(value || '').trim()
|
||||
const labels = {
|
||||
duplicate_invoice: '重复发票',
|
||||
split_billing: '拆分报销',
|
||||
frequent_small_claims: '高频小额',
|
||||
location_mismatch: '地点不一致',
|
||||
amount_outlier: '金额异常',
|
||||
preapproval_absent: '缺少事前申请'
|
||||
}
|
||||
return labels[text] || text.replace(/_/g, ' ') || '未知风险'
|
||||
}
|
||||
|
||||
function openClaim(item) {
|
||||
if (!item.claimId) {
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: 'app-document-detail',
|
||||
params: { requestId: item.claimId }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.risk-observation-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
border: 1px solid #edf2f7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-head h3 {
|
||||
min-width: 0;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-head .mdi {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.risk-window-label {
|
||||
flex: 0 0 auto;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-window-controls {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-window-select {
|
||||
width: 108px;
|
||||
}
|
||||
|
||||
.risk-trend-panel,
|
||||
.risk-signal-panel,
|
||||
.risk-dimension-panel,
|
||||
.risk-ranking-panel {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.risk-level-panel,
|
||||
.risk-source-panel,
|
||||
.risk-effect-panel,
|
||||
.risk-recent-panel {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.risk-dashboard-state,
|
||||
.risk-dashboard-empty {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.risk-dashboard-state i,
|
||||
.risk-dashboard-empty i {
|
||||
color: #94a3b8;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.risk-dashboard-state.error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.risk-dashboard-inline-empty {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-dimension-grid,
|
||||
.risk-ranking-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.risk-dimension-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.risk-ranking-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.risk-dimension-group,
|
||||
.risk-ranking-group {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 4px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.risk-dimension-title,
|
||||
.risk-ranking-title {
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.risk-dimension-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-dimension-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-dimension-row span,
|
||||
.risk-dimension-row strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-dimension-row strong {
|
||||
color: #0f172a;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.risk-dimension-row i {
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.risk-dimension-row b {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--theme-primary);
|
||||
}
|
||||
|
||||
.risk-ranking-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.risk-ranking-list li {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr) 38px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-ranking-list em {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.risk-ranking-list div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.risk-ranking-list strong,
|
||||
.risk-ranking-list small,
|
||||
.risk-ranking-list span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-ranking-list strong {
|
||||
color: #0f172a;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.risk-ranking-list small {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-ranking-list span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.risk-effect-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.risk-effect-item {
|
||||
min-height: 84px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 4px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.risk-effect-item span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-effect-item strong {
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.risk-recent-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-recent-row {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.risk-recent-row:hover:not(:disabled) {
|
||||
border-color: rgba(var(--theme-primary-rgb), .26);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.risk-recent-row:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.risk-recent-level {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
border-radius: 4px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.risk-recent-level.critical,
|
||||
.risk-recent-level.high {
|
||||
background: rgba(239, 68, 68, .1);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.risk-recent-level.medium {
|
||||
background: rgba(245, 158, 11, .12);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.risk-recent-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.risk-recent-main strong,
|
||||
.risk-recent-main small {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-recent-main strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.risk-recent-main small {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-recent-score {
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.risk-trend-panel,
|
||||
.risk-signal-panel,
|
||||
.risk-dimension-panel,
|
||||
.risk-ranking-panel {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.risk-level-panel,
|
||||
.risk-source-panel,
|
||||
.risk-effect-panel,
|
||||
.risk-recent-panel {
|
||||
grid-column: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.risk-observation-dashboard {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.risk-trend-panel,
|
||||
.risk-signal-panel,
|
||||
.risk-dimension-panel,
|
||||
.risk-ranking-panel,
|
||||
.risk-level-panel,
|
||||
.risk-source-panel,
|
||||
.risk-effect-panel,
|
||||
.risk-recent-panel {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.risk-dimension-grid,
|
||||
.risk-ranking-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user