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:
2026-05-06 11:00:38 +08:00
parent 9a7b0794a1
commit 9785fb527b
85 changed files with 10474 additions and 10047 deletions

View 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>