Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
2025-12-31 11:42:09 +08:00
11 changed files with 179 additions and 99 deletions

View File

@@ -29,4 +29,12 @@ public class HrmFlowInstanceBo extends BaseEntity {
private Long startUserId;
private String remark;
private String startTime;
private String endTime;
private String hours;
private String bizTitle;
}

View File

@@ -57,6 +57,7 @@
"jsbarcode": "^3.12.1",
"jsencrypt": "3.0.0-rc.1",
"jspdf": "^2.5.2",
"pdfjs-dist": "^3.6.172",
"konva": "^10.0.2",
"mqtt": "^5.13.3",
"nprogress": "0.2.0",

View File

@@ -54,7 +54,7 @@ export function transferFlowTask(taskId, data) {
return request({
url: `/hrm/flow/task/${taskId}/transfer`,
method: 'post',
data
params: data
})
}

View File

@@ -131,7 +131,7 @@
</el-select>
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input-number v-model="form.version" :min="1" :step="1" controls-position="right" style="width: 100%" />
<el-input-number v-model="form.version" :min="1" :step="1" control="false" style="width: 100%" />
<div class="hint-text">同一业务可存在多版本建议先从 v1 开始</div>
</el-form-item>
<el-form-item label="启用" prop="enabled">

View File

@@ -72,7 +72,7 @@
>
<el-table-column label="申请类型" min-width="100">
<template slot-scope="scope">
<el-tag :type="getTypeTagType(scope.row.procDefKey)">{{ getTypeText(scope.row.procDefKey) }}</el-tag>
<el-tag :type="getTypeTagType(scope.row.bizType)">{{ getTypeText(scope.row.bizType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="申请人" min-width="120">
@@ -96,7 +96,7 @@
</el-table-column>
<el-table-column label="状态" prop="status" min-width="110">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.procStatus)">{{ statusText(scope.row.procStatus) }}</el-tag>
<el-tag :type="statusType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" prop="createTime" min-width="160">
@@ -202,12 +202,12 @@ export default {
return '-'
},
statusText(status) {
const map = { pending: '审批中', draft: '草稿', approved: '已通过', rejected: '已驳回', finished: '已完成' }
const map = { running: '审批中', draft: '草稿', approved: '已通过', rejected: '已驳回', finished: '已完成' }
return map[status] || status || '-'
},
statusType(status) {
if (!status) return 'info'
const map = { pending: 'warning', draft: 'info', approved: 'success', rejected: 'danger', finished: 'success' }
const map = { running: 'warning', draft: 'info', approved: 'success', rejected: 'danger', finished: 'success' }
return map[status] || 'info'
},
formatDate(val) {
@@ -244,11 +244,11 @@ export default {
}
},
goDetail(row) {
if (!row || !row.businessKey) {
if (!row || !row.bizId || !row.bizType) {
this.$message.warning('缺少businessKey无法打开详情')
return
}
const [type, bizId] = row.businessKey.split(':')
const { bizId, bizType: type } = row
if (!bizId) {
this.$message.warning('businessKey格式不正确无法解析业务ID')
return

View File

@@ -173,8 +173,8 @@
<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 :key="`line-${n.nodeId || idx}`" class="line"></div>
<div :key="`node-${n.nodeId || idx}`" class="flow-step">
<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>
@@ -223,7 +223,7 @@ export default {
flowLoading: false,
flowTpl: null,
flowNodes: [],
approverMode: 'template',
approverMode: 'manual',
availableTpls: [],
tplId: null,
assigneeUserId: null,

View File

@@ -139,8 +139,8 @@
<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 :key="`line-${n.nodeId || idx}`" class="line"></div>
<div :key="`node-${n.nodeId || idx}`" class="flow-step">
<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>
@@ -190,7 +190,7 @@ export default {
flowLoading: false,
flowTpl: null,
flowNodes: [],
approverMode: 'template',
approverMode: 'manual',
availableTpls: [],
tplId: null,
assigneeUserId: null,

View File

@@ -169,8 +169,8 @@
<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 :key="`line-${n.nodeId || idx}`" class="line"></div>
<div :key="`node-${n.nodeId || idx}`" class="flow-step">
<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>
@@ -221,7 +221,7 @@ export default {
flowLoading: false,
flowTpl: null,
flowNodes: [],
approverMode: 'template',
approverMode: 'manual',
availableTpls: [],
tplId: null,
assigneeUserId: null,

View File

@@ -29,19 +29,12 @@
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="出差类型" prop="travelType">
<el-select
v-model="form.travelType"
filterable
allow-create
default-first-option
clearable
placeholder="选择或输入(如:客户拜访/项目支持/培训学习)"
style="width: 100%"
>
<el-select v-model="form.travelType" filterable allow-create default-first-option clearable
placeholder="选择或输入(如:客户拜访/项目支持/培训学习)" style="width: 100%">
<el-option v-for="t in travelTypeOptions" :key="t" :label="t" :value="t" />
</el-select>
<div class="hint-text">优先选择若公司类型未配置可直接输入</div>
</el-form-item>
</el-form-item>
</el-col>
</el-row>
@@ -49,12 +42,14 @@
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" placeholder="请选择开始时间" style="width: 100%" :picker-options="pickerOptions" />
<el-date-picker v-model="form.startTime" type="datetime" placeholder="请选择开始时间" style="width: 100%"
:picker-options="pickerOptions" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" placeholder="请选择结束时间" style="width: 100%" :picker-options="pickerOptions" />
<el-date-picker v-model="form.endTime" type="datetime" placeholder="请选择结束时间" style="width: 100%"
:picker-options="pickerOptions" />
</el-form-item>
</el-col>
</el-row>
@@ -66,17 +61,13 @@
<div class="block-title">出差说明</div>
<el-form-item label="事由" prop="reason">
<el-input v-model="form.reason" type="textarea" :rows="4" placeholder="请说明出差目的、任务目标、预期成果等" show-word-limit maxlength="200" />
<el-input v-model="form.reason" type="textarea" :rows="4" placeholder="请说明出差目的、任务目标、预期成果等" show-word-limit
maxlength="200" />
</el-form-item>
<el-form-item label="交通/住宿/行程附件" prop="accessoryApplyIds">
<file-upload
v-model="form.accessoryApplyIds"
:limit="8"
:file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']"
multiple
/>
<file-upload v-model="form.accessoryApplyIds" :limit="8" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
<div class="hint-text">上传机票酒店行程单等pdf/jpg/png/doc/docx便于审批与后续报销</div>
</el-form-item>
@@ -93,21 +84,10 @@
<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 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>
@@ -130,21 +110,16 @@
</div>
<el-form-item label="回执附件(可选)" prop="accessoryReceiptIds">
<file-upload
v-model="form.accessoryReceiptIds"
:limit="8"
:file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']"
multiple
/>
<file-upload v-model="form.accessoryReceiptIds" :limit="8" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
<div class="hint-text">可选上传回执发票盖章回单等审核/归档使用</div>
</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="收款人姓名/公司" />
<el-form-item label="收款人" prop="payeeName">
<el-input v-model="form.payeeName" placeholder="收款人姓名/公司" />
<div class="hint-text">出差费用报销收款方</div>
</el-form-item>
</el-col>
@@ -152,7 +127,7 @@
<el-form-item label="预估费用">
<el-input-number v-model="form.estimatedCost" :min="0" :step="100" style="width: 100%" />
<div class="hint-text">预估总费用便于预算控制</div>
</el-form-item>
</el-form-item>
</el-col>
</el-row>
@@ -170,7 +145,8 @@
</el-row>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选:补充说明、特殊要求等" show-word-limit maxlength="200" />
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选:补充说明、特殊要求等" show-word-limit
maxlength="200" />
</el-form-item>
<!-- 提交流程提示真实节点配置 / 手动一次性审批预览 -->
@@ -178,7 +154,7 @@
<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-if="flowTpl">当前模板{{ flowTpl.tplName }}v{{ flowTpl.version || 1 }}</span>
<span v-else>请选择流程模板</span>
</template>
<template v-else>
@@ -188,30 +164,45 @@
<!-- 模板模式 -->
<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 :key="`line-${n.nodeId || idx}`" class="line"></div>
<div :key="`node-${n.nodeId || idx}`" class="flow-step">
<div class="dot" :class="{ success: idx === flowNodes.length - 1 }"></div>
<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>
<!-- 手动审批模式 -->
<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 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>
@@ -248,7 +239,7 @@ export default {
flowLoading: false,
flowTpl: null,
flowNodes: [],
approverMode: 'template',
approverMode: 'manual',
availableTpls: [],
tplId: null,
assigneeUserId: null,
@@ -422,12 +413,12 @@ export default {
}
try {
await addTravelReq(payload)
this.$message.success('提交成功')
this.$router.push('/hrm/requests')
this.$message.success('提交成功')
this.$router.push('/hrm/requests')
} catch (e) {
this.$message.error('提交失败,请稍后重试')
this.$message.error('提交失败,请稍后重试')
} finally {
this.submitting = false
this.submitting = false
}
})
}
@@ -440,6 +431,7 @@ export default {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.form-card {
max-width: 980px;
margin: 0 auto;
@@ -447,6 +439,7 @@ export default {
border-radius: 12px;
background: #ffffff;
}
.card-header {
display: flex;
justify-content: space-between;
@@ -454,11 +447,16 @@ export default {
font-weight: 700;
color: #2b2f36;
}
.actions {
display: flex;
gap: 8px;
}
.metal-form { padding-right: 8px; }
.metal-form {
padding-right: 8px;
}
.block-title {
margin: 20px 0 12px;
padding-left: 10px;
@@ -466,12 +464,14 @@ export default {
color: #2f3440;
border-left: 3px solid #9aa3b2;
}
.hint-text {
margin-top: 6px;
font-size: 12px;
color: #8a8f99;
line-height: 1.4;
}
.form-summary {
display: flex;
justify-content: space-between;
@@ -483,20 +483,57 @@ export default {
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; }
.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;
}
.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; }
.approve-panel {
margin-top: 12px;
}
.approve-row {
display: flex;
align-items: center;
gap: 12px;
}
.approve-row .k {
font-size: 14px;
color: #606266;
}
.flow-preview {
margin-top: 20px;
padding: 12px;
@@ -504,8 +541,18 @@ export default {
border-radius: 10px;
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;
display: flex;
@@ -513,6 +560,7 @@ export default {
gap: 10px;
flex-wrap: wrap;
}
.flow-step {
display: flex;
align-items: center;
@@ -522,17 +570,40 @@ export default {
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; }
.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: 24px;
}
@media (max-width: 1200px) {
.summary-right { display: none; }
.summary-right {
display: none;
}
}
</style>