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:
2026-04-28 17:20:52 +08:00
commit 7141e1d11a
40 changed files with 10133 additions and 0 deletions

View 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>