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,63 @@
import { nextTick, ref } from 'vue'
import { initialMessages, prompts } from '../data/requests.js'
export function useChat(activeView) {
const messages = ref([...initialMessages])
const draft = ref('')
const uploadedFiles = ref([])
const messageList = ref(null)
const activeCase = ref(null)
function agentReply(text) {
const c = activeCase.value
if (!c) return '建议先核对政策阈值和附件完整性,再决定通过、退回补件或转人工复核。'
if (text.includes('审批')) return `${c.id} 建议审批意见:发票验真通过,费用归属与预算中心匹配;${c.risk} 已触发规则提示,建议保留业务说明后通过。`
if (text.includes('补件')) return '补件优先级:业务目的说明、行程或客户名单、直属经理确认记录。'
if (text.includes('拦截')) return `拦截原因是 ${c.risk},该风险需要财务复核并留下制度依据。`
if (text.includes('审计')) return `审计摘要:${c.person} 提交 ${c.amount} 报销,命中 ${c.risk},系统已保留 AI 判断。`
return '建议先核对政策阈值和附件完整性,再决定通过、退回补件或转人工复核。'
}
function scrollToBottom() {
nextTick(() => messageList.value?.scrollTo({ top: messageList.value.scrollHeight, behavior: 'smooth' }))
}
function sendMessage() {
const text = draft.value.trim()
if (!text) return false
messages.value.push({ id: Date.now(), role: 'user', text })
draft.value = ''
setTimeout(() => {
messages.value.push({ id: Date.now() + 1, role: 'agent', text: agentReply(text) })
scrollToBottom()
}, 260)
return true
}
function handleUpload(event) {
uploadedFiles.value = Array.from(event.target.files ?? []).map((file) => ({
name: file.name,
size: file.size
}))
if (uploadedFiles.value.length) {
const names = uploadedFiles.value.map((file) => file.name).join('、')
messages.value.push({
id: Date.now(),
role: 'agent',
text: `已接收 ${uploadedFiles.value.length} 个附件:${names}。我会优先核对发票验真、费用标准、预算归属和必备审批材料。`
})
scrollToBottom()
}
}
function openChat(request) {
activeCase.value = request
activeView.value = 'chat'
nextTick(() => messageList.value?.scrollTo({ top: messageList.value.scrollHeight }))
}
return {
messages, draft, uploadedFiles, messageList, activeCase, prompts,
sendMessage, handleUpload, openChat, scrollToBottom
}
}

View File

@@ -0,0 +1,24 @@
import { computed, ref } from 'vue'
import { icons } from '../data/icons.js'
export const navItems = [
{ id: 'overview', label: '运营总览', icon: icons.dashboard, title: '企业报销智能运营台', desc: '面向财务共享中心的审批、风控、SLA 与智能体协同工作台。' },
{ id: 'chat', label: '合规对话', icon: icons.message, title: 'AI 合规对话', desc: '上传单据、追问制度依据,并生成可留痕的审核建议。' },
{ id: 'requests', label: '报销队列', icon: icons.list, title: '报销申请队列', desc: '按风险、补件状态和 AI 建议处理待审单据。' },
{ id: 'policies', label: '政策规则', icon: icons.file, title: '政策规则中心', desc: '维护差旅、招待、采购和发票校验规则。' },
{ id: 'audit', label: '审计追踪', icon: icons.audit, title: '审计追踪', desc: '查看关键审批动作、AI 建议和制度命中记录。' }
]
export function useNavigation() {
const activeView = ref('overview')
const currentView = computed(
() => navItems.find((item) => item.id === activeView.value) ?? navItems[0]
)
function setView(view) {
activeView.value = view
}
return { activeView, currentView, setView, navItems }
}

View File

@@ -0,0 +1,37 @@
import { computed, reactive, ref } from 'vue'
import { initialRequests } from '../data/requests.js'
export function useRequests() {
const requests = ref(initialRequests)
const search = ref('')
const filters = reactive({ entity: '全部主体', category: '全部费用', risk: '全部风险' })
const ranges = ['今日', '本周', '本月']
const activeRange = ref('今日')
const filteredRequests = computed(() => {
const key = search.value.trim().toLowerCase()
return requests.value.filter((item) => {
const matchesSearch = !key || `${item.id}${item.person}${item.category}${item.risk}`.toLowerCase().includes(key)
const matchesCategory = filters.category === '全部费用' || item.category.includes(filters.category.replace('交通', ''))
const matchesRisk = filters.risk === '全部风险' || (filters.risk === '高风险' ? item.status === 'danger' : item.verdict.includes(filters.risk.replace('低风险', '通过')))
return matchesSearch && matchesCategory && matchesRisk
})
})
function approveRequest(request) {
request.verdict = '已通过'
request.status = 'success'
return `${request.id} 已标记为通过,审计日志已更新。`
}
function rejectRequest(request) {
request.verdict = '已退回补件'
request.status = 'danger'
return `${request.id} 已退回,系统将通知申请人补充材料。`
}
return {
requests, search, filters, ranges, activeRange,
filteredRequests, approveRequest, rejectRequest
}
}

View File

@@ -0,0 +1,13 @@
import { ref } from 'vue'
export function useToast() {
const toastText = ref('')
function toast(text) {
toastText.value = text
clearTimeout(toast.timer)
toast.timer = setTimeout(() => { toastText.value = '' }, 3200)
}
return { toastText, toast }
}