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>
This commit is contained in:
317
web/src/views/TravelRequestDetailView.vue
Normal file
317
web/src/views/TravelRequestDetailView.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user