feat: add travel reimbursement creation page

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-30 17:10:30 +08:00
parent d860b0cc34
commit 053ebdbcb0

View File

@@ -0,0 +1,803 @@
<template>
<section class="create-expense-view">
<article class="assistant-panel panel">
<header class="assistant-head">
<button class="back-btn" type="button" aria-label="返回差旅申请/报销" @click="emit('backToRequests')">
<i class="pi pi-arrow-left"></i>
<span>返回</span>
</button>
</header>
<div class="chat-scroll" aria-live="polite">
<div class="message-row user">
<div class="message-bubble user-bubble">
<p>我要报销昨天去上海出差的费用</p>
<time>09:41 <i class="pi pi-check"></i></time>
</div>
<span class="avatar user-avatar"><i class="pi pi-user"></i></span>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<div class="message-bubble">
<p>好的我已识别到这是一次差旅报销我建议你先补充行程日期交通票据和酒店发票</p>
<div class="message-actions">
<button type="button" aria-label="赞同"><i class="pi pi-thumbs-up"></i></button>
<button type="button" aria-label="不赞同"><i class="pi pi-thumbs-down"></i></button>
</div>
<time>09:42</time>
</div>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<div class="message-bubble wide">
<p>已自动识别</p>
<div class="detected-grid">
<article v-for="item in detectedItems" :key="item.label">
<i :class="item.icon"></i>
<strong>{{ item.label }}</strong>
<span>{{ item.count }}</span>
</article>
</div>
<time>09:42</time>
</div>
</div>
<div class="message-row user">
<div class="message-bubble user-bubble compact">
<p>现在到哪一步了</p>
<time>09:43 <i class="pi pi-check"></i></time>
</div>
<span class="avatar user-avatar"><i class="pi pi-user"></i></span>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<div class="message-bubble">
<p>当前已完成票据识别与费用归类下一步请确认费用明细并提交审批</p>
<ul class="check-list">
<li><i class="pi pi-check"></i>票据识别已完成</li>
<li><i class="pi pi-check"></i>费用归类已完成</li>
<li><i class="pi pi-circle"></i>明细确认待你确认</li>
<li><i class="pi pi-circle"></i>提交审批待提交</li>
</ul>
<div class="message-actions">
<button type="button" aria-label="赞同"><i class="pi pi-thumbs-up"></i></button>
<button type="button" aria-label="不赞同"><i class="pi pi-thumbs-down"></i></button>
</div>
<time>09:43</time>
</div>
</div>
</div>
<div class="quick-actions" aria-label="快捷操作">
<button v-for="action in quickActions" :key="action.label" type="button">
<i :class="action.icon"></i>
<span>{{ action.label }}</span>
</button>
</div>
<form class="assistant-composer" @submit.prevent>
<textarea rows="2" placeholder="请输入你的问题,或让 AI 帮你填写报销..."></textarea>
<div class="composer-foot">
<div class="tool-set">
<button type="button" aria-label="上传附件"><i class="pi pi-paperclip"></i></button>
<button type="button" aria-label="上传图片"><i class="pi pi-image"></i></button>
<button type="button" aria-label="语音输入"><i class="pi pi-microphone"></i></button>
</div>
<button class="send-btn" type="submit" aria-label="发送">
<i class="pi pi-send"></i>
</button>
</div>
</form>
</article>
<aside class="create-side">
<article class="progress-card panel">
<header>
<h3>当前报销状态</h3>
<strong>整体进度 <span>72%</span></strong>
</header>
<div class="progress-steps">
<div v-for="step in progressSteps" :key="step.label" class="step" :class="{ active: step.active, done: step.done }">
<span>{{ step.index }}</span>
<strong>{{ step.label }}</strong>
<small>{{ step.time }}</small>
</div>
</div>
</article>
<div class="side-grid">
<article class="side-card panel">
<header>
<h3>当前摘要</h3>
<button type="button">查看详情 <i class="pi pi-angle-right"></i></button>
</header>
<dl class="summary-list">
<template v-for="item in summaryItems" :key="item.label">
<dt><i :class="item.icon"></i>{{ item.label }}</dt>
<dd :class="{ money: item.money }">{{ item.value }}</dd>
</template>
</dl>
</article>
<article class="side-card panel">
<h3>待办提醒</h3>
<div class="todo-list">
<div v-for="item in todos" :key="item.text">
<i :class="item.icon"></i>
<span>{{ item.text }}</span>
<b :class="item.tone">{{ item.tag }}</b>
</div>
</div>
</article>
<article class="side-card panel">
<h3>AI 建议</h3>
<ul class="advice-list">
<li v-for="item in advice" :key="item.text">
<i :class="item.icon"></i>
<span>{{ item.text }}</span>
</li>
</ul>
<button class="more-link" type="button">查看更多建议 <i class="pi pi-angle-right"></i></button>
</article>
<article class="side-card panel">
<h3>注意事项</h3>
<ul class="notice-list">
<li v-for="item in notices" :key="item">{{ item }}</li>
</ul>
<button class="more-link" type="button">查看差旅管理制度 <i class="pi pi-angle-right"></i></button>
</article>
</div>
<article class="recent-card panel">
<header>
<h3>最近操作</h3>
<button type="button">查看全部 <i class="pi pi-angle-right"></i></button>
</header>
<ol>
<li v-for="item in recentOps" :key="item.time">
<time>{{ item.time }}</time>
<span>{{ item.text }}</span>
</li>
</ol>
</article>
</aside>
</section>
</template>
<script setup>
const emit = defineEmits(['backToRequests'])
const detectedItems = [
{ label: '机票', count: '2 张', icon: 'pi pi-send' },
{ label: '酒店发票', count: '1 张', icon: 'pi pi-building' },
{ label: '出租车发票', count: '3 张', icon: 'pi pi-car' },
{ label: '其他票据', count: '0 张', icon: 'pi pi-file' }
]
const quickActions = [
{ label: '一键生成报销单', icon: 'pi pi-file-edit' },
{ label: '检查缺失材料', icon: 'pi pi-verified' },
{ label: '查看审批进度', icon: 'pi pi-list-check' },
{ label: '差旅标准查询', icon: 'pi pi-search' }
]
const progressSteps = [
{ index: '✓', label: '发起报销', time: '07-12 09:20', done: true },
{ index: '✓', label: '票据识别', time: '07-12 09:25', done: true },
{ index: '✓', label: '费用归类', time: '07-12 09:35', done: true },
{ index: '4', label: '待确认提交', time: '当前步骤', active: true },
{ index: '5', label: '审批中', time: '', active: false }
]
const summaryItems = [
{ label: '报销类型', value: '差旅报销', icon: 'pi pi-tags' },
{ label: '申请人', value: '张晓明', icon: 'pi pi-user' },
{ label: '金额预估', value: '¥3,680', icon: 'pi pi-wallet', money: true },
{ label: '出差时间', value: '07-11 ~ 07-12', icon: 'pi pi-calendar' },
{ label: '单据数量', value: '6', icon: 'pi pi-copy' }
]
const todos = [
{ text: '缺少酒店入住清单', tag: '缺失', tone: 'danger', icon: 'pi pi-times-circle' },
{ text: '发票抬头已识别为个人,建议核对', tag: '警告', tone: 'warning', icon: 'pi pi-exclamation-triangle' },
{ text: '有 1 笔出租车费用超过标准', tag: '注意', tone: 'notice', icon: 'pi pi-info-circle' },
{ text: '请在今日 18:00 前提交以保证本周审批', tag: '提醒', tone: 'success', icon: 'pi pi-clock' }
]
const advice = [
{ text: '建议补充酒店入住清单,提升审批通过率', icon: 'pi pi-lightbulb' },
{ text: '出租车费用超标 ¥28建议说明原因或调整', icon: 'pi pi-car' },
{ text: '可合并同类票据,减少审批批次', icon: 'pi pi-check-circle' }
]
const notices = [
'差旅住宿标准:一线城市 ¥500 / 晚',
'餐补标准¥80 / 天',
'市内交通标准¥100 / 天'
]
const recentOps = [
{ time: '09:48', text: '检测到缺失材料:酒店入住清单' },
{ time: '09:45', text: '费用归类完成,总计 ¥3,680' },
{ time: '09:42', text: '识别发票完成,共 6 张票据' }
]
</script>
<style scoped>
.create-expense-view {
min-height: 0;
height: 100%;
display: grid;
grid-template-columns: minmax(620px, 1.35fr) minmax(420px, .95fr);
gap: 16px;
animation: fadeUp 220ms var(--ease) both;
}
.assistant-panel,
.create-side,
.side-card,
.progress-card,
.recent-card {
min-width: 0;
}
.assistant-panel {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto auto;
overflow: hidden;
}
.assistant-head {
min-height: 64px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 14px;
padding: 14px 22px;
border-bottom: 1px solid #e6edf5;
}
.back-btn {
min-height: 36px;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 0 12px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #f8fafc;
color: #334155;
font-size: 13px;
font-weight: 800;
}
.back-btn i {
color: #059669;
}
.progress-card h3,
.side-card h3,
.recent-card h3 {
color: #0f172a;
font-size: 18px;
font-weight: 900;
}
.side-card header button,
.recent-card header button {
min-height: 36px;
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #334155;
font-size: 13px;
font-weight: 750;
}
.chat-scroll {
min-height: 0;
display: grid;
align-content: start;
gap: 16px;
padding: 18px 22px 20px;
overflow-y: auto;
}
.message-row {
display: grid;
align-items: start;
gap: 12px;
}
.message-row.user {
grid-template-columns: minmax(0, .42fr) 38px;
justify-content: end;
}
.message-row.assistant {
grid-template-columns: 38px minmax(0, .72fr);
}
.avatar {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 999px;
background: #dff7ee;
color: #059669;
font-size: 18px;
}
.message-bubble {
padding: 14px 16px;
border: 1px solid #dce5ef;
border-radius: 10px;
background: #fff;
color: #24324a;
font-size: 14px;
line-height: 1.65;
}
.message-bubble p {
margin: 0;
}
.user-bubble {
background: linear-gradient(135deg, rgba(16,185,129,.14), rgba(16,185,129,.06));
border-color: rgba(16,185,129,.22);
}
.message-bubble.compact {
justify-self: end;
}
.message-bubble.wide {
max-width: 620px;
}
.message-bubble time {
float: right;
margin-left: 12px;
color: #64748b;
font-size: 12px;
}
.message-bubble time i {
color: #3b82f6;
}
.message-actions {
display: flex;
gap: 10px;
margin-top: 8px;
}
.message-actions button {
border: 0;
background: transparent;
color: #64748b;
}
.detected-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.detected-grid article {
min-height: 58px;
display: grid;
grid-template-columns: 26px minmax(0, 1fr);
align-items: center;
gap: 8px;
padding: 10px;
border-radius: 8px;
background: #f7fafc;
}
.detected-grid i {
grid-row: span 2;
color: #3b82f6;
font-size: 20px;
}
.detected-grid article:nth-child(2) i { color: #10b981; }
.detected-grid article:nth-child(3) i { color: #f59e0b; }
.detected-grid strong {
color: #334155;
font-size: 13px;
font-weight: 850;
}
.detected-grid span {
color: #64748b;
font-size: 12px;
}
.check-list {
display: grid;
gap: 5px;
margin: 10px 0 0;
padding: 0;
list-style: none;
color: #334155;
}
.check-list i {
margin-right: 8px;
color: #059669;
}
.quick-actions {
display: flex;
gap: 12px;
padding: 14px 22px 10px;
border-top: 1px solid #eef2f7;
}
.quick-actions button {
min-height: 34px;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 0 13px;
border: 1px solid #d7e0ea;
border-radius: 999px;
background: #fff;
color: #334155;
font-size: 13px;
font-weight: 750;
}
.quick-actions i {
color: #059669;
}
.assistant-composer {
margin: 0 22px 18px;
border: 1px solid #d7e0ea;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.assistant-composer textarea {
width: 100%;
min-height: 56px;
resize: none;
border: 0;
padding: 14px 16px 4px;
color: #0f172a;
font-size: 14px;
}
.assistant-composer textarea:focus {
outline: none;
}
.composer-foot {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px 10px 12px;
}
.tool-set {
display: flex;
gap: 8px;
}
.tool-set button,
.send-btn {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
}
.tool-set button {
background: #f8fafc;
color: #42526b;
}
.send-btn {
background: #10b981;
color: #fff;
}
.create-side {
min-height: 0;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 14px;
overflow-y: auto;
}
.progress-card,
.side-card,
.recent-card {
padding: 18px 20px;
}
.progress-card header,
.side-card header,
.recent-card header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.progress-card header strong {
color: #64748b;
font-size: 14px;
}
.progress-card header span {
color: #059669;
font-size: 25px;
font-weight: 900;
}
.progress-steps {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
margin-top: 20px;
}
.step {
position: relative;
display: grid;
justify-items: center;
gap: 7px;
text-align: center;
}
.step:not(:last-child)::after {
content: "";
position: absolute;
top: 16px;
left: calc(50% + 20px);
width: calc(100% - 28px);
height: 2px;
background: #d7e0ea;
}
.step.done:not(:last-child)::after,
.step.active:not(:last-child)::after {
background: #10b981;
}
.step > span {
position: relative;
z-index: 1;
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 999px;
background: #e5e7eb;
color: #64748b;
font-weight: 900;
}
.step.done > span,
.step.active > span {
background: #10b981;
color: #fff;
box-shadow: 0 9px 18px rgba(16,185,129,.22);
}
.step strong {
color: #0f172a;
font-size: 13px;
font-weight: 850;
}
.step small {
color: #64748b;
font-size: 12px;
}
.step.active small {
color: #059669;
font-weight: 850;
}
.side-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.side-card header button,
.recent-card header button {
border: 0;
background: transparent;
color: #64748b;
}
.summary-list {
display: grid;
grid-template-columns: 1fr auto;
row-gap: 14px;
margin: 18px 0 0;
}
.summary-list dt,
.summary-list dd {
margin: 0;
}
.summary-list dt {
display: inline-flex;
align-items: center;
gap: 8px;
color: #64748b;
font-size: 13px;
font-weight: 750;
}
.summary-list dt i,
.advice-list i,
.notice-list li::before {
color: #059669;
}
.summary-list dd {
color: #0f172a;
font-size: 13px;
font-weight: 850;
}
.summary-list dd.money {
color: #059669;
font-size: 17px;
}
.todo-list,
.advice-list,
.notice-list {
display: grid;
gap: 12px;
margin-top: 18px;
}
.todo-list div {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
color: #334155;
font-size: 13px;
}
.todo-list i {
color: #f59e0b;
}
.todo-list b {
padding: 3px 7px;
border-radius: 7px;
font-size: 12px;
}
.todo-list .danger { background: #fee2e2; color: #ef4444; }
.todo-list .warning { background: #ffedd5; color: #f97316; }
.todo-list .notice { background: #ffedd5; color: #f97316; }
.todo-list .success { background: #dff7ee; color: #059669; }
.advice-list,
.notice-list {
padding: 0;
list-style: none;
}
.advice-list li {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
gap: 8px;
color: #334155;
font-size: 13px;
line-height: 1.55;
}
.notice-list li {
position: relative;
padding-left: 16px;
color: #334155;
font-size: 13px;
}
.notice-list li::before {
content: "";
position: absolute;
left: 0;
top: .58em;
width: 7px;
height: 7px;
border-radius: 999px;
background: #10b981;
}
.more-link {
margin-top: 16px;
border: 0;
background: transparent;
color: #059669;
font-size: 13px;
font-weight: 850;
}
.recent-card ol {
display: grid;
gap: 10px;
margin: 16px 0 0;
padding: 0;
list-style: none;
}
.recent-card li {
display: grid;
grid-template-columns: 48px minmax(0, 1fr);
gap: 10px;
color: #64748b;
font-size: 13px;
}
.recent-card time {
color: #64748b;
font-weight: 850;
}
.recent-card span {
color: #334155;
}
@media (max-width: 1280px) {
.create-expense-view {
grid-template-columns: 1fr;
overflow-y: auto;
}
.create-side {
overflow: visible;
}
}
@media (max-width: 760px) {
.detected-grid,
.side-grid,
.progress-steps {
grid-template-columns: 1fr;
}
.message-row.assistant,
.message-row.user {
grid-template-columns: 34px minmax(0, 1fr);
}
.message-row.user {
grid-template-columns: minmax(0, 1fr) 34px;
}
.quick-actions {
flex-wrap: wrap;
}
}
</style>