feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
<template>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<section class="dashboard">
|
|
|
|
|
|
<div class="kpi-grid">
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
<article
|
2026-04-29 23:36:18 +08:00
|
|
|
|
v-for="metric in kpiMetrics"
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
:key="metric.label"
|
2026-04-29 23:36:18 +08:00
|
|
|
|
class="kpi-card panel"
|
|
|
|
|
|
:style="{ '--accent': metric.accent }"
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<div class="kpi-top">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p>{{ metric.label }}</p>
|
|
|
|
|
|
<strong>{{ metric.displayValue }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kpi-icon">
|
|
|
|
|
|
<i :class="metric.icon"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kpi-bottom" :class="metric.trend">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<i :class="metric.trend === 'down' ? 'pi pi-arrow-down' : 'pi pi-arrow-up'"></i>
|
|
|
|
|
|
{{ metric.changeText }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<small>{{ metric.delta }}</small>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<div class="content-grid top-grid">
|
|
|
|
|
|
<article class="panel dashboard-card trend-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>报销申请与审批趋势 <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
<select v-model="activeTrendRange" class="card-select" aria-label="趋势时间范围">
|
|
|
|
|
|
<option v-for="range in trendRanges" :key="range">{{ range }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<TrendChart
|
|
|
|
|
|
:labels="activeTrend.labels"
|
|
|
|
|
|
:applications="activeTrend.applications"
|
|
|
|
|
|
:approved="activeTrend.approved"
|
|
|
|
|
|
:avg-hours="activeTrend.avgHours"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel dashboard-card donut-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>费用结构 <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="donut-wrap">
|
|
|
|
|
|
<div class="donut" :style="{ background: spendDonutBackground }">
|
|
|
|
|
|
<div class="donut-center">
|
|
|
|
|
|
<strong>{{ spendTotalLabel }}</strong>
|
|
|
|
|
|
<span>待处理金额</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="donut-legend">
|
|
|
|
|
|
<div v-for="item in spendLegend" :key="item.name" class="legend-row">
|
|
|
|
|
|
<span><i :style="{ background: item.color }"></i>{{ item.name }}</span>
|
|
|
|
|
|
</div>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<p class="panel-note">* 百分比为占待处理金额比例</p>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<article class="panel dashboard-card donut-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>风险异常分布 <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="donut-wrap">
|
|
|
|
|
|
<div class="donut" :style="{ background: riskDonutBackground }">
|
|
|
|
|
|
<div class="donut-center">
|
|
|
|
|
|
<strong>{{ riskTotal }}</strong>
|
|
|
|
|
|
<span>异常预警单</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="donut-legend">
|
|
|
|
|
|
<div v-for="item in riskLegend" :key="item.name" class="legend-row">
|
|
|
|
|
|
<span><i :style="{ background: item.color }"></i>{{ item.name }}</span>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<p class="panel-note">* 近30天数据</p>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-29 23:36:18 +08:00
|
|
|
|
<div class="content-grid bottom-grid">
|
|
|
|
|
|
<article class="panel dashboard-card rank-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>部门报销排行(待处理金额) <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
<select v-model="activeDepartmentRange" class="card-select" aria-label="部门排行时间范围">
|
|
|
|
|
|
<option v-for="range in departmentRangeOptions" :key="range">{{ range }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rank-list">
|
|
|
|
|
|
<div v-for="item in rankedDepartments" :key="item.name" class="rank-row">
|
|
|
|
|
|
<span class="rank-index">{{ item.rank }}</span>
|
|
|
|
|
|
<span class="rank-name">{{ item.shortName }}</span>
|
|
|
|
|
|
<div class="rank-track">
|
|
|
|
|
|
<div class="rank-fill" :style="{ width: item.width, background: item.color }"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<strong class="rank-value">{{ item.amountLabel }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel dashboard-card bottleneck-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>审批瓶颈(平均处理时长) <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="bottleneck-list">
|
|
|
|
|
|
<div v-for="item in bottlenecks" :key="item.name" class="bottleneck-row">
|
|
|
|
|
|
<div class="reviewer">
|
|
|
|
|
|
<div class="reviewer-avatar">{{ item.avatar }}</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>{{ item.name }}</strong>
|
|
|
|
|
|
<span>{{ item.role }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="reviewer-stats">
|
|
|
|
|
|
<strong>{{ item.duration }}</strong>
|
|
|
|
|
|
<span class="status-tag" :class="item.tone">{{ item.status }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="text-link">查看全部 <i class="pi pi-angle-right"></i></button>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel dashboard-card budget-panel">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h3>预算执行率(本月) <i class="pi pi-info-circle"></i></h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="budget-gauge" :style="{ '--budget-ratio': `${budgetSummary.ratio}` }">
|
|
|
|
|
|
<div class="budget-gauge-ring"></div>
|
|
|
|
|
|
<div class="budget-gauge-center">
|
|
|
|
|
|
<strong>{{ budgetSummary.ratio }}%</strong>
|
|
|
|
|
|
<span>已执行</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="budget-summary">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>预算总额</span>
|
|
|
|
|
|
<strong>{{ budgetSummary.total }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>已执行</span>
|
|
|
|
|
|
<strong>{{ budgetSummary.used }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>剩余可用</span>
|
|
|
|
|
|
<strong>{{ budgetSummary.left }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="text-link">查看详情 <i class="pi pi-angle-right"></i></button>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
metricBlueprints,
|
|
|
|
|
|
trendRanges,
|
|
|
|
|
|
trendSeries,
|
|
|
|
|
|
spendByCategory,
|
|
|
|
|
|
exceptionMix,
|
|
|
|
|
|
departmentRangeOptions,
|
|
|
|
|
|
bottlenecks,
|
|
|
|
|
|
budgetSummary
|
|
|
|
|
|
} from '../data/metrics.js'
|
|
|
|
|
|
import TrendChart from '../components/charts/TrendChart.vue'
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
|
|
|
|
|
|
defineProps({
|
|
|
|
|
|
filteredRequests: { type: Array, required: true }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-29 23:36:18 +08:00
|
|
|
|
const activeTrendRange = ref(trendRanges[0])
|
|
|
|
|
|
const activeDepartmentRange = ref(departmentRangeOptions[0])
|
|
|
|
|
|
|
|
|
|
|
|
const demoTotals = {
|
|
|
|
|
|
pendingCount: 128,
|
|
|
|
|
|
pendingAmount: 361600,
|
|
|
|
|
|
avgSla: 6.8,
|
|
|
|
|
|
autoPassRate: 78,
|
|
|
|
|
|
riskCount: 14,
|
|
|
|
|
|
slaRate: 96
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const demoDepartments = [
|
|
|
|
|
|
{ name: '销售部', amount: 182000, color: '#10b981' },
|
|
|
|
|
|
{ name: '研发中心', amount: 146000, color: '#3b82f6' },
|
|
|
|
|
|
{ name: '市场部', amount: 96000, color: '#f59e0b' },
|
|
|
|
|
|
{ name: '运营部', amount: 68600, color: '#8b5cf6' },
|
|
|
|
|
|
{ name: '行政部', amount: 48300, color: '#3b82f6' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const formatCompact = (value) => {
|
|
|
|
|
|
if (value >= 1_000_000) return `¥${(value / 1_000_000).toFixed(1)}M`
|
|
|
|
|
|
if (value >= 1_000) return `¥${(value / 1_000).toFixed(1)}K`
|
|
|
|
|
|
return `¥${value}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (value) => formatCompact(value)
|
|
|
|
|
|
|
|
|
|
|
|
const kpiMetrics = computed(() => metricBlueprints.map((metric) => {
|
|
|
|
|
|
const rawValue = demoTotals[metric.key]
|
|
|
|
|
|
const displayValue = metric.key === 'pendingAmount'
|
|
|
|
|
|
? formatCurrency(rawValue)
|
|
|
|
|
|
: metric.unit && !String(rawValue).endsWith(metric.unit)
|
|
|
|
|
|
? `${rawValue} ${metric.unit}`
|
|
|
|
|
|
: `${rawValue}`
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...metric,
|
|
|
|
|
|
displayValue,
|
|
|
|
|
|
changeText: metric.change
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const activeTrend = computed(() => trendSeries[activeTrendRange.value])
|
|
|
|
|
|
|
|
|
|
|
|
const spendTotal = computed(() => spendByCategory.reduce((sum, item) => sum + item.value, 0))
|
|
|
|
|
|
const spendTotalLabel = computed(() => formatCurrency(demoTotals.pendingAmount))
|
|
|
|
|
|
|
|
|
|
|
|
const buildDonutBackground = (items, total) => {
|
|
|
|
|
|
let current = 0
|
|
|
|
|
|
const stops = items.map((item) => {
|
|
|
|
|
|
const start = current
|
|
|
|
|
|
current += (item.value / total) * 100
|
|
|
|
|
|
return `${item.color} ${start}% ${current}%`
|
|
|
|
|
|
}).join(',')
|
|
|
|
|
|
|
|
|
|
|
|
return `radial-gradient(circle, #fff 0 60%, transparent 61%), conic-gradient(${stops})`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const spendDonutBackground = computed(() => buildDonutBackground(spendByCategory, spendTotal.value))
|
|
|
|
|
|
const riskTotal = computed(() => exceptionMix.reduce((sum, item) => sum + item.value, 0))
|
|
|
|
|
|
const riskDonutBackground = computed(() => buildDonutBackground(exceptionMix, riskTotal.value))
|
|
|
|
|
|
|
|
|
|
|
|
const spendLegend = computed(() => spendByCategory.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
display: `${Math.round((item.value / spendTotal.value) * 100)}%`
|
|
|
|
|
|
})))
|
|
|
|
|
|
|
|
|
|
|
|
const riskLegend = computed(() => exceptionMix.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
display: `${item.value} 单`
|
|
|
|
|
|
})))
|
|
|
|
|
|
|
|
|
|
|
|
const rankedDepartments = computed(() => {
|
|
|
|
|
|
const rows = demoDepartments
|
|
|
|
|
|
const max = Math.max(...rows.map((item) => item.amount), 1)
|
|
|
|
|
|
|
|
|
|
|
|
return rows.slice(0, 5).map((item, index) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
rank: index + 1,
|
|
|
|
|
|
shortName: item.name,
|
|
|
|
|
|
amountLabel: formatCurrency(item.amount),
|
|
|
|
|
|
width: `${Math.max((item.amount / max) * 100, 18)}%`,
|
|
|
|
|
|
color: item.color
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-04-29 23:36:18 +08:00
|
|
|
|
.dashboard {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
animation: fadeUp 260ms var(--ease) both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-card {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-top {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-top > div {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-top p {
|
|
|
|
|
|
margin: 0 0 6px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-top strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-size: clamp(20px, 2vw, 26px);
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-icon {
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: color-mix(in srgb, var(--accent) 10%, white);
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-bottom {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 1px;
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
|
border-top: 1px solid #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-bottom span {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-bottom.up span {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-bottom.down span {
|
|
|
|
|
|
color: #16a34a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kpi-bottom small {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dashboard-card {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trend-panel,
|
|
|
|
|
|
.rank-panel {
|
|
|
|
|
|
grid-column: span 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-panel,
|
|
|
|
|
|
.bottleneck-panel,
|
|
|
|
|
|
.budget-panel {
|
|
|
|
|
|
grid-column: span 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-head h3 {
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-head .pi {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
vertical-align: 1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-select {
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
min-width: 82px;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border: 1px solid #cbd5e1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-wrap {
|
|
|
|
|
|
min-height: 192px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(116px, 1fr) minmax(96px, .8fr);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut {
|
|
|
|
|
|
width: min(148px, 100%);
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
justify-self: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-center {
|
|
|
|
|
|
width: 84px;
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
align-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-center strong {
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-center span {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-legend {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
align-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-row span {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-row i {
|
|
|
|
|
|
width: 10px;
|
|
|
|
|
|
height: 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-note {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-list,
|
|
|
|
|
|
.bottleneck-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 24px 72px minmax(0, 1fr) auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-index {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #94a3b8;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-row:first-child .rank-index {
|
|
|
|
|
|
background: #f59e0b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-name {
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-value {
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-track {
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border-radius: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottleneck-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reviewer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reviewer-avatar {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #e2f6ef;
|
|
|
|
|
|
color: #047857;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reviewer strong,
|
|
|
|
|
|
.reviewer-stats strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reviewer span,
|
|
|
|
|
|
.reviewer-stats span {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reviewer-stats {
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tag.danger {
|
|
|
|
|
|
background: rgba(239,68,68,.10);
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tag.warning {
|
|
|
|
|
|
background: rgba(245,158,11,.10);
|
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tag.success {
|
|
|
|
|
|
background: rgba(16,185,129,.10);
|
|
|
|
|
|
color: #16a34a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.text-link {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 16px 0 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-top: 1px solid #f1f5f9;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #10b981;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-gauge {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 128px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-gauge-ring {
|
|
|
|
|
|
width: 172px;
|
|
|
|
|
|
height: 92px;
|
|
|
|
|
|
border-radius: 172px 172px 0 0;
|
|
|
|
|
|
background: conic-gradient(from 180deg, #10b981 0 calc(var(--budget-ratio) * 1.8deg), #e2e8f0 calc(var(--budget-ratio) * 1.8deg) 180deg);
|
|
|
|
|
|
-webkit-mask: radial-gradient(circle at 50% 100%, transparent 0 52px, #000 53px);
|
|
|
|
|
|
mask: radial-gradient(circle at 50% 100%, transparent 0 52px, #000 53px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-gauge-center {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: auto 0 10px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-gauge-center strong {
|
|
|
|
|
|
color: #10b981;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-gauge-center span {
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-summary {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-summary span {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-summary strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1320px) {
|
|
|
|
|
|
.kpi-grid {
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trend-panel,
|
|
|
|
|
|
.rank-panel {
|
|
|
|
|
|
grid-column: span 12;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-panel,
|
|
|
|
|
|
.bottleneck-panel,
|
|
|
|
|
|
.budget-panel {
|
|
|
|
|
|
grid-column: span 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 860px) {
|
|
|
|
|
|
.kpi-grid,
|
|
|
|
|
|
.content-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trend-panel,
|
|
|
|
|
|
.rank-panel,
|
|
|
|
|
|
.donut-panel,
|
|
|
|
|
|
.bottleneck-panel,
|
|
|
|
|
|
.budget-panel {
|
|
|
|
|
|
grid-column: span 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-head {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.donut-wrap {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-row {
|
|
|
|
|
|
grid-template-columns: 24px 64px minmax(0, 1fr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rank-value {
|
|
|
|
|
|
grid-column: 2 / -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.budget-summary {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
</style>
|