Files
X-Financial/web/src/views/TravelRequestDetailView.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

318 lines
13 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">
<Teleport to="body">
<Transition name="detail-modal">
<div v-if="aiEntryOpen" class="detail-overlay" @click.self="closeAiEntry">
<div class="detail-modal ai-entry-modal">
<header class="modal-header">
<div class="header-left">
<div class="req-badge">AI</div>
<div class="header-title-group">
<h2>智能录入费用明细</h2>
<p>描述票据行程或费用场景AI 会整理成可追加的费用条目</p>
</div>
</div>
<div class="header-right">
<button class="close-btn" type="button" aria-label="关闭" @click="closeAiEntry">
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="modal-body ai-entry-body">
<div class="ai-entry-grid">
<section class="ai-chat-card">
<input
ref="aiFileInput"
class="ai-file-input"
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
@change="handleAiFilesChange"
/>
<div class="ai-chat-scroll">
<article
v-for="message in aiMessages"
:key="message.id"
class="ai-chat-bubble"
:class="message.role"
>
<span class="ai-chat-avatar">
<i :class="message.role === 'assistant' ? 'mdi mdi-robot-outline' : 'mdi mdi-account-circle-outline'"></i>
</span>
<div class="ai-chat-content">
<header>
<strong>{{ message.role === 'assistant' ? 'AI 录入助手' : '我' }}</strong>
</header>
<p>{{ message.text }}</p>
</div>
</article>
</div>
<div class="ai-composer">
<div class="ai-composer-surface">
<textarea
v-model="aiDraft"
rows="3"
placeholder="例如7月12日从上海虹桥到杭州东高铁二等座236元已上传车票和行程单。"
/>
<div class="ai-composer-actions">
<button class="ai-upload-btn" type="button" aria-label="上传单据" @click="triggerAiUpload">
<i class="mdi mdi-paperclip"></i>
</button>
<button class="ai-send-btn" type="button" aria-label="发送给 AI" :disabled="!canSendAiEntry" @click="sendAiEntry">
<i class="mdi mdi-send"></i>
</button>
</div>
</div>
<div v-if="uploadedAiFiles.length" class="ai-upload-list">
<span v-for="file in uploadedAiFiles" :key="file.name" class="ai-upload-chip">
<i class="mdi mdi-paperclip"></i>
{{ file.name }}
</span>
</div>
</div>
</section>
<aside class="ai-preview-card">
<div class="ai-preview-head">
<div>
<h3>识别结果</h3>
<p>确认后会直接追加到费用明细表</p>
</div>
<span v-if="pendingAiExpense" class="attachment-pill neutral">待确认</span>
</div>
<div v-if="pendingAiExpense" class="ai-preview-fields">
<div class="preview-field">
<span>日期</span>
<strong>{{ pendingAiExpense.time }} {{ pendingAiExpense.dayLabel }}</strong>
</div>
<div class="preview-field">
<span>费用项目</span>
<strong>{{ pendingAiExpense.name }}</strong>
</div>
<div class="preview-field">
<span>分类</span>
<strong>{{ pendingAiExpense.category }}</strong>
</div>
<div class="preview-field">
<span>金额</span>
<strong>{{ pendingAiExpense.amount }}</strong>
</div>
<div class="preview-field full">
<span>说明</span>
<strong>{{ pendingAiExpense.desc }}</strong>
<p>{{ pendingAiExpense.detail }}</p>
</div>
<div class="preview-field full">
<span>附件</span>
<strong>{{ pendingAiExpense.attachmentStatus }}</strong>
<p>{{ pendingAiExpense.attachmentHint }}</p>
</div>
<div class="ai-preview-actions">
<button class="ai-preview-secondary" type="button" @click="regenerateAiEntry">
<i class="mdi mdi-refresh"></i>
重新生成
</button>
<button class="ai-preview-primary" type="button" @click="applyAiExpense">
<i class="mdi mdi-plus-circle-outline"></i>
加入费用明细
</button>
</div>
</div>
<div v-else class="ai-preview-empty">
<i class="mdi mdi-robot-outline"></i>
<p>发送一段费用描述后这里会生成结构化结果</p>
</div>
</aside>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
<div class="approval-detail">
<div class="detail-scroll">
<article class="detail-hero panel">
<div class="applicant-card">
<div class="portrait">{{ profile.avatar }}</div>
<div>
<h2>{{ profile.name }} <span>{{ profile.department }}</span></h2>
<p>申请时间 <strong>{{ request.applyTime }}</strong></p>
</div>
</div>
<div class="hero-stat">
<span>金额</span>
<strong>{{ expenseTotal }}</strong>
</div>
<div class="hero-stat">
<span>审批状态</span>
<b class="state-pill">{{ request.node }}</b>
</div>
<div class="hero-stat">
<span>商旅状态</span>
<b :class="['risk-pill', request.travelTone]">{{ request.travel }}</b>
</div>
<div class="hero-stat">
<span>申请状态</span>
<strong class="countdown"><i class="mdi mdi-clock-outline"></i> {{ request.approval }}</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 progressSteps"
: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>按发生时间逐笔展示附件与系统校验直接在表内完成核对</p>
</div>
<button class="smart-entry-btn" type="button" @click="openAiEntry">
<i class="mdi mdi-robot-outline"></i>
<span>智能录入</span>
</button>
</div>
<div class="detail-expense-table">
<table>
<thead>
<tr>
<th>时间</th>
<th>费用项目</th>
<th>说明</th>
<th>金额</th>
<th>附件材料</th>
<th>系统校验</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" :value="detailNote" placeholder="输入申请说明..." />
</article>
</section>
</div>
</div>
<footer class="detail-actions">
<button class="back-action" type="button" @click="emit('backToRequests')">
<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-send-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-pencil-outline"></i> 编辑申请</button>
</div>
</footer>
</div>
</section>
</template>
<script src="./scripts/TravelRequestDetailView.js"></script>
<style scoped src="../assets/styles/views/travel-request-detail-view.css"></style>