Files
fad_oa/ruoyi-ui/src/views/hrm/requests/appropriation.vue

815 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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.amount != null ? '¥' + form.amount : '-' }}</div>
</div>
</div>
</div>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="拨款类型" prop="appropriationType">
<el-select v-model="form.appropriationType" filterable allow-create default-first-option clearable
placeholder="选择或输入(如:差旅费/招待费/办公费)" style="width: 100%">
<el-option v-for="t in appropriationTypeOptions" :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.amount != null && form.amount > 0 ? '¥' + form.amount : '根据发票明细自动汇总' }}
</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="50" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png']" multiple
@delete="onFileDelete" />
<div class="hint-text">
可上传多份单据PDF 电子发票数电票/电子普通/专用发票走文字提取<br/>
单据图片jpg/png可不是发票样式如收据/车票/小票将自动调用 AI 识别金额与标题
</div>
</el-form-item>
<!-- 识别中提示 -->
<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>
<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-title">标题</span>
<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.itemName"
placeholder="识别标题(可编辑)"
size="small"
class="col-title"
/>
<el-input
v-model="item.reason"
placeholder="请手动填写事由"
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,.jpg,.jpeg,.png" 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="1000" />
</el-form-item>
<div class="block-title">费用信息</div>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="收款人" prop="payeeName">
<el-input v-model="form.payeeName" placeholder="收款人姓名/公司" />
<div class="hint-text">拨款申请的收款人</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="开户行" prop="bankName">
<el-input v-model="form.bankName" placeholder="XX银行XX支行" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行账号" prop="bankAccount">
<el-input v-model="form.bankAccount" placeholder="汇款账号" />
</el-form-item>
</el-col>
</el-row>
<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 />
<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 { addAppropriationReq, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } 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: 'HrmAppropriationRequest',
components: { FileUpload, UserSelect, UserMultiSelect },
data () {
return {
currentEmp: null,
submitting: false,
flowLoading: false,
flowTpl: null,
flowNodes: [],
approverMode: 'template',
availableTpls: [],
tplId: null,
assigneeUserId: null,
assigneeUserName: '',
appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
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: '',
appropriationType: '',
amount: 0,
reason: '',
accessoryApplyIds: '',
accessoryReceiptIds: '',
remark: '',
payeeName: '',
bankName: '',
bankAccount: ''
},
ccForm: {
selectedUsers: [{
userId: '1859249502579310593',
nickName: '胡雪娇',
userName: 'huangxuejiao'
}, {
userId: '1859252208375152641',
nickName: '高伟',
userName: 'gaowei'
}],
remark: ''
},
rules: {
appropriationType: [{ required: true, message: '请选择/输入拨款类型', trigger: 'change' }],
amount: []
}
}
},
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 ocrAppropriationInvoice(ossId)
if (res.code === 200 && res.data) {
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
// 发票头部信息(类型 · 销售方 · 开票日期作为标题前缀PDF 票据才有
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
if (items && items.length) {
const startIdx = this.invoiceItems.length
items.forEach((item, i) => {
// 标题 = 识别结果(图片单据直接为 AI 标题PDF 票据拼接发票头部)
const title = [prefix, item.itemName].filter(Boolean).join(' / ')
this.invoiceItems.push({
ossId: ossId,
itemName: title,
reason: '', // 事由由用户手动填写
amount: item.amount || 0,
sortNo: startIdx + i
})
})
} else if (totalAmount) {
// 没有明细时用总金额创建一条,标题取发票头部信息
this.invoiceItems.push({
ossId: ossId,
itemName: prefix || sellerName || '',
reason: '',
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(
'单据识别失败,已为你添加空白条目,可手动填写标题、事由与金额。' +
(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.amount = parseFloat(total.toFixed(2))
},
async loadTemplates () {
try {
const res = await listFlowTemplate({ pageNum: 1, pageSize: 200, bizType: 'appropriation', 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,
appropriationType: this.form.appropriationType,
amount: this.form.amount,
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,
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 || '', // 事由(手写)
amount: item.amount || 0
}))
}
if (this.approverMode === 'template') {
payload.tplId = this.tplId
}
const { data: instance } = await addAppropriationReq(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: 'appropriation',
ccUserIds,
remark: this.ccForm.remark,
fromUserId,
nodeId: 0,
readFlag: 0,
nodeName: '节点#0'
})
}
this.$message.success('提交成功')
this.$router.push('/hrm/apply')
} catch (err) {
console.log(err)
} 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-title { flex: 1; }
.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-title { flex: 1; }
.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>