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
This commit is contained in:
94
src/views/OverviewView.vue
Normal file
94
src/views/OverviewView.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<section class="view">
|
||||
<div class="metric-strip">
|
||||
<article
|
||||
v-for="(metric, index) in metrics"
|
||||
:key="metric.label"
|
||||
v-motion
|
||||
class="metric"
|
||||
:initial="{ opacity: 0, y: 16 }"
|
||||
:enter="{ opacity: 1, y: 0, transition: { delay: index * 0.07, duration: 0.36 } }"
|
||||
:style="{ '--accent': metric.color }"
|
||||
>
|
||||
<div class="metric-top">
|
||||
<span>{{ metric.label }}</span>
|
||||
<b :class="metric.tone">{{ metric.delta }}</b>
|
||||
</div>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<small>{{ metric.note }}</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="overview-grid">
|
||||
<article class="panel spend-panel">
|
||||
<PanelHead eyebrow="Category spend" title="费用类型月度支出" note="用企业报销常见科目展示本月费用压力。" />
|
||||
<div class="bar-chart" role="img" aria-label="费用类型月度支出柱状图">
|
||||
<div v-for="item in spendByCategory" :key="item.name" class="bar-row">
|
||||
<span>{{ item.name }}</span>
|
||||
<div class="bar-track"><div class="bar-fill" :style="{ width: item.width, background: item.color }"></div></div>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<PanelHead eyebrow="Compliance mix" title="审核结论占比" note="把自动通过、需补件和高风险分层展示。" />
|
||||
<div class="donut-layout">
|
||||
<div class="donut" aria-label="审核结论环形图"><strong>68%</strong><span>可自动处理</span></div>
|
||||
<div class="legend">
|
||||
<div v-for="item in auditMix" :key="item.name" class="legend-row">
|
||||
<i :style="{ background: item.color }"></i><span>{{ item.name }}</span><strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<RequestTable :requests="filteredRequests" @ask="emit('ask', $event)" @approve="emit('approve', $event)" @reject="emit('reject', $event)" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PanelHead from '../components/shared/PanelHead.vue'
|
||||
import RequestTable from '../components/business/RequestTable.vue'
|
||||
import { metrics, spendByCategory, auditMix } from '../data/metrics.js'
|
||||
|
||||
defineProps({
|
||||
filteredRequests: { type: Array, required: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['ask', 'approve', 'reject'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view { display: grid; gap: 22px; animation: fadeUp 220ms var(--ease) both; }
|
||||
.metric-strip { display: grid; grid-template-columns: repeat(4, minmax(190px, 1fr)); gap: 16px; }
|
||||
.metric {
|
||||
min-height: 128px; padding: 18px; border-top: 3px solid var(--accent);
|
||||
border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
|
||||
box-shadow: 0 1px 2px rgba(16,24,40,.04);
|
||||
transition: transform 220ms var(--ease), box-shadow 220ms var(--ease), border-color 220ms var(--ease);
|
||||
}
|
||||
.metric:hover { transform: translateY(-2px); border-color: rgba(51,92,255,.22); box-shadow: 0 16px 42px rgba(16,24,40,.08); }
|
||||
.metric-top { display: flex; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 12px; font-weight: 800; text-transform: uppercase; }
|
||||
.metric-top b { padding: 4px 8px; border-radius: 999px; background: var(--success-soft); color: var(--success); font-size: 12px; }
|
||||
.metric-top b.warn { background: var(--warning-soft); color: var(--warning); }
|
||||
.metric-top b.bad { background: var(--danger-soft); color: var(--danger); }
|
||||
.metric strong { display: block; margin-top: 16px; color: var(--ink); font-size: 30px; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.metric small { display: block; margin-top: 10px; color: var(--muted); }
|
||||
.overview-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(360px, .65fr); gap: 22px; }
|
||||
.spend-panel { padding: 20px; }
|
||||
.bar-chart { display: grid; gap: 14px; }
|
||||
.bar-row { display: grid; grid-template-columns: 94px minmax(0, 1fr) 70px; align-items: center; gap: 14px; font-weight: 700; }
|
||||
.bar-track { height: 36px; overflow: hidden; border-radius: 7px; background: #f2f4f7; }
|
||||
.bar-fill { height: 100%; border-radius: inherit; animation: grow 520ms var(--ease) both; }
|
||||
.bar-row strong { color: var(--ink); text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.donut-layout { display: grid; grid-template-columns: 190px minmax(0, 1fr); align-items: center; gap: 20px; }
|
||||
.donut { width: 174px; aspect-ratio: 1; display: grid; place-items: center; border-radius: 50%; background: radial-gradient(circle,#fff 0 52%,transparent 53%), conic-gradient(#335cff 0 42%,#0e9384 42% 68%,#f79009 68% 86%,#d92d20 86% 100%); }
|
||||
.donut strong { display: block; color: var(--ink); font-size: 28px; text-align: center; }
|
||||
.donut span { color: var(--muted); font-size: 12px; }
|
||||
.legend { display: grid; gap: 10px; }
|
||||
.legend-row { display: grid; grid-template-columns: 12px 1fr auto; align-items: center; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
||||
.legend-row i { width: 12px; height: 12px; border-radius: 999px; }
|
||||
.legend-row strong { color: var(--ink); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user