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:
63
src/composables/useChat.js
Normal file
63
src/composables/useChat.js
Normal 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
|
||||
}
|
||||
}
|
||||
24
src/composables/useNavigation.js
Normal file
24
src/composables/useNavigation.js
Normal 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 }
|
||||
}
|
||||
37
src/composables/useRequests.js
Normal file
37
src/composables/useRequests.js
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/composables/useToast.js
Normal file
13
src/composables/useToast.js
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user