Files
X-Financial/web/src/views/ApprovalCenterView.vue
DESKTOP-72TV0V4\caoxiaozhu 9785fb527b refactor: split project into web and server directories
- Move frontend to web/ directory
- Add server/ directory for backend
- Restructure project for前后端分离架构

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:00:38 +08:00

495 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="approval-page">
<!-- Detail Modal Overlay -->
<Teleport to="body">
<Transition name="detail-modal">
<div v-if="false && selectedRow" class="detail-overlay" @click.self="selectedRow = null">
<div class="detail-modal">
<!-- Modal Header -->
<header class="modal-header">
<div class="header-left">
<div class="req-badge">{{ selectedRow.id }}</div>
<div class="header-title-group">
<h2>{{ selectedRow.type }}审批详情</h2>
<p>申请人{{ selectedRow.applicant }} · {{ selectedRow.department }} · {{ selectedRow.time }}</p>
</div>
</div>
<div class="header-right">
<div class="header-indicator" :class="selectedRow.riskTone">
<i class="mdi" :class="selectedRow.riskTone === 'high' ? 'mdi-alert-circle' : selectedRow.riskTone === 'medium' ? 'mdi-alert' : 'mdi-shield-check'"></i>
<span>{{ selectedRow.risk }}</span>
</div>
<div class="header-indicator status" :class="selectedRow.statusTone">
<span>{{ selectedRow.node }}</span>
</div>
<button class="close-btn" type="button" aria-label="关闭" @click="selectedRow = null">
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<!-- Progress Bar -->
<div class="modal-progress">
<div class="progress-track">
<div v-for="(step, idx) in approvalSteps" :key="step.label" class="progress-node" :class="{ done: step.done, active: step.active, current: step.current }">
<span class="node-dot">
<i v-if="step.done" class="mdi mdi-check"></i>
<template v-else>{{ step.index }}</template>
</span>
<div class="node-label">
<strong>{{ step.label }}</strong>
<small>{{ step.time }}</small>
</div>
<span v-if="idx < approvalSteps.length - 1" class="node-line" :class="{ filled: step.done || step.active }"></span>
</div>
</div>
</div>
<!-- Modal Body -->
<div class="modal-body">
<div class="body-grid">
<!-- Left Column -->
<div class="body-main">
<!-- 费用摘要 -->
<article class="content-card">
<div class="card-header">
<div class="card-title">
<i class="mdi mdi-clipboard-text-outline"></i>
<h3>费用摘要</h3>
</div>
</div>
<div class="metrics-strip">
<div class="metric-block amount">
<span class="metric-label">报销金额</span>
<strong class="metric-value">{{ selectedRow.amount }}</strong>
</div>
<div class="metric-block">
<span class="metric-label">SLA 剩余</span>
<strong class="metric-value sla" :class="selectedRow.slaTone">
<i class="mdi mdi-clock-outline"></i>
{{ selectedRow.sla }}
</strong>
</div>
<div class="metric-block">
<span class="metric-label">费用明细</span>
<strong class="metric-value">5 </strong>
</div>
<div class="metric-block">
<span class="metric-label">附件材料</span>
<strong class="metric-value">6 </strong>
</div>
</div>
<div class="summary-grid">
<div v-for="item in summaryItems" :key="item.label" class="summary-cell">
<div class="cell-icon"><i :class="item.icon"></i></div>
<div class="cell-content">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</div>
</article>
<!-- 费用明细 -->
<article class="content-card">
<div class="card-header">
<div class="card-title">
<i class="mdi mdi-receipt-text-outline"></i>
<h3>费用明细</h3>
</div>
<span class="card-badge">合计 6,920</span>
</div>
<div class="expense-table-wrap">
<table class="expense-table">
<thead>
<tr>
<th>费用项目</th>
<th>说明</th>
<th class="right">金额</th>
<th class="center">是否超标</th>
</tr>
</thead>
<tbody>
<tr v-for="item in expenseItems" :key="item.name">
<td><strong>{{ item.name }}</strong></td>
<td>{{ item.desc }}</td>
<td class="right">{{ item.amount }}</td>
<td class="center">
<span class="over-badge" :class="item.tone">
<i class="mdi" :class="item.tone === 'ok' ? 'mdi-check-circle' : 'mdi-alert-circle'"></i>
{{ item.status }}
</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2"><strong>合计</strong></td>
<td class="right"><strong class="total-amount">6,920</strong></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</article>
<!-- 审批意见 -->
<article class="content-card">
<div class="card-header">
<div class="card-title">
<i class="mdi mdi-comment-text-outline"></i>
<h3>审批意见</h3>
</div>
</div>
<div class="opinion-wrap">
<textarea rows="4" placeholder="请输入审批意见..."></textarea>
</div>
</article>
</div>
<!-- Right Column -->
<aside class="body-side">
<!-- AI 风险识别 -->
<article class="side-card risk-card">
<div class="card-header">
<div class="card-title">
<i class="mdi mdi-robot-outline"></i>
<h3>AI 风险识别</h3>
</div>
<div class="risk-total high">
<span>综合风险</span>
<strong></strong>
</div>
</div>
<div class="risk-items">
<div v-for="risk in riskItems" :key="risk.text" class="risk-row" :class="risk.tone">
<div class="risk-icon">
<i :class="risk.icon"></i>
</div>
<span class="risk-text">{{ risk.text }}</span>
<span class="risk-level" :class="risk.tone">{{ risk.level }}</span>
</div>
</div>
<div class="risk-note">
<strong>AI 审核建议</strong>
<p>优先补齐酒店入住清单并复核出租车发票抬头与超标费用说明完成后可继续流转</p>
</div>
</article>
<!-- 附件材料 -->
<article class="side-card">
<div class="card-header">
<div class="card-title">
<i class="mdi mdi-paperclip"></i>
<h3>附件材料</h3>
</div>
<span class="card-badge warn">1 份缺失</span>
</div>
<div class="attachment-list-side">
<div v-for="file in attachments" :key="file.name" class="attachment-row" :class="{ missing: file.missing }">
<div class="file-icon-sm" :class="file.iconClass">
<i :class="file.icon"></i>
</div>
<div class="file-detail">
<strong>{{ file.name }}</strong>
<span>{{ file.size }}</span>
</div>
</div>
</div>
</article>
</aside>
</div>
</div>
<!-- Modal Footer -->
<footer class="modal-footer">
<div class="footer-left">
<button class="action-btn back" type="button" @click="selectedRow = null">
<i class="mdi mdi-arrow-left"></i>
<span>返回列表</span>
</button>
</div>
<div class="footer-right">
<button class="action-btn supplement" type="button">
<i class="mdi mdi-undo"></i>
<span>补充材料</span>
</button>
<button class="action-btn reject" type="button">
<i class="mdi mdi-close-circle-outline"></i>
<span>驳回</span>
</button>
<button class="action-btn approve" type="button">
<i class="mdi mdi-check-circle-outline"></i>
<span>通过</span>
</button>
</div>
</footer>
</div>
</div>
</Transition>
</Teleport>
<div v-if="selectedRow" class="approval-detail">
<div class="detail-scroll">
<article class="detail-hero panel">
<div class="applicant-card">
<div class="portrait">{{ selectedRow.avatar }}</div>
<div>
<h2>{{ selectedRow.applicant }} <span>{{ selectedRow.department }}</span></h2>
<p>提交时间 <strong>{{ selectedRow.time }}</strong></p>
</div>
</div>
<div class="hero-stat">
<span>金额</span>
<strong>{{ selectedRow.amount }}</strong>
</div>
<div class="hero-stat">
<span>风险等级</span>
<b :class="['risk-pill', selectedRow.riskTone]">{{ selectedRow.risk }}</b>
</div>
<div class="hero-stat">
<span>当前状态</span>
<b class="state-pill">{{ selectedRow.node }}</b>
</div>
<div class="hero-stat">
<span>SLA 剩余时间</span>
<strong class="countdown"><i class="mdi mdi-clock-outline"></i> 剩余 {{ selectedRow.sla }}</strong>
</div>
<div class="hero-summary-panel">
<div v-for="item in heroSummaryItems" :key="item.label" class="hero-summary-item">
<div class="hero-summary-label">
<span class="hero-summary-icon"><i :class="item.icon"></i></span>
<span>{{ item.label }}</span>
</div>
<strong>{{ item.value }}</strong>
</div>
</div>
<div class="progress-block">
<div class="progress-head">
<h3>当前进度</h3>
</div>
<div class="progress-line">
<div v-for="step in approvalSteps" :key="step.label" class="progress-step" :class="{ active: step.active, current: step.current }">
<span>
<i
v-if="step.current"
v-motion
class="current-progress-ring"
:initial="currentProgressRingMotion.initial"
:enter="currentProgressRingMotion.enter"
aria-hidden="true"
></i>
<i v-if="step.done" class="mdi mdi-check"></i>
<template v-else>{{ step.index }}</template>
</span>
<strong>{{ step.label }}</strong>
<small>{{ step.time }}</small>
</div>
</div>
</div>
</article>
<div class="detail-grid">
<section class="detail-left">
<article class="detail-card panel">
<div class="detail-card-head">
<div>
<h3>费用明细</h3>
<p>按发生时间逐笔展示附件与 AI 风险直接在表内完成核对</p>
</div>
<span class="detail-total">{{ expenseTotal }}</span>
</div>
<div class="detail-expense-table">
<table>
<thead>
<tr>
<th>时间</th>
<th>费用项目</th>
<th>说明</th>
<th>金额</th>
<th>附件材料</th>
<th>AI 风险识别</th>
</tr>
</thead>
<tbody>
<template v-for="item in expenseItems" :key="item.id">
<tr>
<td class="expense-time">
<strong>{{ item.time }}</strong>
<span>{{ item.dayLabel }}</span>
</td>
<td class="expense-type">
<strong>{{ item.name }}</strong>
<span>{{ item.category }}</span>
</td>
<td class="expense-desc">
<strong>{{ item.desc }}</strong>
<span>{{ item.detail }}</span>
</td>
<td class="expense-amount">
<strong>{{ item.amount }}</strong>
<span v-if="item.tone !== 'ok'" :class="['over-tag', item.tone]">{{ item.status }}</span>
</td>
<td class="expense-attachment">
<div class="expense-attachment-main">
<span :class="['attachment-pill', item.attachmentTone]">{{ item.attachmentStatus }}</span>
<button
v-if="item.attachments.length"
class="inline-action"
type="button"
@click="toggleExpenseAttachments(item.id)"
>
{{ expandedExpenseId === item.id ? '收起附件' : '查看附件' }}
</button>
</div>
<span class="attachment-hint">{{ item.attachmentHint }}</span>
</td>
<td class="expense-risk">
<template v-if="showExpenseRisk(item)">
<span :class="['risk-inline-tag', item.riskTone]">{{ item.riskLabel }}</span>
<p>{{ item.riskText }}</p>
</template>
</td>
</tr>
<tr v-if="expandedExpenseId === item.id" class="expense-expand-row">
<td colspan="6">
<div class="expense-files">
<span v-for="file in item.attachments" :key="file" class="expense-file-chip">
<i class="mdi mdi-paperclip"></i>
{{ file }}
</span>
</div>
</td>
</tr>
</template>
<tr class="total-row">
<td colspan="3">合计</td>
<td>{{ expenseTotal }}</td>
<td>{{ uploadedExpenseCount }} 项已上传票据</td>
<td>1 项待补材料1 项需补充超标说明</td>
</tr>
</tbody>
</table>
</div>
</article>
<article class="detail-card panel">
<h3>审批意见</h3>
<textarea rows="3" placeholder="输入审批意见..."></textarea>
</article>
</section>
</div>
</div>
<footer class="detail-actions">
<button class="back-action" type="button" @click="selectedRow = null">
<i class="mdi mdi-arrow-left"></i>
<span>退回列表</span>
</button>
<div class="approval-action-group" aria-label="审批操作">
<button class="approve-action" type="button"><i class="mdi mdi-check-circle-outline"></i> 通过</button>
<button class="reject-action" type="button"><i class="mdi mdi-close-circle-outline"></i> 驳回</button>
<button class="supplement-action" type="button"><i class="mdi mdi-undo"></i> 补充</button>
</div>
</footer>
</div>
<!-- Approval List -->
<article v-else class="approval-list panel">
<nav class="status-tabs" aria-label="审批状态">
<button
v-for="tab in tabs"
:key="tab"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
<span>{{ filter }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
</div>
</div>
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击单据行查看审批详情</p>
<div class="table-wrap">
<table>
<colgroup>
<col><col><col><col><col><col><col><col><col><col><col>
</colgroup>
<thead>
<tr>
<th>单号</th>
<th>申请人</th>
<th>申请部门</th>
<th>报销类型</th>
<th>金额</th>
<th>提交时间 <i class="mdi mdi-sort"></i></th>
<th>风险等级</th>
<th>SLA剩余</th>
<th>当前节点</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in visibleRows" :key="row.id" :class="{ spotlight: row.spotlight }" @click="selectedRow = row">
<td><strong class="doc-id">{{ row.id }}</strong></td>
<td>
<span class="person">
<span class="avatar">{{ row.avatar }}</span>
{{ row.applicant }}
</span>
</td>
<td>{{ row.department }}</td>
<td>{{ row.type }}</td>
<td>{{ row.amount }}</td>
<td>{{ row.time }}</td>
<td><span class="risk-tag" :class="row.riskTone">{{ row.risk }}</span></td>
<td><strong class="sla" :class="row.slaTone">{{ row.sla }}</strong></td>
<td>{{ row.node }}</td>
<td><span class="status-tag" :class="row.statusTone">{{ row.status }}</span></td>
<td>
<button class="more-btn" type="button" aria-label="查看审批详情" @click.stop="selectedRow = row">
<i class="mdi mdi-dots-horizontal"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="list-foot">
<span class="page-summary"> 126 当前第 1 </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" aria-label="上一页"><i class="mdi mdi-chevron-left"></i></button>
<button class="page-number active" type="button" aria-current="page">1</button>
<button class="page-number" type="button">2</button>
<button class="page-number" type="button">3</button>
<button class="page-number" type="button">4</button>
<button class="page-number" type="button">5</button>
<span>...</span>
<button class="page-number" type="button">13</button>
<button class="page-nav" type="button" aria-label="下一页"><i class="mdi mdi-chevron-right"></i></button>
</div>
<button class="page-size" type="button">10 / <i class="mdi mdi-chevron-down"></i></button>
</footer>
</article>
</section>
</template>
<script src="./scripts/ApprovalCenterView.js"></script>
<style scoped src="../assets/styles/views/approval-center-view.css"></style>