|
|
@@ -1,137 +1,476 @@
|
|
|
<template>
|
|
|
- <div class="page-container">
|
|
|
- <el-row :gutter="16" class="stat-row">
|
|
|
- <el-col :span="6">
|
|
|
- <div class="page-card stat-card">
|
|
|
- <div class="stat-card__label">项目总数</div>
|
|
|
- <div class="stat-card__value">{{ data.projectCount ?? '-' }}</div>
|
|
|
+ <div class="dash">
|
|
|
+ <!-- ===== 标题区(对齐原型区块②) ===== -->
|
|
|
+ <div class="page-head">
|
|
|
+ <div>
|
|
|
+ <div class="crumb">资源中枢 <b>/</b> <b>资源总览</b></div>
|
|
|
+ <h1>资源总览</h1>
|
|
|
+ </div>
|
|
|
+ <div class="head-tools">
|
|
|
+ <div class="seg" role="group" aria-label="时间范围">
|
|
|
+ <button :class="{ active: range === 'month' }" @click="range = 'month'">本月</button>
|
|
|
+ <button :class="{ active: range === 'quarter' }" @click="range = 'quarter'">本季</button>
|
|
|
+ <button :class="{ active: range === 'year' }" @click="range = 'year'">本年</button>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="page-card stat-card">
|
|
|
- <div class="stat-card__label">资源总数</div>
|
|
|
- <div class="stat-card__value">{{ data.resourceCount ?? '-' }}</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="page-card stat-card">
|
|
|
- <div class="stat-card__label">即将到期</div>
|
|
|
- <div class="stat-card__value" style="color: var(--color-warning)">{{ data.expiringCount ?? '-' }}</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="page-card stat-card">
|
|
|
- <div class="stat-card__label">已过期</div>
|
|
|
- <div class="stat-card__value" style="color: var(--color-danger)">{{ data.expiredCount ?? '-' }}</div>
|
|
|
+ <button class="btn" @click="onExport">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M7 10l5 5 5-5"/><path d="M5 21h14"/></svg>
|
|
|
+ 导出报表
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ===== KPI 指标卡(区块③:6 卡,全部真实派生) ===== -->
|
|
|
+ <section class="kpi-grid">
|
|
|
+ <article class="kpi" v-for="k in kpis" :key="k.label">
|
|
|
+ <div class="k-label">{{ k.label }}</div>
|
|
|
+ <div class="k-value mono-num" :style="k.color ? { color: k.color } : null">{{ k.value }}</div>
|
|
|
+ <div class="k-cap" :class="k.capClass">{{ k.caption }}</div>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ===== 图表区:项目状态环形图 + 资源类型分布条(区块④⑤) ===== -->
|
|
|
+ <section class="charts-row">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <h2>项目状态分布</h2>
|
|
|
+ <span class="hint">共 {{ totalProjects }} 个项目</span>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
-
|
|
|
- <el-row :gutter="16" style="margin-top: 16px">
|
|
|
- <el-col :span="16">
|
|
|
- <div class="page-card">
|
|
|
- <div class="section-title">需要关注的资源(即将到期 / 已过期)</div>
|
|
|
- <el-table :data="data.attentionResources" size="default" empty-text="暂无需要关注的资源">
|
|
|
- <el-table-column prop="name" label="资源名称" min-width="140" />
|
|
|
- <el-table-column prop="typeName" label="类型" width="110" />
|
|
|
- <el-table-column prop="ownerName" label="负责人" width="90" />
|
|
|
- <el-table-column prop="expireDate" label="到期日期" width="110" class-name="mono-num" />
|
|
|
- <el-table-column label="状态" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <span :class="['status-badge', statusClass(row.status)]">{{ row.statusText }}</span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <div class="donut-wrap">
|
|
|
+ <div ref="statusChart" class="chart-donut"></div>
|
|
|
+ <ul class="legend">
|
|
|
+ <li v-for="s in projectStatus" :key="s.name">
|
|
|
+ <span class="dot" :style="{ background: s.colorVar }"></span>
|
|
|
+ <span class="lg-label">{{ s.name }}</span>
|
|
|
+ <span class="lg-val mono-num">{{ s.value }}</span>
|
|
|
+ <span class="lg-pct mono-num">{{ pct(s.value, totalProjects) }}%</span>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="page-card">
|
|
|
- <div class="section-title">资源类型分布</div>
|
|
|
- <div v-for="(count, name) in data.typeDistribution" :key="name" class="dist-row">
|
|
|
- <span class="dist-row__name">{{ name }}</span>
|
|
|
- <div class="dist-row__bar-bg">
|
|
|
- <div class="dist-row__bar" :style="{ width: barWidth(count) + '%' }"></div>
|
|
|
- </div>
|
|
|
- <span class="dist-row__count">{{ count }}</span>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <h2>资源类型分布</h2>
|
|
|
+ <span class="hint">共 {{ typeList.length }} 类 · {{ data.resourceCount ?? 0 }} 个资源</span>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ <div ref="typeChart" class="chart-bars"></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ===== 近期项目动态 / 风险清单(区块⑥) ===== -->
|
|
|
+ <section class="card table-card">
|
|
|
+ <div class="card-head">
|
|
|
+ <h2>近期项目动态 / 风险清单</h2>
|
|
|
+ <span class="hint">点击表头排序</span>
|
|
|
+ </div>
|
|
|
+ <el-table :data="projects" size="default" empty-text="暂无项目">
|
|
|
+ <el-table-column label="项目名" min-width="180" prop="name" sortable>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" @click="goDetail(row.id)">{{ row.name }}</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="ownerName" label="负责人" width="110" />
|
|
|
+ <el-table-column label="状态" width="120" prop="status" sortable>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="['status-badge', row.status === 1 ? 'status-badge--success' : 'status-badge--disabled']">
|
|
|
+ {{ row.status === 1 ? '进行中' : '已下线' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="resourceCount" label="关联资源数" width="130" header-align="right" align="right" sortable>
|
|
|
+ <template #default="{ row }"><span class="mono-num">{{ row.resourceCount }}</span></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="风险标记" width="130">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="riskSet.has(row.name)" class="status-badge status-badge--danger">风险</span>
|
|
|
+ <span v-else class="status-badge status-badge--success">正常</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, computed } from 'vue'
|
|
|
+import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
import { dashboardApi } from '@/api/dashboard'
|
|
|
-import { resourceStatusClass } from '@/utils/status'
|
|
|
+import { projectPageApi } from '@/api/project'
|
|
|
+import { useChart } from '@/composables/useChart'
|
|
|
+import { downloadCsv } from '@/utils/exportCsv'
|
|
|
|
|
|
+const router = useRouter()
|
|
|
const data = ref({})
|
|
|
+const projects = ref([])
|
|
|
+const range = ref('month')
|
|
|
+// 时间窗口:本月/本季/本年 → 30/90/365 天(用于基于真实到期日过滤)
|
|
|
+const RANGE_DAYS = { month: 30, quarter: 90, year: 365 }
|
|
|
+const windowDays = computed(() => RANGE_DAYS[range.value] || 30)
|
|
|
+const statusChart = ref(null)
|
|
|
+const typeChart = ref(null)
|
|
|
|
|
|
-const maxCount = computed(() => {
|
|
|
- const values = Object.values(data.value.typeDistribution || {})
|
|
|
- return values.length ? Math.max(...values) : 1
|
|
|
+const totalProjects = computed(() => projects.value.length)
|
|
|
+const typeList = computed(() =>
|
|
|
+ Object.entries(data.value.typeDistribution || {})
|
|
|
+ .map(([name, value]) => ({ name, value }))
|
|
|
+ .sort((a, b) => b.value - a.value)
|
|
|
+)
|
|
|
+
|
|
|
+// 由 attentionResources 的真实到期日(daysToExpire)按时间窗口计算临期/过期与风险项目
|
|
|
+const expiryStats = computed(() => {
|
|
|
+ const list = data.value.attentionResources || []
|
|
|
+ const risk = new Set()
|
|
|
+ let expiring = 0
|
|
|
+ let expired = 0
|
|
|
+ list.forEach((r) => {
|
|
|
+ const d = typeof r.daysToExpire === 'number' ? r.daysToExpire : null
|
|
|
+ if (d == null) return
|
|
|
+ if (d <= 0) expired++
|
|
|
+ else if (d <= windowDays.value) expiring++
|
|
|
+ if (d <= windowDays.value) {
|
|
|
+ ;(r.projects || []).forEach((p) => risk.add(p.name))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return { expiring, expired, risk }
|
|
|
})
|
|
|
+const riskSet = computed(() => expiryStats.value.risk)
|
|
|
+
|
|
|
+const projectStatus = ref([])
|
|
|
|
|
|
-function barWidth(count) {
|
|
|
- return Math.max(6, Math.round((count / maxCount.value) * 100))
|
|
|
+const kpis = computed(() => {
|
|
|
+ const d = data.value
|
|
|
+ const ongoing = projectStatus.value.find((s) => s.name === '进行中')?.value || 0
|
|
|
+ const offline = projectStatus.value.find((s) => s.name === '已下线')?.value || 0
|
|
|
+ const types = typeList.value
|
|
|
+ const topType = types[0]
|
|
|
+ return [
|
|
|
+ { label: '项目总数', value: d.projectCount ?? '-', caption: `进行中 ${ongoing} · 已下线 ${offline}` },
|
|
|
+ { label: '资源总数', value: d.resourceCount ?? '-', caption: `覆盖 ${types.length} 种类型` },
|
|
|
+ { label: '进行中项目', value: ongoing, caption: totalProjects.value ? `占比 ${Math.round((ongoing / totalProjects.value) * 100)}%` : '—' },
|
|
|
+ { label: '资源类型数', value: types.length, caption: topType ? `${topType.name} 最多(${topType.value})` : '—' },
|
|
|
+ { label: '即将到期', value: expiryStats.value.expiring, color: 'var(--color-warning)', caption: `未来 ${windowDays.value} 天 · ${expiryStats.value.expiring} 个待续费` },
|
|
|
+ { label: '已过期', value: expiryStats.value.expired, color: 'var(--color-danger)', caption: `近 ${windowDays.value} 天 · ${expiryStats.value.expired} 个已过期` }
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+function pct(v, total) {
|
|
|
+ if (!total) return 0
|
|
|
+ return Math.round((v / total) * 100)
|
|
|
}
|
|
|
|
|
|
-function statusClass(status) {
|
|
|
- return resourceStatusClass(status)
|
|
|
+function goDetail(id) {
|
|
|
+ router.push(`/project/${id}`)
|
|
|
}
|
|
|
|
|
|
+function onExport() {
|
|
|
+ const rows = [['项目名', '负责人', '状态', '关联资源数', '风险标记']]
|
|
|
+ projects.value.forEach((p) => {
|
|
|
+ rows.push([
|
|
|
+ p.name,
|
|
|
+ p.ownerName || '-',
|
|
|
+ p.status === 1 ? '进行中' : '已下线',
|
|
|
+ p.resourceCount,
|
|
|
+ riskSet.value.has(p.name) ? '风险' : '正常'
|
|
|
+ ])
|
|
|
+ })
|
|
|
+ downloadCsv(`资源总览-项目清单-${range.value}.csv`, rows)
|
|
|
+}
|
|
|
+
|
|
|
+// —— 环形图:项目状态分布 ——
|
|
|
+const { render: renderStatus } = useChart(statusChart, (c) => ({
|
|
|
+ tooltip: { trigger: 'item', formatter: '{b}: {c} 个 ({d}%)' },
|
|
|
+ title: {
|
|
|
+ text: String(totalProjects.value),
|
|
|
+ subtext: '个项目',
|
|
|
+ left: 'center',
|
|
|
+ top: '38%',
|
|
|
+ textAlign: 'center',
|
|
|
+ itemGap: 4,
|
|
|
+ textStyle: { fontSize: 28, fontWeight: 700, color: c.text },
|
|
|
+ subtextStyle: { fontSize: 12, color: c.sub }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['58%', '80%'],
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ label: { show: false },
|
|
|
+ labelLine: { show: false },
|
|
|
+ data: projectStatus.value.map((s) => ({
|
|
|
+ name: s.name,
|
|
|
+ value: s.value,
|
|
|
+ itemStyle: { color: c.palette[s.cidx] }
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}))
|
|
|
+
|
|
|
+// —— 条形图:资源类型分布 ——
|
|
|
+const { render: renderType } = useChart(typeChart, (c) => {
|
|
|
+ const items = typeList.value
|
|
|
+ return {
|
|
|
+ grid: { left: 96, right: 48, top: 8, bottom: 8 },
|
|
|
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}: {c} 个' },
|
|
|
+ xAxis: { type: 'value', show: false },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: items.map((i) => i.name),
|
|
|
+ inverse: true,
|
|
|
+ axisLine: { show: false },
|
|
|
+ axisTick: { show: false },
|
|
|
+ axisLabel: { color: c.sub, fontSize: 12 }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: items.map((i) => i.value),
|
|
|
+ barWidth: 12,
|
|
|
+ itemStyle: { color: c.primary, borderRadius: [0, 6, 6, 0] },
|
|
|
+ label: { show: true, position: 'right', color: c.sub, fontSize: 12, fontFamily: 'var(--font-mono)' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
- const res = await dashboardApi()
|
|
|
- data.value = res.data
|
|
|
+ const [dash, proj] = await Promise.all([dashboardApi(), projectPageApi({ pageNum: 1, pageSize: 100 })])
|
|
|
+ data.value = dash.data
|
|
|
+ const list = (proj.data.records || []).slice()
|
|
|
+ projects.value = list
|
|
|
+
|
|
|
+ // 项目状态聚合(真实)
|
|
|
+ const ongoing = list.filter((p) => p.status === 1).length
|
|
|
+ const offline = list.filter((p) => p.status === 0).length
|
|
|
+ projectStatus.value = [
|
|
|
+ { name: '进行中', value: ongoing, colorVar: 'var(--c1)', cidx: 0 },
|
|
|
+ { name: '已下线', value: offline, colorVar: 'var(--c8)', cidx: 7 }
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 数据就绪后刷新图表
|
|
|
+ renderStatus()
|
|
|
+ renderType()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.stat-card__label {
|
|
|
+.page-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+.page-head h1 {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 650;
|
|
|
+ line-height: 1.3;
|
|
|
+}
|
|
|
+.crumb {
|
|
|
font-size: 13px;
|
|
|
color: var(--color-text-secondary);
|
|
|
- margin-bottom: 8px;
|
|
|
+ margin-bottom: 4px;
|
|
|
}
|
|
|
-.stat-card__value {
|
|
|
- font-family: var(--font-mono);
|
|
|
- font-size: 28px;
|
|
|
- font-weight: 600;
|
|
|
+.crumb b {
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.head-tools {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 时间范围分段控件 */
|
|
|
+.seg {
|
|
|
+ display: inline-flex;
|
|
|
+ padding: 3px;
|
|
|
+ gap: 2px;
|
|
|
+ background: var(--color-surface-2);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: var(--radius-sm);
|
|
|
+}
|
|
|
+.seg button {
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 5px 14px;
|
|
|
+ border-radius: var(--radius-xs);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.15s, color 0.15s;
|
|
|
+}
|
|
|
+.seg button:hover {
|
|
|
color: var(--color-text-primary);
|
|
|
}
|
|
|
-.section-title {
|
|
|
- font-size: 14px;
|
|
|
+.seg button.active {
|
|
|
+ background: var(--color-surface);
|
|
|
+ color: var(--color-primary);
|
|
|
font-weight: 600;
|
|
|
- margin-bottom: 16px;
|
|
|
+ box-shadow: var(--shadow-card);
|
|
|
+}
|
|
|
+
|
|
|
+.btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ height: 34px;
|
|
|
+ padding: 0 14px;
|
|
|
+ border-radius: var(--radius-sm);
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ background: var(--color-surface);
|
|
|
color: var(--color-text-primary);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.15s, border-color 0.15s;
|
|
|
+}
|
|
|
+.btn:hover {
|
|
|
+ background: var(--color-surface-2);
|
|
|
+}
|
|
|
+.btn svg {
|
|
|
+ width: 15px;
|
|
|
+ height: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+/* KPI 卡 */
|
|
|
+.kpi-grid {
|
|
|
+ display: grid;
|
|
|
+ gap: 16px;
|
|
|
+ grid-template-columns: repeat(6, 1fr);
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+.kpi {
|
|
|
+ background: var(--color-surface);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: var(--radius-md);
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: var(--shadow-card);
|
|
|
+ transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
|
}
|
|
|
-.dist-row {
|
|
|
+.kpi:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ border-color: var(--color-border-strong);
|
|
|
+ box-shadow: var(--shadow-md);
|
|
|
+}
|
|
|
+.k-label {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ letter-spacing: 0.3px;
|
|
|
+}
|
|
|
+.k-value {
|
|
|
+ font-size: 30px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.1;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+.k-cap {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表区 */
|
|
|
+.charts-row {
|
|
|
+ display: grid;
|
|
|
+ gap: 16px;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+.card {
|
|
|
+ background: var(--color-surface);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: var(--radius-md);
|
|
|
+ padding: 24px;
|
|
|
+ box-shadow: var(--shadow-card);
|
|
|
+}
|
|
|
+.card-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 12px;
|
|
|
- font-size: 13px;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.card-head h2 {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
-.dist-row__name {
|
|
|
- width: 80px;
|
|
|
+.card-head .hint {
|
|
|
+ font-size: 12px;
|
|
|
color: var(--color-text-secondary);
|
|
|
+}
|
|
|
+
|
|
|
+.donut-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+.chart-donut {
|
|
|
+ width: 180px;
|
|
|
+ height: 180px;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
-.dist-row__bar-bg {
|
|
|
+.chart-bars {
|
|
|
+ width: 100%;
|
|
|
+ height: 300px;
|
|
|
+}
|
|
|
+.legend {
|
|
|
+ list-style: none;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
flex: 1;
|
|
|
- height: 8px;
|
|
|
- background: var(--color-bg);
|
|
|
- border-radius: 4px;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.dist-row__bar {
|
|
|
- height: 100%;
|
|
|
- background: var(--color-primary-light);
|
|
|
- border-radius: 4px;
|
|
|
-}
|
|
|
-.dist-row__count {
|
|
|
- width: 28px;
|
|
|
- text-align: right;
|
|
|
- font-family: var(--font-mono);
|
|
|
+ min-width: 160px;
|
|
|
+}
|
|
|
+.legend li {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.legend .dot {
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 3px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.legend .lg-label {
|
|
|
color: var(--color-text-secondary);
|
|
|
}
|
|
|
+.legend .lg-val {
|
|
|
+ margin-left: auto;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.legend .lg-pct {
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ width: 44px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+.table-card {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式 */
|
|
|
+@media (max-width: 1279px) {
|
|
|
+ .kpi-grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+@media (max-width: 1023px) {
|
|
|
+ .charts-row {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ .kpi-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+@media (max-width: 639px) {
|
|
|
+ .kpi-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|