feat(报销/拨款): 新增发票明细子表与OCR自动识别
- 新增 hrm_invoice_item 共享子表(biz_type区分报销/拨款),每条记录对应一张发票条目 - 新增 HrmInvoiceOcrService,上传附件后自动调用 ai-ocr Python服务识别发票,结果逐条回填表单 - 报销/拨款申请提交及更新时同步保存发票明细;queryById 返回关联发票条目列表 - 前端:附件上传后自动触发OCR,展示"模型思考中"状态,识别完成后自动填充金额 - 详情页新增发票明细只读表格展示,兼容无明细的历史记录 - application.yml 增加 fad.ocr 配置项 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,3 +57,14 @@ export function getAppropriationStats (query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
||||
*/
|
||||
export function ocrAppropriationInvoice (ossId) {
|
||||
return request({
|
||||
url: '/hrm/appropriation/ocr-by-oss',
|
||||
method: 'post',
|
||||
params: { ossId }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,3 +47,14 @@ export function allReimburseReq(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
||||
*/
|
||||
export function ocrReimburseInvoice(ossId) {
|
||||
return request({
|
||||
url: '/hrm/reimburse/ocr-by-oss',
|
||||
method: 'post',
|
||||
params: { ossId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -47,13 +47,63 @@
|
||||
<el-col>
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
<project-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%" />
|
||||
<!-- <el-input v-model="form.projectId" placeholder="请输入项目ID" style="width: 100%" /> -->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="拨款事由" prop="reason">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="4" placeholder="请说明拨款事由、费用用途等" show-word-limit
|
||||
<!-- 拨款单据附件(OCR触发区) -->
|
||||
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
|
||||
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
|
||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
|
||||
<div class="hint-text">上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别)</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- OCR 识别中提示 -->
|
||||
<div v-if="anyOcrLoading" class="ocr-thinking">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>模型思考中,正在识别发票内容…</span>
|
||||
</div>
|
||||
|
||||
<!-- 发票明细条目表 -->
|
||||
<div class="block-title">
|
||||
拨款明细
|
||||
<span class="block-title-hint">(上传发票后自动填充,也可手动添加)</span>
|
||||
</div>
|
||||
<div class="invoice-table">
|
||||
<div class="invoice-table-header">
|
||||
<span class="col-reason">事由说明</span>
|
||||
<span class="col-amount">金额(元)</span>
|
||||
<span class="col-action"></span>
|
||||
</div>
|
||||
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
||||
<el-input
|
||||
v-model="item.reason"
|
||||
:placeholder="item.itemName || '请填写事由'"
|
||||
size="small"
|
||||
class="col-reason"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="item.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
size="small"
|
||||
class="col-amount"
|
||||
@change="recalcTotal"
|
||||
/>
|
||||
<div class="col-action">
|
||||
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-table-footer">
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addInvoiceItem">添加条目</el-button>
|
||||
<span class="total-hint" v-if="invoiceItems.length">
|
||||
合计:<b>¥{{ invoiceTotalFormatted }}</b>(已自动更新拨款总金额)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item label="补充说明" prop="reason">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="2" placeholder="可选:填写整体说明或补充" show-word-limit
|
||||
maxlength="1000" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -80,12 +130,6 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
|
||||
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
|
||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
|
||||
<div class="hint-text">上传发票、收据、付款截图等(必填)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="回执附件(可选)" prop="accessoryReceiptIds">
|
||||
<file-upload v-model="form.accessoryReceiptIds" :limit="10" :file-size="50"
|
||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
|
||||
@@ -106,14 +150,13 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 抄送备注 -->
|
||||
<el-form-item label="抄送备注" prop="ccRemark">
|
||||
<el-input v-model="ccForm.remark" type="textarea" :rows="2" placeholder="可以填写抄送的目的或原因等信息" show-word-limit
|
||||
maxlength="1000" />
|
||||
</el-form-item>
|
||||
<UserMultiSelect ref="userMultiSelect" :init="ccUserIds" @onSelected="onCcUsersSelected" />
|
||||
|
||||
<!-- 审批方式(模板/自选审批人) -->
|
||||
<!-- 审批方式 -->
|
||||
<div class="block-title">审批方式</div>
|
||||
<div class="approve-mode">
|
||||
<el-radio-group v-model="approverMode" size="small" @change="onApproverModeChange">
|
||||
@@ -163,18 +206,11 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 模板模式 -->
|
||||
<div v-if="approverMode === 'template'">
|
||||
<div v-if="flowNodes && flowNodes.length" class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">填写申请</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">填写申请</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">提交</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">提交</div></div>
|
||||
<template v-for="(n, idx) in flowNodes">
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
@@ -188,22 +224,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动审批模式 -->
|
||||
<div v-else class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">填写申请</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">填写申请</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">提交审批({{ assigneeUserName || '请选择' }})</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">提交审批({{ assigneeUserName || '请选择' }})</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot success"></div>
|
||||
<div class="txt">审批结束</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot success"></div><div class="txt">审批结束</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,14 +239,13 @@
|
||||
</div>
|
||||
|
||||
<UserSelect ref="userSelect" @onSelected="onUserSelected" />
|
||||
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addAppropriationReq, listFlowNode, listFlowTemplate } from '@/api/hrm';
|
||||
import { addAppropriationReq, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } from '@/api/hrm';
|
||||
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
||||
import { ccFlowTask } from '@/api/hrm/flow';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
@@ -243,6 +268,12 @@ export default {
|
||||
assigneeUserId: null,
|
||||
assigneeUserName: '',
|
||||
appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
||||
// 发票明细条目
|
||||
invoiceItems: [],
|
||||
// OCR加载状态 { ossId: true/false }
|
||||
ocrLoadingMap: {},
|
||||
// 已触发过OCR的ossId集合
|
||||
ocrDoneSet: new Set(),
|
||||
form: {
|
||||
empId: '',
|
||||
appropriationType: '',
|
||||
@@ -256,7 +287,6 @@ export default {
|
||||
bankAccount: ''
|
||||
},
|
||||
ccForm: {
|
||||
// 默认抄送胡雪娇
|
||||
selectedUsers: [{
|
||||
userId: '1859249502579310593',
|
||||
nickName: '胡雪娇',
|
||||
@@ -270,9 +300,7 @@ export default {
|
||||
},
|
||||
rules: {
|
||||
appropriationType: [{ required: true, message: '请选择/输入拨款类型', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请填写拨款总金额', trigger: 'blur' }],
|
||||
reason: [{ required: true, message: '请填写拨款事由', trigger: 'blur' }],
|
||||
// accessoryApplyIds: [{ required: true, message: '请上传拨款单据附件', trigger: 'change' }]
|
||||
amount: [{ required: true, message: '请填写拨款总金额', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -295,11 +323,79 @@ export default {
|
||||
ccUserIds () {
|
||||
return this.ccForm.selectedUsers?.map(u => u.userId) || []
|
||||
},
|
||||
anyOcrLoading () {
|
||||
return Object.values(this.ocrLoadingMap).some(v => v)
|
||||
},
|
||||
invoiceTotalFormatted () {
|
||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||
return total.toFixed(2)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'form.accessoryApplyIds' (newVal, oldVal) {
|
||||
const newIds = newVal ? newVal.split(',').filter(Boolean) : []
|
||||
const oldIds = oldVal ? oldVal.split(',').filter(Boolean) : []
|
||||
const added = newIds.filter(id => !oldIds.includes(id))
|
||||
added.forEach(id => this.triggerOcr(id))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openUserMultiSelect () { this.$refs.userMultiSelect.open() },
|
||||
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
||||
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
||||
|
||||
async triggerOcr (ossId) {
|
||||
if (this.ocrDoneSet.has(ossId)) return
|
||||
this.$set(this.ocrLoadingMap, ossId, true)
|
||||
try {
|
||||
const res = await ocrAppropriationInvoice(ossId)
|
||||
if (res.code === 200 && res.data) {
|
||||
const { items, totalAmount } = res.data
|
||||
if (items && items.length) {
|
||||
const startIdx = this.invoiceItems.length
|
||||
items.forEach((item, i) => {
|
||||
this.invoiceItems.push({
|
||||
ossId: Number(ossId),
|
||||
itemName: item.itemName || '',
|
||||
reason: item.itemName || '',
|
||||
amount: item.amount || 0,
|
||||
sortNo: startIdx + i
|
||||
})
|
||||
})
|
||||
} else if (totalAmount) {
|
||||
this.invoiceItems.push({
|
||||
ossId: Number(ossId),
|
||||
itemName: '',
|
||||
reason: '',
|
||||
amount: totalAmount,
|
||||
sortNo: this.invoiceItems.length
|
||||
})
|
||||
}
|
||||
this.recalcTotal()
|
||||
}
|
||||
this.ocrDoneSet.add(ossId)
|
||||
} catch (e) {
|
||||
console.error('[OCR] 识别失败', e)
|
||||
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
||||
} finally {
|
||||
this.$set(this.ocrLoadingMap, ossId, false)
|
||||
}
|
||||
},
|
||||
|
||||
addInvoiceItem () {
|
||||
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||
},
|
||||
|
||||
removeInvoiceItem (idx) {
|
||||
this.invoiceItems.splice(idx, 1)
|
||||
this.recalcTotal()
|
||||
},
|
||||
|
||||
recalcTotal () {
|
||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||
this.form.amount = parseFloat(total.toFixed(2))
|
||||
},
|
||||
|
||||
async loadTemplates () {
|
||||
try {
|
||||
const res = await listFlowTemplate({ pageNum: 1, pageSize: 200, bizType: 'appropriation', enabled: 1 })
|
||||
@@ -315,16 +411,8 @@ export default {
|
||||
async refreshFlowPreview () {
|
||||
this.flowLoading = true
|
||||
try {
|
||||
if (this.approverMode === 'manual') {
|
||||
this.flowTpl = null
|
||||
this.flowNodes = []
|
||||
return
|
||||
}
|
||||
if (!this.tplId) {
|
||||
this.flowTpl = null
|
||||
this.flowNodes = []
|
||||
return
|
||||
}
|
||||
if (this.approverMode === 'manual') { this.flowTpl = null; this.flowNodes = []; return }
|
||||
if (!this.tplId) { this.flowTpl = null; this.flowNodes = []; return }
|
||||
this.flowTpl = this.availableTpls.find(t => t.tplId === this.tplId) || null
|
||||
const nodeRes = await listFlowNode({ pageNum: 1, pageSize: 500, tplId: this.tplId })
|
||||
this.flowNodes = (nodeRes.rows || nodeRes.data || []).filter(n => n.nodeType !== 'end').sort((a, b) => (a.orderNo || 0) - (b.orderNo || 0))
|
||||
@@ -356,10 +444,7 @@ export default {
|
||||
},
|
||||
async loadCurrentEmployee () {
|
||||
const userId = this.$store?.state?.user?.id
|
||||
if (!userId) {
|
||||
this.$message.error('无法获取当前用户信息,请重新登录')
|
||||
return
|
||||
}
|
||||
if (!userId) { this.$message.error('无法获取当前用户信息,请重新登录'); return }
|
||||
try {
|
||||
const res = await getEmployeeByUserId(userId)
|
||||
if (res.code === 200 && res.data) {
|
||||
@@ -372,18 +457,13 @@ export default {
|
||||
this.$message.error('加载员工信息失败')
|
||||
}
|
||||
},
|
||||
async onTplChange (val) {
|
||||
this.tplId = val
|
||||
await this.refreshFlowPreview()
|
||||
},
|
||||
async onTplChange (val) { this.tplId = val; await this.refreshFlowPreview() },
|
||||
onApproverModeChange (val) {
|
||||
this.approverMode = val
|
||||
if (val === 'manual') this.tplId = null
|
||||
this.refreshFlowPreview()
|
||||
},
|
||||
openUserSelect () {
|
||||
this.$refs.userSelect.open()
|
||||
},
|
||||
openUserSelect () { this.$refs.userSelect.open() },
|
||||
onUserSelected (row) {
|
||||
if (row) {
|
||||
this.assigneeUserId = row.userId
|
||||
@@ -392,7 +472,6 @@ export default {
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
console.log('提交申请')
|
||||
try {
|
||||
await this.$refs.formRef.validate()
|
||||
if (this.approverMode === 'template' && !this.tplId) {
|
||||
@@ -413,37 +492,40 @@ export default {
|
||||
remark: this.form.remark,
|
||||
status: 'pending',
|
||||
projectId: this.form.projectId,
|
||||
// tplId: this.tplId,
|
||||
manualAssigneeUserId: this.assigneeUserId,
|
||||
payeeName: this.form.payeeName,
|
||||
bankName: this.form.bankName,
|
||||
bankAccount: this.form.bankAccount,
|
||||
invoiceItems: this.invoiceItems.map((item, i) => ({
|
||||
ossId: item.ossId || null,
|
||||
sortNo: i,
|
||||
itemName: item.itemName || '',
|
||||
reason: item.reason || item.itemName || '',
|
||||
amount: item.amount || 0
|
||||
}))
|
||||
}
|
||||
if (this.approverMode === 'template') {
|
||||
payload.tplId = this.tplId
|
||||
}
|
||||
const { data: instance } = await addAppropriationReq(payload)
|
||||
console.log(instance, this.ccForm)
|
||||
if (this.ccForm.selectedUsers.length && instance?.instId) {
|
||||
const ccUserIds = this.ccForm.selectedUsers.map(u => u.userId)
|
||||
const fromUserId = this.$store?.state?.user?.id
|
||||
const payload = {
|
||||
await ccFlowTask({
|
||||
instId: instance.instId,
|
||||
bizId: instance.bizId,
|
||||
bizType: 'appropriation',
|
||||
ccUserIds: ccUserIds,
|
||||
ccUserIds,
|
||||
remark: this.ccForm.remark,
|
||||
fromUserId,
|
||||
nodeId: 0,
|
||||
readFlag: 0,
|
||||
nodeName: '节点#0'
|
||||
}
|
||||
await ccFlowTask(payload)
|
||||
})
|
||||
}
|
||||
this.$message.success('提交成功')
|
||||
this.$router.push('/hrm/apply')
|
||||
} catch (err) {
|
||||
// no-op
|
||||
console.log(err)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
@@ -475,14 +557,8 @@ export default {
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metal-form {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.actions { display: flex; gap: 8px; }
|
||||
.metal-form { padding-right: 8px; }
|
||||
|
||||
.hint-text {
|
||||
margin-top: 6px;
|
||||
@@ -503,32 +579,84 @@ export default {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.summary-title { font-size: 16px; font-weight: 800; color: #2b2f36; }
|
||||
.summary-sub { margin-top: 4px; font-size: 12px; color: #8a8f99; }
|
||||
.summary-right { display: flex; gap: 16px; }
|
||||
.summary-item .k { font-size: 12px; color: #8a8f99; }
|
||||
.summary-item .v { margin-top: 2px; font-weight: 700; color: #2b2f36; }
|
||||
|
||||
.summary-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
|
||||
.summary-right {
|
||||
/* OCR 思考中提示 */
|
||||
.ocr-thinking {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0 12px;
|
||||
border-radius: 8px;
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
i { font-size: 16px; animation: spin 1s linear infinite; }
|
||||
}
|
||||
|
||||
.summary-item .k {
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 发票明细表 */
|
||||
.invoice-table {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invoice-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #e6e8ed;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-item .v {
|
||||
margin-top: 2px;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
.invoice-table-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.col-reason { flex: 1; }
|
||||
.col-amount { width: 140px; flex-shrink: 0; }
|
||||
.col-action { width: 32px; flex-shrink: 0; display: flex; justify-content: center; }
|
||||
|
||||
.invoice-table-header .col-reason { flex: 1; }
|
||||
.invoice-table-header .col-amount { width: 140px; }
|
||||
.invoice-table-header .col-action { width: 32px; }
|
||||
|
||||
.invoice-table-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 10px 12px;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #f0f1f3;
|
||||
}
|
||||
|
||||
.total-hint {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
b { color: #e6a23c; font-size: 14px; }
|
||||
}
|
||||
|
||||
.flow-preview {
|
||||
@@ -539,16 +667,8 @@ export default {
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.flow-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
.flow-title { font-weight: 800; color: #2b2f36; }
|
||||
.flow-sub { margin-top: 4px; font-size: 12px; color: #8a8f99; }
|
||||
|
||||
.flow-steps {
|
||||
margin-top: 10px;
|
||||
@@ -568,28 +688,10 @@ export default {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.flow-step .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #9aa3b2;
|
||||
}
|
||||
|
||||
.flow-step .dot.success {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
.flow-step .txt {
|
||||
font-size: 12px;
|
||||
color: #2b2f36;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-steps .line {
|
||||
width: 26px;
|
||||
height: 1px;
|
||||
background: #e6e8ed;
|
||||
}
|
||||
.flow-step .dot { width: 8px; height: 8px; border-radius: 50%; background: #9aa3b2; }
|
||||
.flow-step .dot.success { background: #67c23a; }
|
||||
.flow-step .txt { font-size: 12px; color: #2b2f36; font-weight: 600; }
|
||||
.flow-steps .line { width: 26px; height: 1px; background: #e6e8ed; }
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
@@ -599,9 +701,7 @@ export default {
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.summary-right {
|
||||
display: none;
|
||||
}
|
||||
.summary-right { display: none; }
|
||||
}
|
||||
|
||||
.block-title {
|
||||
@@ -612,6 +712,13 @@ export default {
|
||||
border-left: 3px solid #9aa3b2;
|
||||
}
|
||||
|
||||
.block-title-hint {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #8a8f99;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.approve-mode {
|
||||
padding: 12px;
|
||||
border: 1px solid #e6e8ed;
|
||||
@@ -619,18 +726,9 @@ export default {
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.approve-panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.approve-panel { margin-top: 12px; }
|
||||
.approve-row { display: flex; align-items: center; gap: 12px; }
|
||||
.approve-row .k { font-size: 14px; color: #606266; }
|
||||
|
||||
.approve-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approve-row .k {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.selected-users { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,21 @@
|
||||
<file-preview v-model="detail.accessoryApplyIds"></file-preview>
|
||||
</el-card>
|
||||
|
||||
<!-- 发票明细 -->
|
||||
<template v-if="detail.invoiceItems && detail.invoiceItems.length">
|
||||
<div class="block-title">发票明细</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
<el-table :data="detail.invoiceItems" border size="small" style="width:100%">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="itemName" label="项目名称" min-width="140" />
|
||||
<el-table-column prop="reason" label="事由" min-width="180" />
|
||||
<el-table-column prop="amount" label="金额(元)" width="120" align="right">
|
||||
<template slot-scope="{ row }">¥{{ row.amount }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<!-- 拨款理由说明 -->
|
||||
<div class="block-title">拨款理由说明</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
|
||||
@@ -47,20 +47,64 @@
|
||||
<el-col>
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
<project-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%" />
|
||||
<!-- <el-input v-model="form.projectId" placeholder="请输入项目ID" style="width: 100%" /> -->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="报销事由" prop="reason">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="4" placeholder="请说明报销事由、费用用途等" show-word-limit
|
||||
maxlength="3000" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 报销单据附件(OCR触发区) -->
|
||||
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
|
||||
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
|
||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
|
||||
<div class="hint-text">上传发票、收据、付款截图等(必填)</div>
|
||||
<div class="hint-text">上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别)</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- OCR 识别中提示 -->
|
||||
<div v-if="anyOcrLoading" class="ocr-thinking">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>模型思考中,正在识别发票内容…</span>
|
||||
</div>
|
||||
|
||||
<!-- 发票明细条目表 -->
|
||||
<div class="block-title">
|
||||
发票明细
|
||||
<span class="block-title-hint">(上传发票后自动填充,也可手动添加)</span>
|
||||
</div>
|
||||
<div class="invoice-table">
|
||||
<div class="invoice-table-header">
|
||||
<span class="col-reason">事由说明</span>
|
||||
<span class="col-amount">金额(元)</span>
|
||||
<span class="col-action"></span>
|
||||
</div>
|
||||
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
||||
<el-input
|
||||
v-model="item.reason"
|
||||
:placeholder="item.itemName || '请填写事由'"
|
||||
size="small"
|
||||
class="col-reason"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="item.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
size="small"
|
||||
class="col-amount"
|
||||
@change="recalcTotal"
|
||||
/>
|
||||
<div class="col-action">
|
||||
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-table-footer">
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addInvoiceItem">添加条目</el-button>
|
||||
<span class="total-hint" v-if="invoiceItems.length">
|
||||
合计:<b>¥{{ invoiceTotalFormatted }}</b>(已自动更新报销总金额)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item label="补充说明" prop="reason">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="2" placeholder="可选:填写整体说明或补充" show-word-limit
|
||||
maxlength="3000" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="回执附件(可选)" prop="accessoryReceiptIds">
|
||||
@@ -83,14 +127,13 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 抄送备注 -->
|
||||
<el-form-item label="抄送备注" prop="ccRemark">
|
||||
<el-input v-model="ccForm.remark" type="textarea" :rows="2" placeholder="可以填写抄送的目的或原因等信息" show-word-limit
|
||||
maxlength="1000" />
|
||||
</el-form-item>
|
||||
<UserMultiSelect ref="userMultiSelect" :init="ccUserIds" @onSelected="onCcUsersSelected" />
|
||||
|
||||
<!-- 审批方式(模板/自选审批人) -->
|
||||
<!-- 审批方式 -->
|
||||
<div class="block-title">审批方式</div>
|
||||
<div class="approve-mode">
|
||||
<el-radio-group v-model="approverMode" size="small" @change="onApproverModeChange">
|
||||
@@ -140,18 +183,11 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 模板模式 -->
|
||||
<div v-if="approverMode === 'template'">
|
||||
<div v-if="flowNodes && flowNodes.length" class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">填写申请</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">填写申请</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">提交</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">提交</div></div>
|
||||
<template v-for="(n, idx) in flowNodes">
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
@@ -165,22 +201,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动审批模式 -->
|
||||
<div v-else class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">填写申请</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">填写申请</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot"></div>
|
||||
<div class="txt">提交审批({{ assigneeUserName || '请选择' }})</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot"></div><div class="txt">提交审批({{ assigneeUserName || '请选择' }})</div></div>
|
||||
<div class="line"></div>
|
||||
<div class="flow-step">
|
||||
<div class="dot success"></div>
|
||||
<div class="txt">审批结束</div>
|
||||
</div>
|
||||
<div class="flow-step"><div class="dot success"></div><div class="txt">审批结束</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -190,14 +216,13 @@
|
||||
</div>
|
||||
|
||||
<UserSelect ref="userSelect" @onSelected="onUserSelected" />
|
||||
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addReimburseReq, listFlowNode, listFlowTemplate } from '@/api/hrm';
|
||||
import { addReimburseReq, listFlowNode, listFlowTemplate, ocrReimburseInvoice } from '@/api/hrm';
|
||||
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
||||
import { ccFlowTask } from '@/api/hrm/flow';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
@@ -220,6 +245,12 @@ export default {
|
||||
assigneeUserId: null,
|
||||
assigneeUserName: '',
|
||||
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
||||
// 发票明细条目
|
||||
invoiceItems: [],
|
||||
// OCR加载状态 { ossId: true/false }
|
||||
ocrLoadingMap: {},
|
||||
// 已触发过OCR的ossId集合
|
||||
ocrDoneSet: new Set(),
|
||||
form: {
|
||||
empId: '',
|
||||
reimburseType: '',
|
||||
@@ -230,7 +261,6 @@ export default {
|
||||
remark: ''
|
||||
},
|
||||
ccForm: {
|
||||
// 默认抄送胡雪娇
|
||||
selectedUsers: [{
|
||||
userId: '1859249502579310593',
|
||||
nickName: '胡雪娇',
|
||||
@@ -245,7 +275,6 @@ export default {
|
||||
rules: {
|
||||
reimburseType: [{ required: true, message: '请选择/输入报销类型', trigger: 'change' }],
|
||||
totalAmount: [{ required: true, message: '请填写报销总金额', trigger: 'blur' }],
|
||||
reason: [{ required: true, message: '请填写报销事由', trigger: 'blur' }],
|
||||
accessoryApplyIds: [{ required: true, message: '请上传报销单据附件', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
@@ -269,11 +298,80 @@ export default {
|
||||
ccUserIds () {
|
||||
return this.ccForm.selectedUsers?.map(u => u.userId) || []
|
||||
},
|
||||
anyOcrLoading () {
|
||||
return Object.values(this.ocrLoadingMap).some(v => v)
|
||||
},
|
||||
invoiceTotalFormatted () {
|
||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||
return total.toFixed(2)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'form.accessoryApplyIds' (newVal, oldVal) {
|
||||
const newIds = newVal ? newVal.split(',').filter(Boolean) : []
|
||||
const oldIds = oldVal ? oldVal.split(',').filter(Boolean) : []
|
||||
const added = newIds.filter(id => !oldIds.includes(id))
|
||||
added.forEach(id => this.triggerOcr(id))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openUserMultiSelect () { this.$refs.userMultiSelect.open() },
|
||||
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
||||
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
||||
|
||||
async triggerOcr (ossId) {
|
||||
if (this.ocrDoneSet.has(ossId)) return
|
||||
this.$set(this.ocrLoadingMap, ossId, true)
|
||||
try {
|
||||
const res = await ocrReimburseInvoice(ossId)
|
||||
if (res.code === 200 && res.data) {
|
||||
const { items, totalAmount } = res.data
|
||||
if (items && items.length) {
|
||||
const startIdx = this.invoiceItems.length
|
||||
items.forEach((item, i) => {
|
||||
this.invoiceItems.push({
|
||||
ossId: Number(ossId),
|
||||
itemName: item.itemName || '',
|
||||
reason: item.itemName || '',
|
||||
amount: item.amount || 0,
|
||||
sortNo: startIdx + i
|
||||
})
|
||||
})
|
||||
} else if (totalAmount) {
|
||||
// 没有明细时用总金额创建一条
|
||||
this.invoiceItems.push({
|
||||
ossId: Number(ossId),
|
||||
itemName: '',
|
||||
reason: '',
|
||||
amount: totalAmount,
|
||||
sortNo: this.invoiceItems.length
|
||||
})
|
||||
}
|
||||
this.recalcTotal()
|
||||
}
|
||||
this.ocrDoneSet.add(ossId)
|
||||
} catch (e) {
|
||||
console.error('[OCR] 识别失败', e)
|
||||
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
||||
} finally {
|
||||
this.$set(this.ocrLoadingMap, ossId, false)
|
||||
}
|
||||
},
|
||||
|
||||
addInvoiceItem () {
|
||||
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||
},
|
||||
|
||||
removeInvoiceItem (idx) {
|
||||
this.invoiceItems.splice(idx, 1)
|
||||
this.recalcTotal()
|
||||
},
|
||||
|
||||
recalcTotal () {
|
||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||
this.form.totalAmount = parseFloat(total.toFixed(2))
|
||||
},
|
||||
|
||||
async loadTemplates () {
|
||||
try {
|
||||
const res = await listFlowTemplate({ pageNum: 1, pageSize: 200, bizType: 'reimburse', enabled: 1 })
|
||||
@@ -289,16 +387,8 @@ export default {
|
||||
async refreshFlowPreview () {
|
||||
this.flowLoading = true
|
||||
try {
|
||||
if (this.approverMode === 'manual') {
|
||||
this.flowTpl = null
|
||||
this.flowNodes = []
|
||||
return
|
||||
}
|
||||
if (!this.tplId) {
|
||||
this.flowTpl = null
|
||||
this.flowNodes = []
|
||||
return
|
||||
}
|
||||
if (this.approverMode === 'manual') { this.flowTpl = null; this.flowNodes = []; return }
|
||||
if (!this.tplId) { this.flowTpl = null; this.flowNodes = []; return }
|
||||
this.flowTpl = this.availableTpls.find(t => t.tplId === this.tplId) || null
|
||||
const nodeRes = await listFlowNode({ pageNum: 1, pageSize: 500, tplId: this.tplId })
|
||||
this.flowNodes = (nodeRes.rows || nodeRes.data || []).filter(n => n.nodeType !== 'end').sort((a, b) => (a.orderNo || 0) - (b.orderNo || 0))
|
||||
@@ -330,10 +420,7 @@ export default {
|
||||
},
|
||||
async loadCurrentEmployee () {
|
||||
const userId = this.$store?.state?.user?.id
|
||||
if (!userId) {
|
||||
this.$message.error('无法获取当前用户信息,请重新登录')
|
||||
return
|
||||
}
|
||||
if (!userId) { this.$message.error('无法获取当前用户信息,请重新登录'); return }
|
||||
try {
|
||||
const res = await getEmployeeByUserId(userId)
|
||||
if (res.code === 200 && res.data) {
|
||||
@@ -346,18 +433,13 @@ export default {
|
||||
this.$message.error('加载员工信息失败')
|
||||
}
|
||||
},
|
||||
async onTplChange (val) {
|
||||
this.tplId = val
|
||||
await this.refreshFlowPreview()
|
||||
},
|
||||
async onTplChange (val) { this.tplId = val; await this.refreshFlowPreview() },
|
||||
onApproverModeChange (val) {
|
||||
this.approverMode = val
|
||||
if (val === 'manual') this.tplId = null
|
||||
this.refreshFlowPreview()
|
||||
},
|
||||
openUserSelect () {
|
||||
this.$refs.userSelect.open()
|
||||
},
|
||||
openUserSelect () { this.$refs.userSelect.open() },
|
||||
onUserSelected (row) {
|
||||
if (row) {
|
||||
this.assigneeUserId = row.userId
|
||||
@@ -366,7 +448,6 @@ export default {
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
console.log('提交申请')
|
||||
try {
|
||||
await this.$refs.formRef.validate()
|
||||
if (this.approverMode === 'template' && !this.tplId) {
|
||||
@@ -387,29 +468,33 @@ export default {
|
||||
remark: this.form.remark,
|
||||
status: 'pending',
|
||||
projectId: this.form.projectId,
|
||||
// tplId: this.tplId,
|
||||
manualAssigneeUserId: this.assigneeUserId
|
||||
manualAssigneeUserId: this.assigneeUserId,
|
||||
invoiceItems: this.invoiceItems.map((item, i) => ({
|
||||
ossId: item.ossId || null,
|
||||
sortNo: i,
|
||||
itemName: item.itemName || '',
|
||||
reason: item.reason || item.itemName || '',
|
||||
amount: item.amount || 0
|
||||
}))
|
||||
}
|
||||
if (this.approverMode === 'template') {
|
||||
payload.tplId = this.tplId
|
||||
}
|
||||
const { data: instance } = await addReimburseReq(payload)
|
||||
console.log(instance, this.ccForm)
|
||||
if (this.ccForm.selectedUsers.length && instance?.instId) {
|
||||
const ccUserIds = this.ccForm.selectedUsers.map(u => u.userId)
|
||||
const fromUserId = this.$store?.state?.user?.id
|
||||
const payload = {
|
||||
await ccFlowTask({
|
||||
instId: instance.instId,
|
||||
bizId: instance.bizId,
|
||||
bizType: 'reimburse',
|
||||
ccUserIds: ccUserIds,
|
||||
ccUserIds,
|
||||
remark: this.ccForm.remark,
|
||||
fromUserId,
|
||||
nodeId: 0,
|
||||
readFlag: 0,
|
||||
nodeName: '节点#0'
|
||||
}
|
||||
await ccFlowTask(payload)
|
||||
})
|
||||
}
|
||||
this.$message.success('提交成功')
|
||||
this.$router.push('/hrm/apply')
|
||||
@@ -445,14 +530,9 @@ export default {
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.actions { display: flex; gap: 8px; }
|
||||
|
||||
.metal-form {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.metal-form { padding-right: 8px; }
|
||||
|
||||
.hint-text {
|
||||
margin-top: 6px;
|
||||
@@ -473,32 +553,97 @@ export default {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.summary-title { font-size: 16px; font-weight: 800; color: #2b2f36; }
|
||||
.summary-sub { margin-top: 4px; font-size: 12px; color: #8a8f99; }
|
||||
.summary-right { display: flex; gap: 16px; }
|
||||
.summary-item .k { font-size: 12px; color: #8a8f99; }
|
||||
.summary-item .v { margin-top: 2px; font-weight: 700; color: #2b2f36; }
|
||||
|
||||
.summary-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
|
||||
.summary-right {
|
||||
/* OCR 思考中提示 */
|
||||
.ocr-thinking {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0 12px;
|
||||
border-radius: 8px;
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
i { font-size: 16px; animation: spin 1s linear infinite; }
|
||||
}
|
||||
|
||||
.summary-item .k {
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 发票明细表 */
|
||||
.invoice-table {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invoice-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #e6e8ed;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-item .v {
|
||||
margin-top: 2px;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
.invoice-table-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.col-reason {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.col-amount {
|
||||
width: 140px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.col-action {
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.invoice-table-header .col-reason { flex: 1; }
|
||||
.invoice-table-header .col-amount { width: 140px; }
|
||||
.invoice-table-header .col-action { width: 32px; }
|
||||
|
||||
.invoice-table-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 10px 12px;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #f0f1f3;
|
||||
}
|
||||
|
||||
.total-hint {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
|
||||
b { color: #e6a23c; font-size: 14px; }
|
||||
}
|
||||
|
||||
.flow-preview {
|
||||
@@ -509,16 +654,8 @@ export default {
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.flow-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
.flow-title { font-weight: 800; color: #2b2f36; }
|
||||
.flow-sub { margin-top: 4px; font-size: 12px; color: #8a8f99; }
|
||||
|
||||
.flow-steps {
|
||||
margin-top: 10px;
|
||||
@@ -545,21 +682,9 @@ export default {
|
||||
background: #9aa3b2;
|
||||
}
|
||||
|
||||
.flow-step .dot.success {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
.flow-step .txt {
|
||||
font-size: 12px;
|
||||
color: #2b2f36;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-steps .line {
|
||||
width: 26px;
|
||||
height: 1px;
|
||||
background: #e6e8ed;
|
||||
}
|
||||
.flow-step .dot.success { background: #67c23a; }
|
||||
.flow-step .txt { font-size: 12px; color: #2b2f36; font-weight: 600; }
|
||||
.flow-steps .line { width: 26px; height: 1px; background: #e6e8ed; }
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
@@ -569,9 +694,7 @@ export default {
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.summary-right {
|
||||
display: none;
|
||||
}
|
||||
.summary-right { display: none; }
|
||||
}
|
||||
|
||||
.block-title {
|
||||
@@ -582,6 +705,13 @@ export default {
|
||||
border-left: 3px solid #9aa3b2;
|
||||
}
|
||||
|
||||
.block-title-hint {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #8a8f99;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.approve-mode {
|
||||
padding: 12px;
|
||||
border: 1px solid #e6e8ed;
|
||||
@@ -589,18 +719,9 @@ export default {
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.approve-panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.approve-panel { margin-top: 12px; }
|
||||
.approve-row { display: flex; align-items: center; gap: 12px; }
|
||||
.approve-row .k { font-size: 14px; color: #606266; }
|
||||
|
||||
.approve-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approve-row .k {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.selected-users { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
<file-preview v-model="detail.accessoryApplyIds"></file-preview>
|
||||
</el-card>
|
||||
|
||||
<!-- 发票明细 -->
|
||||
<template v-if="detail.invoiceItems && detail.invoiceItems.length">
|
||||
<div class="block-title">发票明细</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
<el-table :data="detail.invoiceItems" border size="small" style="width:100%">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="itemName" label="项目名称" min-width="140" />
|
||||
<el-table-column prop="reason" label="事由" min-width="180" />
|
||||
<el-table-column prop="amount" label="金额(元)" width="120" align="right">
|
||||
<template slot-scope="{ row }">¥{{ row.amount }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<!-- 报销理由说明 -->
|
||||
<div class="block-title">报销理由说明</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
|
||||
Reference in New Issue
Block a user