804 lines
28 KiB
Vue
804 lines
28 KiB
Vue
<template>
|
||
<div class="request-page">
|
||
<el-card class="form-card" shadow="never">
|
||
<div slot="header" class="card-header">
|
||
<span>日常报销申请</span>
|
||
<div class="actions">
|
||
<el-button size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
|
||
<div class="form-summary">
|
||
<div class="summary-left">
|
||
<div class="summary-title">发起日常报销</div>
|
||
<div class="summary-sub">请完善信息后提交,系统将按流程节点流转</div>
|
||
</div>
|
||
<div class="summary-right">
|
||
<div class="summary-item">
|
||
<div class="k">申请人</div>
|
||
<div class="v">{{ currentApplicantText }}</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="k">报销金额</div>
|
||
<div class="v">{{ form.totalAmount != null ? '¥' + form.totalAmount : '-' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-row :gutter="14">
|
||
<el-col :span="12">
|
||
<el-form-item label="报销类型" prop="reimburseType">
|
||
<el-select v-model="form.reimburseType" filterable allow-create default-first-option clearable
|
||
placeholder="选择或输入(如:差旅费/招待费/办公费)" style="width: 100%">
|
||
<el-option v-for="t in reimburseTypeOptions" :key="t" :label="t" :value="t" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="报销总金额">
|
||
<div class="amount-readonly">
|
||
{{ form.totalAmount != null && form.totalAmount > 0 ? '¥' + form.totalAmount : '根据发票明细自动汇总' }}
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<div class="block-title">绑定项目</div>
|
||
<el-row>
|
||
<el-col>
|
||
<el-form-item label="项目" prop="projectId">
|
||
<project-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 报销单据附件(仅 PDF 电子发票) -->
|
||
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
|
||
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
|
||
:file-type="['pdf']" multiple
|
||
@delete="onFileDelete" />
|
||
<div class="hint-text">
|
||
仅支持 PDF 电子发票(含数电票/电子普通发票/电子专用发票),上传后自动解析金额与明细。<br/>
|
||
扫描件 / 图片票 / 纸质票请先在开票平台下载 PDF 原件再上传。
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<!-- 发票解析中提示 -->
|
||
<div v-if="anyOcrLoading" class="ocr-thinking">
|
||
<i class="el-icon-loading"></i>
|
||
<span>正在解析发票 PDF…</span>
|
||
</div>
|
||
|
||
<!-- 发票明细条目表 -->
|
||
<div class="block-title">
|
||
发票明细
|
||
<span class="block-title-hint">(上传 PDF 后自动解析;解析失败或无发票时可手动添加)</span>
|
||
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addManualItem" style="margin-left:12px;vertical-align:middle">手动添加条目</el-button>
|
||
</div>
|
||
<div class="invoice-table" v-if="invoiceItems.length">
|
||
<div class="invoice-table-header">
|
||
<span class="col-reason">事由说明</span>
|
||
<span class="col-amount">金额(元)</span>
|
||
<span class="col-file">附件</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-file">
|
||
<el-tooltip v-if="item.ossId" content="已关联附件" placement="top">
|
||
<i class="el-icon-paperclip" style="color:#409eff"></i>
|
||
</el-tooltip>
|
||
<el-upload v-else :action="uploadFileUrl" :headers="uploadHeaders" :show-file-list="false"
|
||
:data="{ isPublic: 1 }" :on-success="(res) => onRowFileSuccess(res, idx)"
|
||
accept=".pdf" class="row-upload">
|
||
<el-button size="mini" type="text" icon="el-icon-paperclip"></el-button>
|
||
</el-upload>
|
||
</div>
|
||
<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">
|
||
<span class="total-hint">
|
||
合计:<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">
|
||
<file-upload v-model="form.accessoryReceiptIds" :limit="200" :file-size="50"
|
||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
|
||
<div class="hint-text">可选:上传回执、对账单等(归档用)</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="备注">
|
||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选:补充说明" show-word-limit
|
||
maxlength="200" />
|
||
</el-form-item>
|
||
|
||
<div class="block-title">抄送</div>
|
||
<el-form-item label="抄送目标" prop="ccUserIds">
|
||
<el-button size="mini" type="primary" plain icon="el-icon-user" @click="openUserMultiSelect">选择抄送人</el-button>
|
||
<div class="selected-users" v-if="ccForm.selectedUsers && ccForm.selectedUsers.length">
|
||
<el-tag v-for="u in ccForm.selectedUsers" :key="u.userId" size="mini" closable @close="removeCcUser(u)">
|
||
{{ u.nickName || u.userName || ('ID:' + u.userId) }}
|
||
</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">
|
||
<el-radio-button label="template">使用模板流程</el-radio-button>
|
||
<el-radio-button label="manual">手动选择审批人(一次审批)</el-radio-button>
|
||
</el-radio-group>
|
||
|
||
<div class="approve-panel">
|
||
<div v-if="approverMode === 'template'">
|
||
<div class="approve-row">
|
||
<div class="k">流程模板</div>
|
||
<div class="v">
|
||
<el-select v-model="tplId" size="small" clearable filterable placeholder="请选择流程模板"
|
||
style="width: 360px" @change="onTplChange">
|
||
<el-option v-for="t in availableTpls" :key="t.tplId"
|
||
:label="`${t.tplName}${t.version ? ' (v' + t.version + ')' : ''}`" :value="t.tplId" />
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
<div class="hint-text">提示:选择模板后,将按模板节点自动流转(含抄送节点)。</div>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<div class="approve-row">
|
||
<div class="k">审批人</div>
|
||
<div class="v" style="max-width: 520px">
|
||
<el-button size="mini" type="primary" plain @click="openUserSelect">选择审批人</el-button>
|
||
<span style="margin-left: 10px; font-weight: 600; color: #2b2f36">
|
||
{{ assigneeUserName || '未选择' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-text">提示:手动选择审批人将创建一次性审批流程,审批通过后流程立即结束。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-preview" v-loading="flowLoading">
|
||
<div class="flow-title">流程预览</div>
|
||
<div class="flow-sub">
|
||
<template v-if="approverMode === 'template'">
|
||
<span v-if="flowTpl">当前模板:{{ flowTpl.tplName }}(v{{ flowTpl.version || 1 }})</span>
|
||
<span v-else>请选择流程模板</span>
|
||
</template>
|
||
<template v-else>
|
||
<span>一次性审批(手动指定审批人)</span>
|
||
</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="line"></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">
|
||
<div class="dot" :class="{ success: idx === flowNodes.length - 1 }"></div>
|
||
<div class="txt">{{ nodePreviewText(n, idx) }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div v-else class="flow-fallback">
|
||
<div class="hint-text">提示:请选择一个模板后将展示对应节点预览。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="flow-steps">
|
||
<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="line"></div>
|
||
<div class="flow-step"><div class="dot success"></div><div class="txt">审批结束</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<el-button @click="$router.back()">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submit">提交申请</el-button>
|
||
</div>
|
||
|
||
<UserSelect ref="userSelect" @onSelected="onUserSelected" />
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
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';
|
||
import { getToken } from '@/utils/auth';
|
||
import UserMultiSelect from '@/components/UserSelect/multi.vue';
|
||
import UserSelect from '@/components/UserSelect/single.vue';
|
||
import approverNameMixin from '@/views/hrm/minix/approverNameMixin.js';
|
||
|
||
export default {
|
||
mixins: [approverNameMixin],
|
||
name: 'HrmReimburseRequest',
|
||
components: { FileUpload, UserSelect, UserMultiSelect },
|
||
data () {
|
||
return {
|
||
currentEmp: null,
|
||
submitting: false,
|
||
flowLoading: false,
|
||
flowTpl: null,
|
||
flowNodes: [],
|
||
approverMode: 'template',
|
||
availableTpls: [],
|
||
tplId: null,
|
||
assigneeUserId: null,
|
||
assigneeUserName: '',
|
||
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
||
uploadFileUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
|
||
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
|
||
// 发票明细条目
|
||
invoiceItems: [],
|
||
// OCR加载状态 { ossId: true/false }
|
||
ocrLoadingMap: {},
|
||
// 已触发过OCR的ossId集合
|
||
ocrDoneSet: new Set(),
|
||
form: {
|
||
empId: '',
|
||
reimburseType: '',
|
||
totalAmount: 0,
|
||
reason: '',
|
||
accessoryApplyIds: '',
|
||
accessoryReceiptIds: '',
|
||
remark: ''
|
||
},
|
||
ccForm: {
|
||
selectedUsers: [{
|
||
userId: '1859249502579310593',
|
||
nickName: '胡雪娇',
|
||
userName: 'huangxuejiao'
|
||
}, {
|
||
userId: '1859252208375152641',
|
||
nickName: '高伟',
|
||
userName: 'gaowei'
|
||
}],
|
||
remark: ''
|
||
},
|
||
rules: {
|
||
reimburseType: [{ required: true, message: '请选择/输入报销类型', trigger: 'change' }],
|
||
totalAmount: [],
|
||
accessoryApplyIds: [{ required: true, message: '请上传报销单据附件', trigger: 'change' }]
|
||
}
|
||
}
|
||
},
|
||
created () {
|
||
this.loadCurrentEmployee()
|
||
this.loadTemplates()
|
||
this.loadUserNameMap()
|
||
},
|
||
computed: {
|
||
currentApplicantText () {
|
||
if (this.currentEmp) {
|
||
const emp = this.currentEmp
|
||
const name = emp.empName || emp.nickName || emp.userName || ''
|
||
const no = emp.empNo ? ` · ${emp.empNo}` : ''
|
||
const dept = emp.deptName ? ` · ${emp.deptName}` : ''
|
||
return `${name || '员工'}${no}${dept}`.trim()
|
||
}
|
||
const user = this.$store?.state?.user || {}
|
||
return user.nickName || user.userName || '加载中...'
|
||
},
|
||
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.ocrDoneSet.add(ossId)
|
||
this.$set(this.ocrLoadingMap, ossId, true)
|
||
try {
|
||
const res = await ocrReimburseInvoice(ossId)
|
||
if (res.code === 200 && res.data) {
|
||
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
|
||
// 拼接发票头部信息作为事由前缀:发票类型 · 销售方 · 开票日期
|
||
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
|
||
if (items && items.length) {
|
||
const startIdx = this.invoiceItems.length
|
||
items.forEach((item, i) => {
|
||
const reason = [prefix, item.itemName].filter(Boolean).join(' / ')
|
||
this.invoiceItems.push({
|
||
ossId: ossId,
|
||
itemName: item.itemName || '',
|
||
reason,
|
||
amount: item.amount || 0,
|
||
sortNo: startIdx + i
|
||
})
|
||
})
|
||
} else if (totalAmount) {
|
||
// 没有明细时用总金额创建一条,事由取发票头部信息
|
||
this.invoiceItems.push({
|
||
ossId: ossId,
|
||
itemName: sellerName || '',
|
||
reason: prefix || '',
|
||
amount: totalAmount,
|
||
sortNo: this.invoiceItems.length
|
||
})
|
||
} else {
|
||
// OCR返回无内容,添加空条目供手动填写
|
||
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||
}
|
||
this.recalcTotal()
|
||
} else {
|
||
// 接口无有效数据,添加空条目供手动填写
|
||
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||
}
|
||
} catch (e) {
|
||
console.error('[Invoice] 解析失败', e)
|
||
const msg = (e && e.msg) || (e && e.message) || ''
|
||
this.$message.warning(
|
||
'发票解析失败,请确认上传的是开票平台下载的正规 PDF 电子发票(数电票 / 电子普通发票 / 电子专用发票)。' +
|
||
(msg ? ' 错误信息:' + msg : '')
|
||
)
|
||
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||
} finally {
|
||
this.$set(this.ocrLoadingMap, ossId, false)
|
||
}
|
||
},
|
||
|
||
removeInvoiceItem (idx) {
|
||
const item = this.invoiceItems[idx]
|
||
if (item.ossId) {
|
||
// 删除该文件下所有条目
|
||
const ossId = String(item.ossId)
|
||
this.invoiceItems = this.invoiceItems.filter(it => String(it.ossId) !== ossId)
|
||
// 从附件 CSV 中移除该文件
|
||
const ids = (this.form.accessoryApplyIds || '').split(',').filter(id => id && id !== ossId)
|
||
this.form.accessoryApplyIds = ids.join(',')
|
||
} else {
|
||
this.invoiceItems.splice(idx, 1)
|
||
}
|
||
this.recalcTotal()
|
||
},
|
||
|
||
onFileDelete (deletedFile) {
|
||
const ossId = String(deletedFile.ossId)
|
||
this.invoiceItems = this.invoiceItems.filter(item => String(item.ossId) !== ossId)
|
||
this.recalcTotal()
|
||
},
|
||
|
||
addManualItem () {
|
||
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||
},
|
||
|
||
onRowFileSuccess (res, idx) {
|
||
if (res.code === 200 && res.data) {
|
||
const ossId = res.data.ossId
|
||
this.ocrDoneSet.add(ossId)
|
||
this.$set(this.invoiceItems[idx], 'ossId', ossId)
|
||
const ids = (this.form.accessoryApplyIds || '').split(',').filter(Boolean)
|
||
ids.push(ossId)
|
||
this.form.accessoryApplyIds = ids.join(',')
|
||
}
|
||
},
|
||
|
||
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 })
|
||
this.availableTpls = res.rows || res.data || []
|
||
if (!this.tplId && this.availableTpls.length) {
|
||
this.tplId = this.availableTpls[0].tplId
|
||
}
|
||
await this.refreshFlowPreview()
|
||
} catch (err) {
|
||
this.availableTpls = []
|
||
}
|
||
},
|
||
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 }
|
||
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))
|
||
} finally {
|
||
this.flowLoading = false
|
||
}
|
||
},
|
||
normalizeOssIds (val) {
|
||
if (!val) return ''
|
||
if (typeof val === 'string') return val
|
||
if (Array.isArray(val)) {
|
||
const ids = val.map(x => (x && typeof x === 'object') ? (x.ossId ?? x.id ?? x.value) : x).filter(Boolean)
|
||
return ids.join(',')
|
||
}
|
||
return String(val)
|
||
},
|
||
async loadCurrentEmployee () {
|
||
const userId = this.$store?.state?.user?.id
|
||
if (!userId) { this.$message.error('无法获取当前用户信息,请重新登录'); return }
|
||
try {
|
||
const res = await getEmployeeByUserId(userId)
|
||
if (res.code === 200 && res.data) {
|
||
this.currentEmp = res.data
|
||
this.form.empId = res.data.empId
|
||
} else {
|
||
this.$message.error('未找到当前用户对应的员工信息,请在员工管理中关联系统用户')
|
||
}
|
||
} catch (e) {
|
||
this.$message.error('加载员工信息失败')
|
||
}
|
||
},
|
||
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() },
|
||
onUserSelected (row) {
|
||
if (row) {
|
||
this.assigneeUserId = row.userId
|
||
this.assigneeUserName = row.nickName || row.userName || row.userId
|
||
this.refreshFlowPreview()
|
||
}
|
||
},
|
||
async submit () {
|
||
try {
|
||
await this.$refs.formRef.validate()
|
||
if (this.approverMode === 'template' && !this.tplId) {
|
||
return this.$message.warning('请选择一个流程模板')
|
||
}
|
||
if (this.approverMode === 'manual' && !this.assigneeUserId) {
|
||
return this.$message.warning('请选择审批人')
|
||
}
|
||
|
||
this.submitting = true
|
||
const payload = {
|
||
empId: this.form.empId,
|
||
reimburseType: this.form.reimburseType,
|
||
totalAmount: this.form.totalAmount,
|
||
reason: this.form.reason,
|
||
accessoryApplyIds: this.normalizeOssIds(this.form.accessoryApplyIds),
|
||
accessoryReceiptIds: this.normalizeOssIds(this.form.accessoryReceiptIds),
|
||
remark: this.form.remark,
|
||
status: 'pending',
|
||
projectId: this.form.projectId,
|
||
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)
|
||
if (this.ccForm.selectedUsers.length && instance?.instId) {
|
||
const ccUserIds = this.ccForm.selectedUsers.map(u => u.userId)
|
||
const fromUserId = this.$store?.state?.user?.id
|
||
await ccFlowTask({
|
||
instId: instance.instId,
|
||
bizId: instance.bizId,
|
||
bizType: 'reimburse',
|
||
ccUserIds,
|
||
remark: this.ccForm.remark,
|
||
fromUserId,
|
||
nodeId: 0,
|
||
readFlag: 0,
|
||
nodeName: '节点#0'
|
||
})
|
||
}
|
||
this.$message.success('提交成功')
|
||
this.$router.push('/hrm/apply')
|
||
} catch (err) {
|
||
// no-op
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.request-page {
|
||
padding: 16px 20px 32px;
|
||
background: #f8f9fb;
|
||
}
|
||
|
||
.form-card {
|
||
max-width: 980px;
|
||
margin: 0 auto;
|
||
border: 1px solid #d7d9df;
|
||
border-radius: 12px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 700;
|
||
color: #2b2f36;
|
||
}
|
||
|
||
.actions { display: flex; gap: 8px; }
|
||
|
||
.metal-form { padding-right: 8px; }
|
||
|
||
.hint-text {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #8a8f99;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.form-summary {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 12px;
|
||
margin-bottom: 12px;
|
||
border: 1px solid #e6e8ed;
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
|
||
}
|
||
|
||
.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; }
|
||
|
||
/* OCR 思考中提示 */
|
||
.ocr-thinking {
|
||
display: flex;
|
||
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; }
|
||
}
|
||
|
||
@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: #606266;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.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-file {
|
||
width: 64px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.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-file { width: 64px; }
|
||
.invoice-table-header .col-action { width: 32px; }
|
||
|
||
.row-upload { display: inline-flex; }
|
||
|
||
.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; }
|
||
}
|
||
|
||
.amount-readonly {
|
||
height: 32px;
|
||
line-height: 32px;
|
||
padding: 0 12px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #e6a23c;
|
||
background: #fffbf2;
|
||
border: 1px solid #faecd8;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.flow-preview {
|
||
margin-top: 10px;
|
||
padding: 12px;
|
||
border: 1px solid #e6e8ed;
|
||
border-radius: 10px;
|
||
background: #fcfdff;
|
||
}
|
||
|
||
.flow-title { font-weight: 800; color: #2b2f36; }
|
||
.flow-sub { margin-top: 4px; font-size: 12px; color: #8a8f99; }
|
||
|
||
.flow-steps {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.flow-step {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid #e6e8ed;
|
||
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; }
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.summary-right { display: none; }
|
||
}
|
||
|
||
.block-title {
|
||
margin: 20px 0 12px;
|
||
padding-left: 10px;
|
||
font-weight: 700;
|
||
color: #2f3440;
|
||
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;
|
||
border-radius: 10px;
|
||
background: #fcfdff;
|
||
}
|
||
|
||
.approve-panel { margin-top: 12px; }
|
||
.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; }
|
||
|
||
@import "./_form-compact.scss";
|
||
</style>
|