Files
fad_oa/ruoyi-ui/src/views/hrm/requests/reimburse.vue
2026-05-31 14:19:15 +08:00

804 lines
28 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.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>