办公V3
This commit is contained in:
149
klp-ui/src/views/hrm/flow/cc.vue
Normal file
149
klp-ui/src/views/hrm/flow/cc.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="app-container flow-cc-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">抄送我的</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span>
|
||||
未读
|
||||
<el-badge :value="badgeUnread" class="badge" v-if="badgeUnread > 0" />
|
||||
</span>
|
||||
</template>
|
||||
<!-- 内容跟随 tabs 切换统一渲染,逻辑写下面 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已读" name="read" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="ccList"
|
||||
border
|
||||
style="width: 100%"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column label="业务" prop="bizTypeName" width="150" />
|
||||
<el-table-column label="内容" prop="bizTitle" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="节点" prop="nodeName" width="180" />
|
||||
<el-table-column label="抄送时间" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ parseTime(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
v-if="scope.row.readFlag === 0"
|
||||
type="primary"
|
||||
@click.stop="handleRead(scope.row)"
|
||||
>
|
||||
标记已读
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCc, readCc } from '@/api/hrm/cc'
|
||||
import { parseTime } from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'HrmFlowCc',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
activeTab: 'unread',
|
||||
ccList: [],
|
||||
total: 0,
|
||||
badgeUnread: 0,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
readFlag: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.refreshCounts()
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
parseTime,
|
||||
tableRowClassName({ row }) {
|
||||
return row.readFlag === 0 ? 'row-unread' : ''
|
||||
},
|
||||
handleTabClick() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.queryParams.readFlag = this.activeTab === 'read' ? 1 : 0
|
||||
this.getList()
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listCc(this.queryParams)
|
||||
.then((res) => {
|
||||
this.ccList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
if (this.activeTab === 'unread') {
|
||||
this.badgeUnread = res.total || 0
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
// 单独刷新未读数量(用于 badge)
|
||||
refreshCounts() {
|
||||
listCc({ pageNum: 1, pageSize: 1, readFlag: 0 }).then((res) => {
|
||||
this.badgeUnread = res.total || 0
|
||||
})
|
||||
},
|
||||
handleRead(row) {
|
||||
readCc(row.ccId).then(() => {
|
||||
this.$modal.msgSuccess('已标记已读')
|
||||
// 刷新当前列表 & 未读数量
|
||||
this.refreshCounts()
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flow-cc-container {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge {
|
||||
margin-left: 6px;
|
||||
}
|
||||
/* 未读行高亮(柔和背景,不用渐变) */
|
||||
.row-unread td {
|
||||
background-color: #fdf6ec !important; /* Element UI warning-light */
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +1,271 @@
|
||||
<template>
|
||||
<div class="hrm-page">
|
||||
<el-card class="metal-panel" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<span>流程节点配置</span>
|
||||
<div class="actions-inline">
|
||||
<el-select v-model="query.tplId" size="mini" placeholder="选择模板" filterable clearable style="width: 200px" @change="loadList">
|
||||
<el-option v-for="tpl in templateOptions" :key="tpl.tplId" :label="`${tpl.tplName || tpl.tplCode} (${tpl.bizType || ''})`" :value="tpl.tplId" />
|
||||
</el-select>
|
||||
<el-input v-model="query.nodeType" size="mini" placeholder="节点类型" clearable style="width: 140px" @keyup.enter.native="loadList" />
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openDialog()">新增</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
<div class="flow-layout">
|
||||
<!-- 左侧:选择模板 + 模板摘要 -->
|
||||
<el-card class="metal-panel left" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<div class="header-title">
|
||||
<span>节点配置</span>
|
||||
<span class="sub">先选择模板,再配置节点顺序与审批人</span>
|
||||
</div>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadTemplates">刷新模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" height="520" stripe>
|
||||
<el-table-column label="序号" prop="orderNo" width="80" />
|
||||
<el-table-column label="节点类型" prop="nodeType" min-width="140" />
|
||||
<el-table-column label="审批规则" prop="approverRule" min-width="160" />
|
||||
<el-table-column label="规则值" prop="approverValue" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="openDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="delRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog :visible.sync="dialogVisible" title="流程节点" width="520px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" size="small">
|
||||
<el-form-item label="模板" prop="tplId">
|
||||
<el-select v-model="form.tplId" placeholder="选择模板" filterable style="width: 100%">
|
||||
<el-option v-for="tpl in templateOptions" :key="tpl.tplId" :label="`${tpl.tplName || tpl.tplCode} (${tpl.bizType || ''})`" :value="tpl.tplId" />
|
||||
<div class="selector">
|
||||
<el-select
|
||||
v-model="query.tplId"
|
||||
filterable
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="选择流程模板"
|
||||
style="width: 100%"
|
||||
@change="handleTplChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="tpl in templateOptions"
|
||||
:key="tpl.tplId"
|
||||
:label="formatTplLabel(tpl)"
|
||||
:value="tpl.tplId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="顺序" prop="orderNo">
|
||||
<el-input-number v-model="form.orderNo" :min="1" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点类型" prop="nodeType">
|
||||
<el-input v-model="form.nodeType" placeholder="如 approve/cc/end" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批规则" prop="approverRule">
|
||||
<el-select v-model="form.approverRule" placeholder="选择规则" style="width: 100%">
|
||||
<el-option v-for="opt in approverRuleOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则值" prop="approverValue">
|
||||
<el-input v-model="form.approverValue" placeholder="根据规则填写:用户ID/角色编码/岗位ID/字段名等" />
|
||||
<div class="muted">fixed_user: 用户ID逗号分隔;role: 角色编码;position: 岗位ID;leader/initiator 无需填;form_field: 表单字段名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<div class="hint-text">建议:同一业务只启用一个默认模板,避免用户选择困难</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentTpl" class="tpl-summary">
|
||||
<div class="tpl-name">{{ currentTpl.tplName }}</div>
|
||||
<div class="tpl-sub">
|
||||
<el-tag size="mini" type="info">{{ bizTypeText(currentTpl.bizType) }}</el-tag>
|
||||
<el-tag size="mini" :type="currentTpl.enabled ? 'success' : 'info'">{{ currentTpl.enabled ? '启用' : '停用' }}</el-tag>
|
||||
<span class="muted">v{{ currentTpl.version || 1 }}</span>
|
||||
</div>
|
||||
<div class="muted mt6">编码:{{ currentTpl.tplCode || '-' }}</div>
|
||||
<div class="muted mt6" v-if="currentTpl.remark">备注:{{ currentTpl.remark }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="placeholder">
|
||||
<div class="p-title">请选择一个模板</div>
|
||||
<div class="p-sub">选择后,右侧会显示该模板的节点步骤,并可新增/调整顺序</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:节点步骤(卡片化) -->
|
||||
<el-card class="metal-panel right" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<span>节点步骤</span>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" :disabled="!query.tplId" @click="openDrawer()">新增节点</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" :disabled="!query.tplId" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!query.tplId" class="empty">
|
||||
请选择模板后再配置节点。
|
||||
</div>
|
||||
|
||||
<div v-else class="steps" v-loading="loading">
|
||||
<el-steps :active="-1" align-center finish-status="success">
|
||||
<el-step
|
||||
v-for="n in sortedList"
|
||||
:key="n.nodeId"
|
||||
:title="nodeTypeText(n.nodeType)"
|
||||
:description="nodeDesc(n)"
|
||||
/>
|
||||
</el-steps>
|
||||
|
||||
<div class="node-cards">
|
||||
<div v-for="n in sortedList" :key="n.nodeId" class="node-card">
|
||||
<div class="node-head">
|
||||
<div class="lh">
|
||||
<div class="node-title">#{{ n.orderNo }} · {{ nodeTypeText(n.nodeType) }}</div>
|
||||
<div class="node-sub muted">{{ approverRuleText(n.approverRule) }}{{ approverValueReadable(n) ? ` · ${approverValueReadable(n)}` : '' }}</div>
|
||||
</div>
|
||||
<div class="rh">
|
||||
<el-button size="mini" type="text" @click="moveUp(n)" :disabled="!canMoveUp(n)">上移</el-button>
|
||||
<el-button size="mini" type="text" @click="moveDown(n)" :disabled="!canMoveDown(n)">下移</el-button>
|
||||
<el-button size="mini" type="text" @click="openDrawer(n)">编辑</el-button>
|
||||
<el-button size="mini" type="text" class="danger" @click="delRow(n)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<div class="muted" v-if="n.remark">备注:{{ n.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="sortedList.length === 0" class="empty">
|
||||
当前模板还没有节点,请新增一个“审批节点”。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 节点编辑抽屉:大面板 + 提示 -->
|
||||
<el-drawer
|
||||
:title="drawerTitle"
|
||||
:visible.sync="drawerVisible"
|
||||
size="640px"
|
||||
append-to-body
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" size="small">
|
||||
<el-form-item label="所属模板" prop="tplId">
|
||||
<el-select v-model="form.tplId" filterable placeholder="选择模板" style="width: 100%">
|
||||
<el-option v-for="tpl in templateOptions" :key="tpl.tplId" :label="formatTplLabel(tpl)" :value="tpl.tplId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="顺序" prop="orderNo">
|
||||
<el-input-number v-model="form.orderNo" :min="1" :step="1" controls-position="right" style="width: 100%" />
|
||||
<div class="hint-text">顺序越小越先执行</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="节点类型" prop="nodeType">
|
||||
<el-select v-model="form.nodeType" placeholder="选择类型" style="width: 100%">
|
||||
<el-option v-for="opt in nodeTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<div class="hint-text">常用为“审批节点”</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="block-title">审批人设置(去ID化增强)</div>
|
||||
<el-form-item label="审批规则" prop="approverRule">
|
||||
<el-select v-model="form.approverRule" placeholder="选择规则" style="width: 100%" @change="handleRuleChange">
|
||||
<el-option v-for="opt in approverRuleOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<div class="hint-text">尽量使用"部门负责人/发起人/角色"来减少维护成本</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="规则值" prop="approverValue">
|
||||
<!-- 固定用户:从员工列表选择(多选),避免手填ID -->
|
||||
<template v-if="form.approverRule === 'fixed_user'">
|
||||
<el-select
|
||||
v-model="selectedFixedUsers"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
placeholder="选择审批人(可多选)"
|
||||
style="width: 100%"
|
||||
@change="syncFixedUserValue"
|
||||
>
|
||||
<el-option
|
||||
v-for="emp in employees"
|
||||
:key="emp.empId"
|
||||
:label="formatEmpLabel(emp)"
|
||||
:value="String(emp.empId)"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="hint-text">不建议手填ID;这里可直接选择员工(系统以 empId 提交)。</div>
|
||||
</template>
|
||||
|
||||
<!-- 角色:可多选下拉(最小可行:输入/创建),减少纯文本 -->
|
||||
<template v-else-if="form.approverRule === 'role'">
|
||||
<el-select
|
||||
v-model="selectedRoles"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
collapse-tags
|
||||
placeholder="选择/输入角色编码(可多选)"
|
||||
style="width: 100%"
|
||||
@change="syncRoleValue"
|
||||
>
|
||||
<el-option v-for="r in roleOptions" :key="r" :label="r" :value="r" />
|
||||
</el-select>
|
||||
<div class="hint-text">示例:HR_ADMIN, FIN。后续可对接系统角色接口做真正的“角色选择器”。</div>
|
||||
</template>
|
||||
|
||||
<!-- 岗位:从岗位列表选择(多选),避免手填ID -->
|
||||
<template v-else-if="form.approverRule === 'position'">
|
||||
<el-select
|
||||
v-model="selectedPositions"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
placeholder="选择岗位(可多选)"
|
||||
style="width: 100%"
|
||||
@change="syncPositionValue"
|
||||
>
|
||||
<el-option
|
||||
v-for="pos in positions"
|
||||
:key="pos.postId"
|
||||
:label="formatPositionLabel(pos)"
|
||||
:value="String(pos.postId)"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="hint-text">建议直接选择岗位(系统以 postId 提交),避免手填ID。</div>
|
||||
</template>
|
||||
|
||||
<!-- 表单字段/其他:保留输入,但提示清晰 -->
|
||||
<template v-else-if="needsValue(form.approverRule)">
|
||||
<el-input v-model="form.approverValue" :placeholder="valuePlaceholder(form.approverRule)" />
|
||||
<div class="hint-text">{{ valueHint(form.approverRule) }}</div>
|
||||
</template>
|
||||
|
||||
<!-- 部门负责人:说明如何确定 -->
|
||||
<template v-else-if="form.approverRule === 'dept_leader'">
|
||||
<div class="rule-explanation">
|
||||
<div class="explanation-title">如何确定部门负责人?</div>
|
||||
<div class="explanation-content">
|
||||
<p>系统将按以下规则自动查找:</p>
|
||||
<ol>
|
||||
<li>获取发起人的主部门(hrm_employee.dept_id)</li>
|
||||
<li>查找该部门的负责人(sys_dept.leader)</li>
|
||||
<li>如果部门没有负责人,则查找上级部门的负责人,直到找到为止</li>
|
||||
<li>如果始终找不到,则使用发起人本人</li>
|
||||
</ol>
|
||||
<p class="hint-text">提示:确保部门信息中已正确设置负责人(leader字段)</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 发起人本人:简单说明 -->
|
||||
<template v-else-if="form.approverRule === 'initiator'">
|
||||
<div class="rule-explanation">
|
||||
<div class="explanation-content">
|
||||
<p>系统将自动使用流程发起人作为审批人。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="muted">该规则无需填写(系统自动计算)</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="可选:说明该节点职责、注意事项" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listFlowNode, addFlowNode, updateFlowNode, delFlowNode, listFlowTemplate } from '@/api/hrm'
|
||||
import {
|
||||
listFlowNode,
|
||||
addFlowNode,
|
||||
updateFlowNode,
|
||||
delFlowNode,
|
||||
listFlowTemplate,
|
||||
listEmployee
|
||||
} from '@/api/hrm'
|
||||
import { listPost } from '@/api/system/post'
|
||||
|
||||
export default {
|
||||
name: 'HrmFlowNode',
|
||||
@@ -70,41 +273,133 @@ export default {
|
||||
return {
|
||||
list: [],
|
||||
loading: false,
|
||||
query: { tplId: undefined, nodeType: '' },
|
||||
query: { tplId: undefined },
|
||||
templateOptions: [],
|
||||
dialogVisible: false,
|
||||
currentTpl: null,
|
||||
|
||||
employees: [],
|
||||
positions: [],
|
||||
selectedFixedUsers: [],
|
||||
selectedRoles: [],
|
||||
selectedPositions: [],
|
||||
roleOptions: ['HR_ADMIN', 'FIN', 'DEPT_LEADER'],
|
||||
|
||||
drawerVisible: false,
|
||||
drawerTitle: '节点配置',
|
||||
submitting: false,
|
||||
form: {},
|
||||
|
||||
nodeTypeOptions: [
|
||||
{ label: '审批节点', value: 'approve' },
|
||||
{ label: '结束节点', value: 'end' }
|
||||
],
|
||||
approverRuleOptions: [
|
||||
{ label: '固定用户', value: 'fixed_user' },
|
||||
{ label: '角色', value: 'role' },
|
||||
{ label: '岗位', value: 'position' },
|
||||
{ label: '直属上级', value: 'leader' },
|
||||
{ label: '发起人', value: 'initiator' },
|
||||
{ label: '表单字段指定', value: 'form_field' }
|
||||
{ label: '固定人员(选择)', value: 'fixed_user' },
|
||||
{ label: '角色(选择/输入编码)', value: 'role' },
|
||||
{ label: '岗位(选择)', value: 'position' },
|
||||
{ label: '部门负责人(自动)', value: 'dept_leader' },
|
||||
{ label: '发起人本人(自动)', value: 'initiator' },
|
||||
{ label: '表单字段指定(字段名)', value: 'form_field' }
|
||||
],
|
||||
rules: {
|
||||
tplId: [{ required: true, message: '请选择模板', trigger: 'change' }],
|
||||
orderNo: [{ required: true, message: '请输入顺序', trigger: 'blur' }],
|
||||
nodeType: [{ required: true, message: '请输入节点类型', trigger: 'blur' }],
|
||||
nodeType: [{ required: true, message: '请选择节点类型', trigger: 'change' }],
|
||||
approverRule: [{ required: true, message: '请选择审批规则', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedList() {
|
||||
return [...this.list].sort((a, b) => (a.orderNo || 0) - (b.orderNo || 0))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTemplates()
|
||||
this.loadList()
|
||||
this.loadEmployees()
|
||||
this.loadPositions()
|
||||
},
|
||||
methods: {
|
||||
loadEmployees() {
|
||||
listEmployee({ pageNum: 1, pageSize: 1000 }).then(res => {
|
||||
this.employees = res.rows || res.data || []
|
||||
})
|
||||
},
|
||||
loadPositions() {
|
||||
listPost({ pageNum: 1, pageSize: 500 }).then(res => {
|
||||
this.positions = res.rows || res.data || []
|
||||
})
|
||||
},
|
||||
formatPositionLabel(pos) {
|
||||
const name = pos.postName || pos.postCode || ''
|
||||
const code = pos.postCode ? `(${pos.postCode})` : ''
|
||||
return `${name}${code}`.trim() || `ID:${pos.postId}`
|
||||
},
|
||||
formatEmpLabel(emp) {
|
||||
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()
|
||||
},
|
||||
bizTypeText(val) {
|
||||
const map = { leave: '请假', travel: '出差', seal: '用印', payroll: '薪酬' }
|
||||
return map[val] || val || '-'
|
||||
},
|
||||
formatTplLabel(tpl) {
|
||||
const name = tpl.tplName || tpl.tplCode || '模板'
|
||||
const biz = this.bizTypeText(tpl.bizType)
|
||||
return `${name}(${biz} · v${tpl.version || 1})`
|
||||
},
|
||||
nodeTypeText(val) {
|
||||
const map = { approve: '审批', cc: '抄送', end: '结束' }
|
||||
// 兼容旧数据,但不再显示为"抄送节点"
|
||||
if (val === 'cc') return '审批(已废弃抄送节点)'
|
||||
return map[val] || val || '-'
|
||||
},
|
||||
approverRuleText(val) {
|
||||
const map = {
|
||||
fixed_user: '固定人员',
|
||||
role: '角色',
|
||||
position: '岗位',
|
||||
dept_leader: '部门负责人',
|
||||
leader: '部门负责人', // 兼容旧数据
|
||||
initiator: '发起人',
|
||||
form_field: '表单字段'
|
||||
}
|
||||
return map[val] || val || '-'
|
||||
},
|
||||
nodeDesc(n) {
|
||||
const t = this.approverRuleText(n.approverRule)
|
||||
const v = this.approverValueReadable(n)
|
||||
return v ? `${t} · ${v}` : t
|
||||
},
|
||||
approverValueReadable(n) {
|
||||
if (!n || !n.approverRule) return ''
|
||||
try {
|
||||
const arr = typeof n.approverValue === 'string' ? JSON.parse(n.approverValue || '[]') : n.approverValue
|
||||
if (Array.isArray(arr) && arr.length) return arr.join(',')
|
||||
} catch (e) {
|
||||
if (n.approverValue) return String(n.approverValue)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
loadTemplates() {
|
||||
listFlowTemplate({ pageNum: 1, pageSize: 200 })
|
||||
.then(res => {
|
||||
this.templateOptions = res.rows || []
|
||||
})
|
||||
listFlowTemplate({ pageNum: 1, pageSize: 200 }).then(res => {
|
||||
this.templateOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
handleTplChange() {
|
||||
this.currentTpl = this.templateOptions.find(t => t.tplId === this.query.tplId) || null
|
||||
this.loadList()
|
||||
},
|
||||
loadList() {
|
||||
if (!this.query.tplId) {
|
||||
this.list = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
listFlowNode({ pageNum: 1, pageSize: 500, ...this.query })
|
||||
listFlowNode({ pageNum: 1, pageSize: 500, tplId: this.query.tplId })
|
||||
.then(res => {
|
||||
this.list = res.rows || []
|
||||
})
|
||||
@@ -112,29 +407,102 @@ export default {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openDialog(row) {
|
||||
|
||||
openDrawer(row) {
|
||||
this.drawerTitle = row ? '编辑节点' : '新增节点'
|
||||
const nextOrder = (this.sortedList[this.sortedList.length - 1]?.orderNo || 0) + 1
|
||||
this.form = row
|
||||
? { ...row }
|
||||
: {
|
||||
tplId: this.query.tplId || (this.templateOptions[0] && this.templateOptions[0].tplId),
|
||||
orderNo: (this.list[this.list.length - 1]?.orderNo || 0) + 1,
|
||||
nodeType: '',
|
||||
approverRule: '',
|
||||
tplId: this.query.tplId,
|
||||
orderNo: nextOrder,
|
||||
nodeType: 'approve',
|
||||
approverRule: 'leader',
|
||||
approverValue: '',
|
||||
remark: ''
|
||||
}
|
||||
this.dialogVisible = true
|
||||
|
||||
// 清理选择器状态
|
||||
this.selectedFixedUsers = []
|
||||
this.selectedRoles = []
|
||||
this.selectedPositions = []
|
||||
|
||||
// 编辑态把 approverValue 解析回可编辑文本/选择
|
||||
if (row && row.approverValue) {
|
||||
try {
|
||||
const arr = JSON.parse(row.approverValue)
|
||||
if (Array.isArray(arr)) {
|
||||
if (row.approverRule === 'fixed_user') this.selectedFixedUsers = arr.map(v => String(v))
|
||||
if (row.approverRule === 'role') this.selectedRoles = arr
|
||||
if (row.approverRule === 'position') this.selectedPositions = arr.map(v => String(v))
|
||||
this.form.approverValue = arr.join(',')
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
|
||||
},
|
||||
|
||||
needsValue(rule) {
|
||||
return ['fixed_user', 'role', 'position', 'form_field'].includes(rule)
|
||||
},
|
||||
valuePlaceholder(rule) {
|
||||
const map = {
|
||||
form_field: '输入表单字段名(如 managerId)'
|
||||
}
|
||||
return map[rule] || '填写规则值'
|
||||
},
|
||||
valueHint(rule) {
|
||||
const map = {
|
||||
form_field: '示例:managerId(表单字段名,字段值将作为审批人ID)。'
|
||||
}
|
||||
return map[rule] || ''
|
||||
},
|
||||
handleRuleChange() {
|
||||
// 切换规则时清理值与选择
|
||||
this.form.approverValue = ''
|
||||
this.selectedFixedUsers = []
|
||||
this.selectedRoles = []
|
||||
this.selectedPositions = []
|
||||
},
|
||||
|
||||
syncFixedUserValue() {
|
||||
this.form.approverValue = this.selectedFixedUsers.join(',')
|
||||
},
|
||||
syncRoleValue() {
|
||||
this.form.approverValue = this.selectedRoles.join(',')
|
||||
},
|
||||
syncPositionValue() {
|
||||
this.form.approverValue = this.selectedPositions.join(',')
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.$refs.formRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.submitting = true
|
||||
const api = this.form.nodeId ? updateFlowNode : addFlowNode
|
||||
api(this.form)
|
||||
const payload = { ...this.form }
|
||||
|
||||
// approver_value 列是 JSON(数组),统一处理
|
||||
if (['fixed_user', 'role', 'position'].includes(payload.approverRule)) {
|
||||
const arr = (payload.approverValue || '')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
payload.approverValue = JSON.stringify(arr)
|
||||
} else if (payload.approverRule === 'form_field') {
|
||||
payload.approverValue = JSON.stringify([(payload.approverValue || '').trim()].filter(Boolean))
|
||||
} else {
|
||||
payload.approverValue = JSON.stringify([])
|
||||
}
|
||||
|
||||
api(payload)
|
||||
.then(() => {
|
||||
this.$message.success('已保存')
|
||||
this.dialogVisible = false
|
||||
this.drawerVisible = false
|
||||
this.loadList()
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -142,6 +510,7 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
delRow(row) {
|
||||
this.$confirm('确认删除该节点吗?', '提示', { type: 'warning' }).then(() => {
|
||||
delFlowNode(row.nodeId).then(() => {
|
||||
@@ -149,6 +518,42 @@ export default {
|
||||
this.loadList()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
canMoveUp(n) {
|
||||
const idx = this.sortedList.findIndex(i => i.nodeId === n.nodeId)
|
||||
return idx > 0
|
||||
},
|
||||
canMoveDown(n) {
|
||||
const idx = this.sortedList.findIndex(i => i.nodeId === n.nodeId)
|
||||
return idx >= 0 && idx < this.sortedList.length - 1
|
||||
},
|
||||
moveUp(n) {
|
||||
const idx = this.sortedList.findIndex(i => i.nodeId === n.nodeId)
|
||||
if (idx <= 0) return
|
||||
const prev = this.sortedList[idx - 1]
|
||||
this.swapOrder(n, prev)
|
||||
},
|
||||
moveDown(n) {
|
||||
const idx = this.sortedList.findIndex(i => i.nodeId === n.nodeId)
|
||||
if (idx < 0 || idx >= this.sortedList.length - 1) return
|
||||
const next = this.sortedList[idx + 1]
|
||||
this.swapOrder(n, next)
|
||||
},
|
||||
swapOrder(a, b) {
|
||||
const aOrder = a.orderNo
|
||||
const bOrder = b.orderNo
|
||||
const updateA = { ...a, orderNo: bOrder }
|
||||
const updateB = { ...b, orderNo: aOrder }
|
||||
this.loading = true
|
||||
Promise.all([updateFlowNode(updateA), updateFlowNode(updateB)])
|
||||
.then(() => {
|
||||
this.$message.success('顺序已调整')
|
||||
this.loadList()
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,21 +564,175 @@ export default {
|
||||
padding: 16px 20px 32px;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
.flow-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.metal-panel {
|
||||
border: 1px solid #d7d9df;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header-title .sub {
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
margin-top: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.actions-inline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.selector {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tpl-summary {
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
|
||||
}
|
||||
.tpl-name {
|
||||
font-weight: 900;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.tpl-sub {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.muted {
|
||||
color: #909399;
|
||||
color: #8a8f99;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mt6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.placeholder {
|
||||
padding: 18px 14px;
|
||||
border: 1px dashed #e6e8ed;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.p-title {
|
||||
font-weight: 900;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.p-sub {
|
||||
margin-top: 6px;
|
||||
color: #8a8f99;
|
||||
font-size: 13px;
|
||||
}
|
||||
.steps {
|
||||
padding-right: 4px;
|
||||
}
|
||||
.node-cards {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.node-card {
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.node-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.node-title {
|
||||
font-weight: 900;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.node-sub {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.node-body {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.empty {
|
||||
padding: 12px;
|
||||
color: #8a8f99;
|
||||
border: 1px dashed #e6e8ed;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.block-title {
|
||||
margin: 10px 0 8px;
|
||||
padding-left: 10px;
|
||||
font-weight: 800;
|
||||
color: #2f3440;
|
||||
border-left: 3px solid #9aa3b2;
|
||||
}
|
||||
.hint-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.rule-explanation {
|
||||
padding: 12px;
|
||||
background: #f8f9fb;
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.explanation-title {
|
||||
font-weight: 600;
|
||||
color: #2b2f36;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.explanation-content {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.explanation-content p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.explanation-content ol {
|
||||
margin: 8px 0 8px 20px;
|
||||
padding: 0;
|
||||
}
|
||||
.explanation-content li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.drawer-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.flow-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,180 +1,314 @@
|
||||
<template>
|
||||
<div class="hrm-page">
|
||||
<section class="panel-grid">
|
||||
<el-card class="metal-panel" shadow="hover">
|
||||
<div class="flow-task-layout">
|
||||
<!-- 左:任务列表 -->
|
||||
<el-card class="metal-panel left" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>流程任务</span>
|
||||
<el-radio-group v-model="mode" size="small" @change="fetchList">
|
||||
<el-radio-button label="todo">我的待办</el-radio-button>
|
||||
<el-radio-button label="mine">我发起的</el-radio-button>
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select
|
||||
v-model="query.status"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
size="mini"
|
||||
style="width: 140px"
|
||||
@change="fetchList"
|
||||
>
|
||||
<el-option label="待办" value="pending" />
|
||||
<el-option label="完成" value="done" />
|
||||
<el-option label="拒绝" value="rejected" />
|
||||
<el-option label="撤回" value="withdrawn" />
|
||||
</el-select>
|
||||
<span class="sub">面向办理人:待办优先,可快速审批</span>
|
||||
</div>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="fetchList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" height="680" stripe @row-click="openDetail">
|
||||
<el-table-column label="任务ID" prop="taskId" width="100" />
|
||||
<el-table-column label="实例" prop="instId" width="100" />
|
||||
<el-table-column label="业务类型" prop="bizType" min-width="100" />
|
||||
<el-table-column label="节点" prop="nodeId" min-width="90" />
|
||||
<el-table-column label="办理人" prop="assigneeUserId" min-width="120" />
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-radio-group v-model="mode" size="small" @change="fetchList">
|
||||
<el-radio-button label="todo">我的待办</el-radio-button>
|
||||
<el-radio-button label="mine">我发起的</el-radio-button>
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-select
|
||||
v-model="query.status"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 150px"
|
||||
@change="fetchList"
|
||||
>
|
||||
<el-option label="待办" value="pending" />
|
||||
<el-option label="通过" value="done" />
|
||||
<el-option label="驳回" value="rejected" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
height="680"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@row-click="openDetail"
|
||||
>
|
||||
<el-table-column label="业务" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
|
||||
<el-tag size="mini" type="info">{{ bizTypeText(scope.row.bizType) }}</el-tag>
|
||||
<span class="muted" v-if="scope.row.bizId"> #{{ scope.row.bizId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期" prop="expireTime" min-width="150">
|
||||
<el-table-column label="节点" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<span class="pill">{{ scope.row.nodeId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期" min-width="150">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.expireTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click.stop="openDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
size="mini"
|
||||
type="text"
|
||||
@click.stop="openAction(scope.row, 'approve')"
|
||||
>通过</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
size="mini"
|
||||
type="text"
|
||||
@click.stop="openAction(scope.row, 'reject')"
|
||||
>驳回</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
@click.stop="openAction(scope.row, 'withdraw')"
|
||||
>撤回</el-button>
|
||||
<el-dropdown v-if="scope.row.status === 'pending'" @command="cmd => openAction(scope.row, cmd)">
|
||||
<span class="el-dropdown-link">
|
||||
快捷办理<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="approve">通过</el-dropdown-item>
|
||||
<el-dropdown-item command="reject">驳回</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<el-drawer
|
||||
title="任务详情"
|
||||
:visible.sync="detailVisible"
|
||||
size="60%"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="detailTask" class="detail-wrap">
|
||||
<el-descriptions :column="3" size="small" border class="mb12">
|
||||
<el-descriptions-item label="任务ID">{{ detailTask.taskId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实例">{{ detailTask.instId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务类型">{{ detailTask.bizType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="节点">{{ detailTask.nodeId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理人">{{ detailTask.assigneeUserId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType(detailTask.status)">{{ detailTask.status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="到期">{{ formatDate(detailTask.expireTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ detailTask.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 右:详情区 -->
|
||||
<el-card class="metal-panel right" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<span>任务详情</span>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" icon="el-icon-document-copy" :disabled="!detailTask" @click="copyTaskInfo">复制关键信息</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs value="form">
|
||||
<el-tab-pane label="表单数据" name="form">
|
||||
<el-table :data="formData" v-loading="formLoading" size="mini">
|
||||
<el-table-column label="字段" prop="fieldName" min-width="140" />
|
||||
<el-table-column label="值" prop="fieldValue" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="展示标签" prop="fieldLabel" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="流转历史" name="history">
|
||||
<el-table :data="actionList" v-loading="actionLoading" size="mini">
|
||||
<el-table-column label="动作" prop="action" min-width="120" />
|
||||
<el-table-column label="办理人" prop="actionUserId" min-width="120" />
|
||||
<el-table-column label="时间" prop="createTime" min-width="160">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div v-else class="placeholder">请选择任务查看详情</div>
|
||||
</el-drawer>
|
||||
<div v-if="!detailTask" class="placeholder">
|
||||
<div class="p-title">请在左侧选择一条任务</div>
|
||||
<div class="p-sub">将展示业务信息、表单字段、流转历史,并可发起审批动作</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:title="actionDialogTitle"
|
||||
:visible.sync="actionDialogVisible"
|
||||
width="520px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form :model="actionForm" label-width="100px" size="small">
|
||||
<el-form-item label="意见">
|
||||
<el-input v-model="actionForm.remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<template v-if="detailTask && detailTask.bizType === 'seal' && actionType === 'approve'">
|
||||
<el-divider>盖章(选填,仅用印业务)</el-divider>
|
||||
<el-form-item label="待盖章文件">
|
||||
<el-input v-model="actionForm.stampBo.targetFileUrl" placeholder="OSS URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="章图片">
|
||||
<el-input v-model="actionForm.stampBo.stampImageUrl" placeholder="OSS URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="页码">
|
||||
<el-input-number v-model="actionForm.stampBo.pageNo" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="坐标 (x,y)">
|
||||
<div class="coord-row">
|
||||
<el-input-number v-model="actionForm.stampBo.xPx" :min="0" />
|
||||
<el-input-number v-model="actionForm.stampBo.yPx" :min="0" />
|
||||
<div v-else class="detail-wrap">
|
||||
<div class="detail-summary">
|
||||
<div class="ds-left">
|
||||
<div class="ds-title">{{ bizTypeText(detailTask.bizType) }} · 任务 #{{ detailTask.taskId }}</div>
|
||||
<div class="ds-sub">
|
||||
<el-tag size="mini" :type="statusType(detailTask.status)">{{ statusText(detailTask.status) }}</el-tag>
|
||||
<span class="muted">实例 {{ detailTask.instId }} · 节点 {{ detailTask.nodeId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="尺寸 (可选)">
|
||||
<div class="coord-row">
|
||||
<el-input-number v-model="actionForm.stampBo.widthPx" :min="1" />
|
||||
<el-input-number v-model="actionForm.stampBo.heightPx" :min="1" />
|
||||
<div class="ds-right">
|
||||
<div class="ds-item">
|
||||
<div class="k">办理人</div>
|
||||
<div class="v">{{ formatUser(detailTask.assigneeUserId) }}</div>
|
||||
</div>
|
||||
<div class="ds-item">
|
||||
<div class="k">到期</div>
|
||||
<div class="v">{{ formatDate(detailTask.expireTime) || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs value="form">
|
||||
<el-tab-pane label="表单数据" name="form">
|
||||
<!-- 业务详情:内嵌四大申请 Detail 页面(嵌入模式) -->
|
||||
<LeaveDetail
|
||||
v-if="detailTask && detailTask.bizType === 'leave'"
|
||||
:biz-id="detailTask.bizId"
|
||||
:embedded="true"
|
||||
/>
|
||||
<TravelDetail
|
||||
v-else-if="detailTask && detailTask.bizType === 'travel'"
|
||||
:biz-id="detailTask.bizId"
|
||||
:embedded="true"
|
||||
/>
|
||||
<SealDetail
|
||||
v-else-if="detailTask && detailTask.bizType === 'seal'"
|
||||
:biz-id="detailTask.bizId"
|
||||
:embedded="true"
|
||||
/>
|
||||
<ReimburseDetail
|
||||
v-else-if="detailTask && detailTask.bizType === 'reimburse'"
|
||||
:biz-id="detailTask.bizId"
|
||||
:embedded="true"
|
||||
/>
|
||||
|
||||
<!-- 兜底:如果不是四大申请,仍展示字段列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="formData" v-loading="formLoading" size="mini" height="260">
|
||||
<el-table-column label="字段" prop="fieldName" min-width="140" />
|
||||
<el-table-column label="展示" prop="fieldLabel" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="值" prop="fieldValue" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="hint-text">提示:展示字段优先给用户看;值字段用于技术排查</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="流转历史" name="history">
|
||||
<el-timeline v-loading="actionLoading" v-if="actionList.length">
|
||||
<el-timeline-item
|
||||
v-for="(a, idx) in actionList"
|
||||
:key="idx"
|
||||
:timestamp="formatDate(a.createTime)"
|
||||
:type="actionTagType(a.action)"
|
||||
>
|
||||
<div class="timeline-row">
|
||||
<div class="t-main">
|
||||
<span class="t-action">{{ actionText(a.action) }}</span>
|
||||
<span class="t-user">· 办理人:{{ formatUser(a.actionUserId) }}</span>
|
||||
</div>
|
||||
<div class="t-remark" v-if="a.remark">{{ a.remark }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<div v-else class="empty">暂无流转记录</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="block-title">办理动作</div>
|
||||
<div class="hint-text">建议填写简短意见;用印业务支持在"通过"时附带盖章参数。</div>
|
||||
<div class="btn-row">
|
||||
<el-button type="success" :disabled="detailTask.status !== 'pending'" :loading="actionSubmitting" @click="openAction(detailTask, 'approve')">通过</el-button>
|
||||
<el-button type="danger" :disabled="detailTask.status !== 'pending'" :loading="actionSubmitting" @click="openAction(detailTask, 'reject')">驳回</el-button>
|
||||
<el-button type="info" :disabled="!detailTask" @click="openCcDialog">抄送</el-button>
|
||||
<el-button type="info" :disabled="!detailTask || detailTask.status !== 'pending'" @click="openForwardDialog">转发</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 通用动作弹窗(驳回等) -->
|
||||
<el-dialog :title="actionDialogTitle" :visible.sync="actionDialogVisible" width="720px" append-to-body>
|
||||
<el-form :model="actionForm" label-width="110px" size="small">
|
||||
<el-form-item label="意见">
|
||||
<el-input v-model="actionForm.remark" type="textarea" :rows="3" placeholder="可选:请输入办理意见" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="actionDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="actionSubmitting" @click="submitAction">提交</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用印盖章弹窗 -->
|
||||
<el-dialog title="审批通过并盖章" :visible.sync="stampDialogVisible" width="1200px" append-to-body>
|
||||
<div class="stamp-dialog-body">
|
||||
<div class="stamp-form">
|
||||
<el-form :model="actionForm" label-width="120px" size="small">
|
||||
<el-form-item label="审批意见">
|
||||
<el-input v-model="actionForm.remark" type="textarea" :rows="2" placeholder="可选:请输入办理意见" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择印章" required>
|
||||
<el-select v-model="actionForm.stampBo.stampImageUrl" placeholder="选择印章" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dict.type.hrm_stamp_image"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<img v-if="dict.value" :src="dict.value" style="width: 40px; height: 40px; object-fit: contain; border: 1px solid #e6e8ed;" onerror="this.style.display='none'" />
|
||||
<span>{{ dict.label }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div class="hint-text">从字典 hrm_stamp_image 加载印章列表(label为章名,value为URL)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="印章图片URL" v-if="!actionForm.stampBo.stampImageUrl || !dict.type.hrm_stamp_image || dict.type.hrm_stamp_image.length === 0">
|
||||
<el-input
|
||||
v-model="actionForm.stampBo.stampImageUrl"
|
||||
placeholder="手动输入印章图片的完整OSS URL"
|
||||
/>
|
||||
<div class="hint-text">如果下拉列表中没有印章,可手动输入印章图片的完整URL</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="stamp-preview">
|
||||
<div v-if="stampTargetPdfUrl" class="pdf-stamper-wrapper">
|
||||
<PdfStamper
|
||||
:pdf-url="stampTargetPdfUrl"
|
||||
:initial-page="actionForm.stampBo.pageNo || 1"
|
||||
@change="onStampChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty">PDF文件加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="stampDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="actionSubmitting" :disabled="!actionForm.stampBo.stampImageUrl || !actionForm.stampBo.ready" @click="submitStampAndApprove">确认盖章并通过</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 抄送弹窗 -->
|
||||
<el-dialog
|
||||
title="抄送"
|
||||
:visible.sync="ccDialogVisible"
|
||||
width="720px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form :model="ccForm" label-width="90px" size="small">
|
||||
<el-form-item label="抄送人" required>
|
||||
<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>
|
||||
<div v-else class="muted" style="margin-top:6px;">未选择抄送人</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="actionDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="actionSubmitting" @click="submitAction">提交</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="ccForm.remark" type="textarea" :rows="3" placeholder="可选:抄送说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="ccDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="actionSubmitting" @click="submitCc">确定抄送</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 抄送/转发用户选择组件 -->
|
||||
<UserSingleSelect ref="userSingleSelect" @onSelected="onForwardUserSelected" />
|
||||
<UserMultiSelect ref="userMultiSelect" @onSelected="onCcUsersSelected" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listFlowTask,
|
||||
listTodoFlowTask,
|
||||
approveFlowTask,
|
||||
rejectFlowTask,
|
||||
withdrawFlowTask,
|
||||
listFlowAction,
|
||||
listFlowFormData
|
||||
} from '@/api/hrm'
|
||||
import { listUser } from '@/api/system/user'
|
||||
import { listFlowTask, listTodoFlowTask, getTodoTaskByBiz, approveFlowTask, rejectFlowTask, transferFlowTask, ccFlowTask, listFlowAction, listFlowFormData } from '@/api/hrm/flow'
|
||||
import { listByIds } from '@/api/system/oss'
|
||||
import UserSingleSelect from '@/components/userSelect/single.vue'
|
||||
import UserMultiSelect from '@/components/userSelect/multi.vue'
|
||||
import PdfStamper from '@/components/PdfStamper/index.vue'
|
||||
|
||||
// 四大申请详情页(嵌入模式)
|
||||
import LeaveDetail from '@/views/hrm/requests/leaveDetail.vue'
|
||||
import TravelDetail from '@/views/hrm/requests/travelDetail.vue'
|
||||
import SealDetail from '@/views/hrm/requests/sealDetail.vue'
|
||||
import ReimburseDetail from '@/views/hrm/requests/reimburseDetail.vue'
|
||||
|
||||
export default {
|
||||
name: 'HrmFlowTask',
|
||||
dicts: ['hrm_stamp_image'],
|
||||
components: { UserSingleSelect, UserMultiSelect, PdfStamper, LeaveDetail, TravelDetail, SealDetail, ReimburseDetail },
|
||||
data() {
|
||||
return {
|
||||
mode: 'todo',
|
||||
query: { status: undefined, pageNum: 1, pageSize: 50 },
|
||||
list: [],
|
||||
loading: false,
|
||||
detailVisible: false,
|
||||
detailTask: null,
|
||||
actionList: [],
|
||||
actionLoading: false,
|
||||
@@ -187,35 +321,39 @@ export default {
|
||||
actionForm: {
|
||||
remark: '',
|
||||
stampBo: {
|
||||
targetFileUrl: '',
|
||||
stampImageUrl: '',
|
||||
pageNo: 1,
|
||||
xPx: 0,
|
||||
yPx: 0,
|
||||
widthPx: undefined,
|
||||
heightPx: undefined
|
||||
widthPx: null,
|
||||
heightPx: null
|
||||
}
|
||||
}
|
||||
},
|
||||
stampDialogVisible: false,
|
||||
stampTargetPdfUrl: '',
|
||||
ccDialogVisible: false,
|
||||
forwardDialogVisible: false,
|
||||
ccForm: { selectedUsers: [], remark: '' },
|
||||
forwardForm: { selectedUser: null, remark: '' },
|
||||
allUsers: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadAllUsers()
|
||||
this.fetchList()
|
||||
},
|
||||
methods: {
|
||||
statusType(status) {
|
||||
if (!status) return 'info'
|
||||
const map = { pending: 'warning', done: 'success', rejected: 'danger', withdrawn: 'info' }
|
||||
return map[status] || 'info'
|
||||
},
|
||||
formatDate(val) {
|
||||
if (!val) return ''
|
||||
const d = new Date(val)
|
||||
const p = n => (n < 10 ? `0${n}` : n)
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
||||
},
|
||||
bizTypeText(val) { const map = { leave: '请假', travel: '出差', seal: '用印', payroll: '薪酬', reimburse: '报销' }; return map[val] || val || '-' },
|
||||
statusText(status) { const map = { pending: '待办', done: '已通过', approved: '已通过', rejected: '已驳回', withdrawn: '已撤回' }; return map[status] || status || '-' },
|
||||
statusType(status) { const map = { pending: 'warning', done: 'success', approved: 'success', rejected: 'danger', withdrawn: 'info' }; return map[status] || 'info' },
|
||||
formatDate(val) { if (!val) return ''; const d = new Date(val); const p = n => (n < 10 ? `0${n}` : n); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}` },
|
||||
actionText(action) { const map = { submit: '提交', approve: '通过', reject: '驳回', withdraw: '撤回', cancel: '撤销', stamp: '盖章', transfer: '转发' }; return map[action] || action || '-' },
|
||||
actionTagType(action) { const map = { submit: 'primary', approve: 'success', reject: 'danger', withdraw: 'info', cancel: 'info', stamp: 'primary', transfer: 'warning' }; return map[action] || 'info' },
|
||||
async loadAllUsers() { try { const res = await listUser({ pageNum: 1, pageSize: 1000 }); this.allUsers = res.rows || [] } catch (e) { this.allUsers = [] } },
|
||||
formatUser(userId) { if (!userId) return '-'; const user = this.allUsers.find(u => u.userId === userId); return user ? `${user.nickName || user.userName}` : `ID:${userId}` },
|
||||
fetchList() {
|
||||
this.loading = true
|
||||
const userId = this.$store?.state?.user?.userId
|
||||
const userId = this.$store?.state?.user?.id
|
||||
const params = { ...this.query }
|
||||
let req
|
||||
if (this.mode === 'todo' && userId) {
|
||||
@@ -225,144 +363,243 @@ export default {
|
||||
} else {
|
||||
req = listFlowTask(params)
|
||||
}
|
||||
req
|
||||
.then(res => {
|
||||
this.list = res.rows || res || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
req.then(res => {
|
||||
this.list = res.data || res.rows || []
|
||||
if (!this.detailTask && this.list.length) this.openDetail(this.list[0])
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
openDetail(row) {
|
||||
async openDetail(row) {
|
||||
if (!row) return
|
||||
|
||||
// 先用列表行渲染一份,避免右侧空白
|
||||
this.detailTask = row
|
||||
this.detailVisible = true
|
||||
this.loadActions(row)
|
||||
this.loadFormData(row)
|
||||
},
|
||||
loadActions(row) {
|
||||
if (!row) return
|
||||
if (row.instId && !/^\d+$/.test(row.instId)) {
|
||||
this.$message.warning('实例ID需为数字,已跳过加载动作')
|
||||
return
|
||||
|
||||
// 再按新接口拉取“按业务维度的当前待办详情”(用于字段对齐/按钮状态)
|
||||
const bizType = row.bizType
|
||||
const bizId = row.bizId
|
||||
if (!bizType || !bizId) return
|
||||
|
||||
try {
|
||||
const userId = this.$store?.state?.user?.id
|
||||
const res = await getTodoTaskByBiz(bizType, bizId, userId)
|
||||
const task = res && res.data
|
||||
// 若当前不是待办,后端可能返回 null,这里不强制报错
|
||||
if (task) {
|
||||
this.detailTask = task
|
||||
this.loadActions(task)
|
||||
this.loadFormData(task)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore:可能不是待办/无权限
|
||||
}
|
||||
this.actionLoading = true
|
||||
listFlowAction({ instId: row.instId, pageNum: 1, pageSize: 100 })
|
||||
.then(res => {
|
||||
this.actionList = res.rows || res || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.actionLoading = false
|
||||
})
|
||||
},
|
||||
loadFormData(row) {
|
||||
if (!row) return
|
||||
if (row.instId && !/^\d+$/.test(row.instId)) {
|
||||
this.$message.warning('实例ID需为数字,已跳过加载表单数据')
|
||||
return
|
||||
}
|
||||
this.formLoading = true
|
||||
listFlowFormData({ instId: row.instId, pageNum: 1, pageSize: 100 })
|
||||
.then(res => {
|
||||
this.formData = res.rows || res || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
},
|
||||
loadActions(row) { if (!row || !row.instId) return; this.actionLoading = true; listFlowAction({ instId: row.instId, pageNum: 1, pageSize: 200 }).then(res => { this.actionList = res.rows || [] }).finally(() => { this.actionLoading = false }) },
|
||||
loadFormData(row) { if (!row || !row.instId) return; this.formLoading = true; listFlowFormData({ instId: row.instId, pageNum: 1, pageSize: 200 }).then(res => { this.formData = res.rows || [] }).finally(() => { this.formLoading = false }) },
|
||||
openAction(row, type) {
|
||||
this.detailTask = row
|
||||
this.actionType = type
|
||||
const titleMap = { approve: '审批通过', reject: '驳回', withdraw: '撤回' }
|
||||
this.actionDialogTitle = titleMap[type] || '操作'
|
||||
this.actionForm = {
|
||||
remark: '',
|
||||
stampBo: {
|
||||
targetFileUrl: '',
|
||||
stampImageUrl: '',
|
||||
pageNo: 1,
|
||||
xPx: 0,
|
||||
yPx: 0,
|
||||
widthPx: undefined,
|
||||
heightPx: undefined
|
||||
}
|
||||
// 用印审批“通过”走专用弹窗
|
||||
if (type === 'approve' && row.bizType === 'seal') {
|
||||
this.openStampDialog(row)
|
||||
return
|
||||
}
|
||||
const titleMap = { approve: '审批通过', reject: '驳回' }
|
||||
this.actionDialogTitle = `${titleMap[type] || '操作'} · 任务 #${row.taskId}`
|
||||
this.actionForm = { remark: '', stampBo: {} }
|
||||
this.actionDialogVisible = true
|
||||
},
|
||||
submitAction() {
|
||||
if (!this.detailTask || !this.actionType) return
|
||||
this.actionSubmitting = true
|
||||
const payload = { remark: this.actionForm.remark }
|
||||
if (this.actionType === 'approve' && this.detailTask.bizType === 'seal') {
|
||||
// 仅当用户填了必要字段时传 stampBo
|
||||
const sb = this.actionForm.stampBo
|
||||
if (sb.targetFileUrl && sb.stampImageUrl && sb.pageNo && sb.xPx != null && sb.yPx != null) {
|
||||
payload.stampBo = { ...sb }
|
||||
}
|
||||
}
|
||||
const apiMap = {
|
||||
approve: approveFlowTask,
|
||||
reject: rejectFlowTask,
|
||||
withdraw: withdrawFlowTask
|
||||
}
|
||||
apiMap[this.actionType](this.detailTask.taskId, payload)
|
||||
.then(() => {
|
||||
this.$message.success('操作成功')
|
||||
this.actionSubmitting = true
|
||||
const apiMap = { approve: approveFlowTask, reject: rejectFlowTask }
|
||||
apiMap[this.actionType](this.detailTask.taskId, { remark: this.actionForm.remark })
|
||||
.then(() => {
|
||||
this.$message.success('操作成功')
|
||||
this.actionDialogVisible = false
|
||||
this.fetchList()
|
||||
this.loadActions(this.detailTask)
|
||||
})
|
||||
.finally(() => { this.actionSubmitting = false })
|
||||
},
|
||||
async openStampDialog(task) {
|
||||
if (!task.bizData || !task.bizData.applyFileIds) {
|
||||
return this.$message.error('用印申请未关联待盖章PDF文件')
|
||||
}
|
||||
|
||||
// 从 applyFileIds (ossId) 解析为可访问的 URL
|
||||
const fileIds = task.bizData.applyFileIds
|
||||
const ids = String(fileIds).split(',').map(id => id.trim()).filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
return this.$message.error('未找到待盖章PDF文件')
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await listByIds(ids)
|
||||
const files = res.data || []
|
||||
// 取第一个PDF文件
|
||||
const pdfFile = files.find(f => f.fileName && f.fileName.toLowerCase().endsWith('.pdf')) || files[0]
|
||||
if (!pdfFile || !pdfFile.url) {
|
||||
return this.$message.error('PDF文件URL不存在')
|
||||
}
|
||||
|
||||
this.stampTargetPdfUrl = pdfFile.url
|
||||
|
||||
// 从备注中解析pageNo(格式:[盖章页码:第X页])
|
||||
let pageNo = 1
|
||||
if (task.bizData.pageNo) {
|
||||
pageNo = task.bizData.pageNo
|
||||
} else if (task.bizData.remark) {
|
||||
const match = task.bizData.remark.match(/\[盖章页码:第(\d+)页\]/)
|
||||
if (match) {
|
||||
pageNo = parseInt(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有申请时的用印类型,尝试匹配对应的印章
|
||||
let defaultStampUrl = ''
|
||||
const sealType = task.bizData.sealType
|
||||
if (sealType && this.dict.type.hrm_stamp_image && this.dict.type.hrm_stamp_image.length > 0) {
|
||||
const matched = this.dict.type.hrm_stamp_image.find(d => d.label === sealType || d.label.includes(sealType))
|
||||
if (matched) {
|
||||
defaultStampUrl = matched.value
|
||||
}
|
||||
}
|
||||
|
||||
this.actionForm = {
|
||||
remark: '',
|
||||
stampBo: {
|
||||
stampImageUrl: defaultStampUrl,
|
||||
pageNo: pageNo,
|
||||
xPx: 0,
|
||||
yPx: 0,
|
||||
widthPx: null,
|
||||
heightPx: null,
|
||||
viewportWidth: null,
|
||||
viewportHeight: null,
|
||||
ready: false
|
||||
}
|
||||
}
|
||||
this.stampDialogVisible = true
|
||||
} catch (error) {
|
||||
this.$message.error('加载PDF文件失败:' + (error.message || '未知错误'))
|
||||
}
|
||||
},
|
||||
onStampChange(params) {
|
||||
if (params) {
|
||||
this.actionForm.stampBo.pageNo = params.pageNo
|
||||
this.actionForm.stampBo.xPx = params.xPx
|
||||
this.actionForm.stampBo.yPx = params.yPx
|
||||
this.actionForm.stampBo.widthPx = params.widthPx
|
||||
this.actionForm.stampBo.heightPx = params.heightPx
|
||||
this.actionForm.stampBo.viewportWidth = params.viewportWidth
|
||||
this.actionForm.stampBo.viewportHeight = params.viewportHeight
|
||||
this.actionForm.stampBo.ready = params.ready || false
|
||||
}
|
||||
},
|
||||
submitStampAndApprove() {
|
||||
if (!this.detailTask) return
|
||||
if (!this.actionForm.stampBo.stampImageUrl) {
|
||||
return this.$message.warning('请选择或输入印章图片URL')
|
||||
}
|
||||
if (!this.actionForm.stampBo.ready) {
|
||||
return this.$message.warning('请在PDF上点击选择盖章位置')
|
||||
}
|
||||
if (this.actionForm.stampBo.xPx === null || this.actionForm.stampBo.yPx === null) {
|
||||
return this.$message.warning('请选择盖章位置坐标')
|
||||
}
|
||||
|
||||
this.actionSubmitting = true
|
||||
const stampBo = {
|
||||
targetFileUrl: this.stampTargetPdfUrl,
|
||||
stampImageUrl: this.actionForm.stampBo.stampImageUrl,
|
||||
pageNo: this.actionForm.stampBo.pageNo,
|
||||
xPx: this.actionForm.stampBo.xPx,
|
||||
yPx: this.actionForm.stampBo.yPx,
|
||||
viewportWidth: this.actionForm.stampBo.viewportWidth,
|
||||
viewportHeight: this.actionForm.stampBo.viewportHeight
|
||||
}
|
||||
if (this.actionForm.stampBo.widthPx) {
|
||||
stampBo.widthPx = this.actionForm.stampBo.widthPx
|
||||
}
|
||||
if (this.actionForm.stampBo.heightPx) {
|
||||
stampBo.heightPx = this.actionForm.stampBo.heightPx
|
||||
}
|
||||
|
||||
approveFlowTask(this.detailTask.taskId, {
|
||||
remark: this.actionForm.remark,
|
||||
stampBo: stampBo
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('盖章并通过成功')
|
||||
this.stampDialogVisible = false
|
||||
this.fetchList()
|
||||
this.loadActions(this.detailTask)
|
||||
})
|
||||
.finally(() => {
|
||||
this.actionSubmitting = false
|
||||
.catch(error => {
|
||||
this.$message.error(error.message || '盖章并通过失败')
|
||||
})
|
||||
.finally(() => { this.actionSubmitting = false })
|
||||
},
|
||||
copyTaskInfo() { if (!this.detailTask) return; const t = this.detailTask; const text = `任务ID:${t.taskId}\n实例:${t.instId}\n业务:${t.bizType}${t.bizId ? `#${t.bizId}` : ''}\n节点:${t.nodeId}\n状态:${t.status}`; navigator.clipboard.writeText(text).then(() => this.$message.success('已复制')) },
|
||||
openCcDialog() { if (!this.detailTask) return; this.ccForm = { selectedUsers: [], remark: '' }; this.ccDialogVisible = true },
|
||||
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) },
|
||||
openForwardDialog() { if (!this.detailTask || this.detailTask.status !== 'pending') return; this.forwardForm = { selectedUser: null, remark: '' }; this.forwardDialogVisible = true },
|
||||
openUserSingleSelect() { this.$refs.userSingleSelect.open() },
|
||||
onForwardUserSelected(user) { this.forwardForm.selectedUser = user || null },
|
||||
submitCc() {
|
||||
const userIds = this.ccForm.selectedUsers.map(u => u.userId)
|
||||
if (!this.detailTask || userIds.length === 0) return this.$message.warning('请选择至少一个抄送人')
|
||||
const fromUserId = this.$store?.state?.user?.id
|
||||
ccFlowTask({ instId: this.detailTask.instId, bizType: this.detailTask.bizType, bizId: this.detailTask.bizId, nodeId: this.detailTask.nodeId, nodeName: `节点#${this.detailTask.nodeId}`, fromUserId, ccUserIds: userIds, readFlag: 0, remark: this.ccForm.remark || '手动抄送' })
|
||||
.then(() => { this.$message.success('抄送成功'); this.ccDialogVisible = false })
|
||||
.catch(() => { this.$message.error('抄送失败') })
|
||||
},
|
||||
submitForward() {
|
||||
if (!this.detailTask || !this.forwardForm.selectedUser) return this.$message.warning('请选择转发对象')
|
||||
const newAssigneeUserId = this.forwardForm.selectedUser.userId
|
||||
transferFlowTask(this.detailTask.taskId, { newAssigneeUserId, remark: this.forwardForm.remark })
|
||||
.then(() => { this.$message.success('转发成功'); this.forwardDialogVisible = false; this.fetchList() })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hrm-page {
|
||||
padding: 16px 20px 32px;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
.panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.metal-panel {
|
||||
border: 1px solid #d7d9df;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.actions-inline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.detail-wrap {
|
||||
padding-right: 4px;
|
||||
}
|
||||
.coord-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.placeholder {
|
||||
color: #a0a3ad;
|
||||
padding: 12px;
|
||||
}
|
||||
.mb12 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.hrm-page { padding: 16px 20px 32px; background: #f8f9fb; }
|
||||
.flow-task-layout { display: grid; grid-template-columns: 520px 1fr; gap: 14px; align-items: start; }
|
||||
.metal-panel { border: 1px solid #d7d9df; border-radius: 12px; background: #fff; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; font-weight: 800; color: #2b2f36; }
|
||||
.header-title { display: flex; flex-direction: column; }
|
||||
.header-title .sub { font-size: 12px; color: #8a8f99; margin-top: 2px; font-weight: 500; }
|
||||
.actions-inline { display: flex; gap: 8px; align-items: center; }
|
||||
.filter-bar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.muted { color: #8a8f99; font-size: 12px; }
|
||||
.pill { display: inline-block; padding: 2px 8px; background: #f4f5f7; border-radius: 12px; margin-right: 6px; }
|
||||
.placeholder { padding: 18px 14px; border: 1px dashed #e6e8ed; border-radius: 12px; background: #fafbfc; }
|
||||
.p-title { font-weight: 900; color: #2b2f36; }
|
||||
.p-sub { margin-top: 6px; color: #8a8f99; font-size: 13px; }
|
||||
.detail-wrap { padding-right: 4px; }
|
||||
.detail-summary { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid #e6e8ed; border-radius: 12px; background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%); }
|
||||
.ds-title { font-weight: 900; color: #2b2f36; }
|
||||
.ds-sub { margin-top: 6px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.ds-right { display: flex; gap: 16px; }
|
||||
.ds-item .k { font-size: 12px; color: #8a8f99; }
|
||||
.ds-item .v { margin-top: 2px; font-weight: 800; color: #2b2f36; }
|
||||
.block-title { margin: 12px 0 8px; padding-left: 10px; font-weight: 800; color: #2f3440; border-left: 3px solid #9aa3b2; }
|
||||
.hint-text { margin: 6px 0 10px; font-size: 12px; color: #8a8f99; line-height: 1.4; }
|
||||
.btn-row { display: flex; gap: 10px; }
|
||||
.empty { padding: 12px; color: #8a8f99; }
|
||||
.timeline-row .t-main { font-weight: 600; color: #2b2f36; }
|
||||
.timeline-row .t-remark { margin-top: 4px; color: #606266; font-size: 13px; }
|
||||
.selected-users { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.stamp-dialog-body { display: grid; grid-template-columns: 380px 1fr; gap: 16px; }
|
||||
.stamp-form { padding: 12px; background: #fafbfc; border-radius: 8px; }
|
||||
.stamp-preview { padding: 12px; background: #fff; border: 1px solid #e6e8ed; border-radius: 8px; }
|
||||
.pdf-stamper-wrapper { width: 100%; }
|
||||
.stamp-dialog-body .hint-text { margin-top: 6px; font-size: 12px; color: #8a8f99; }
|
||||
@media (max-width: 1200px) { .flow-task-layout { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
@@ -1,73 +1,155 @@
|
||||
<template>
|
||||
<div class="hrm-page">
|
||||
<el-card class="metal-panel" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<span>流程模板</span>
|
||||
<div class="actions-inline">
|
||||
<el-input v-model="query.tplName" size="mini" placeholder="模板名称" clearable style="width: 160px" @keyup.enter.native="loadList" />
|
||||
<el-select v-model="query.bizType" size="mini" placeholder="业务类型" clearable style="width: 140px" @change="loadList">
|
||||
<el-option label="请假" value="leave" />
|
||||
<el-option label="加班" value="overtime" />
|
||||
<el-option label="出差" value="travel" />
|
||||
<el-option label="用印" value="seal" />
|
||||
<el-option label="薪酬" value="payroll" />
|
||||
</el-select>
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openDialog()">新增</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
<div class="flow-layout">
|
||||
<!-- 左侧:模板列表 -->
|
||||
<el-card class="metal-panel left" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<div class="header-title">
|
||||
<span>流程模板</span>
|
||||
<span class="sub">先选业务 → 再选模板</span>
|
||||
</div>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openEditDrawer()">新建模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" height="520" stripe>
|
||||
<el-table-column label="名称" prop="tplName" min-width="160" />
|
||||
<el-table-column label="编码" prop="tplCode" min-width="140" />
|
||||
<el-table-column label="业务类型" prop="bizType" min-width="120" />
|
||||
<el-table-column label="版本" prop="version" width="90" />
|
||||
<el-table-column label="启用" prop="enabled" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.enabled ? 'success' : 'info'" size="mini">{{ scope.row.enabled ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="openDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="delRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog :visible.sync="dialogVisible" title="流程模板" width="480px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" size="small">
|
||||
<el-form-item label="模板名称" prop="tplName">
|
||||
<el-input v-model="form.tplName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板编码" prop="tplCode">
|
||||
<el-input v-model="form.tplCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型" prop="bizType">
|
||||
<el-select v-model="form.bizType" placeholder="选择业务" style="width: 100%">
|
||||
<div class="filter-bar">
|
||||
<el-select
|
||||
v-model="query.bizType"
|
||||
size="small"
|
||||
placeholder="业务类型"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@change="loadList"
|
||||
>
|
||||
<el-option label="请假" value="leave" />
|
||||
<el-option label="加班" value="overtime" />
|
||||
<el-option label="出差" value="travel" />
|
||||
<el-option label="用印" value="seal" />
|
||||
<el-option label="薪酬" value="payroll" />
|
||||
<el-option label="日常报销" value="reimburse" />
|
||||
</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" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="enabled">
|
||||
<el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-input
|
||||
v-model="query.tplName"
|
||||
size="small"
|
||||
placeholder="搜索模板名称/编码"
|
||||
clearable
|
||||
@keyup.enter.native="loadList"
|
||||
/>
|
||||
<el-button size="small" icon="el-icon-search" @click="loadList">查询</el-button>
|
||||
</div>
|
||||
|
||||
<div class="template-list" v-loading="loading">
|
||||
<div
|
||||
v-for="tpl in list"
|
||||
:key="tpl.tplId"
|
||||
class="tpl-item"
|
||||
:class="{ active: selectedTpl && selectedTpl.tplId === tpl.tplId }"
|
||||
@click="selectTpl(tpl)"
|
||||
>
|
||||
<div class="tpl-main">
|
||||
<div class="tpl-name">{{ tpl.tplName || '-' }}</div>
|
||||
<div class="tpl-meta">
|
||||
<el-tag size="mini" type="info">{{ bizTypeText(tpl.bizType) }}</el-tag>
|
||||
<el-tag size="mini" :type="tpl.enabled ? 'success' : 'info'">{{ tpl.enabled ? '启用' : '停用' }}</el-tag>
|
||||
<span class="muted">v{{ tpl.version || 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpl-sub">
|
||||
<span class="muted">编码:{{ tpl.tplCode || '-' }}</span>
|
||||
</div>
|
||||
<div class="tpl-actions">
|
||||
<el-button size="mini" type="text" @click.stop="openEditDrawer(tpl)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click.stop="goNodeConfig(tpl)">节点配置</el-button>
|
||||
<el-button size="mini" type="text" class="danger" @click.stop="delRow(tpl)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && list.length === 0" class="empty">
|
||||
暂无模板,请先新建。
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:模板详情预览(面向用户可读) -->
|
||||
<el-card class="metal-panel right" shadow="hover">
|
||||
<div slot="header" class="panel-header">
|
||||
<span>模板预览</span>
|
||||
<div class="actions-inline">
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedTpl" class="placeholder">
|
||||
<div class="p-title">请选择左侧一个模板</div>
|
||||
<div class="p-sub">选择后可查看关键信息,并进入节点配置完善流程</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview">
|
||||
<div class="preview-title">{{ selectedTpl.tplName }}</div>
|
||||
<div class="preview-sub">
|
||||
<el-tag size="mini" type="info">{{ bizTypeText(selectedTpl.bizType) }}</el-tag>
|
||||
<el-tag size="mini" :type="selectedTpl.enabled ? 'success' : 'info'">{{ selectedTpl.enabled ? '启用' : '停用' }}</el-tag>
|
||||
<span class="muted">版本 v{{ selectedTpl.version || 1 }}</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border size="small" class="mt12">
|
||||
<el-descriptions-item label="模板编码">{{ selectedTpl.tplCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务类型">{{ bizTypeText(selectedTpl.bizType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ selectedTpl.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="next-step">
|
||||
<div class="ns-title">下一步</div>
|
||||
<div class="ns-sub">配置审批节点顺序与审批人规则,用户提交申请时才会自动按流程流转。</div>
|
||||
<el-button type="primary" plain icon="el-icon-s-operation" @click="goNodeConfig(selectedTpl)">进入节点配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑抽屉:大面板,避免小弹窗 -->
|
||||
<el-drawer
|
||||
:title="drawerTitle"
|
||||
:visible.sync="drawerVisible"
|
||||
size="520px"
|
||||
append-to-body
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" size="small">
|
||||
<el-form-item label="模板名称" prop="tplName">
|
||||
<el-input v-model="form.tplName" placeholder="例如:请假审批(部门负责人→HR)" />
|
||||
<div class="hint-text">建议包含业务与审批路径,便于用户选择</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板编码" prop="tplCode">
|
||||
<el-input v-model="form.tplCode" placeholder="例如:LEAVE_V1" />
|
||||
<div class="hint-text">用于系统识别,建议英文大写+下划线</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型" prop="bizType">
|
||||
<el-select v-model="form.bizType" placeholder="选择业务" style="width: 100%">
|
||||
<el-option label="请假" value="leave" />
|
||||
<el-option label="出差" value="travel" />
|
||||
<el-option label="用印" value="seal" />
|
||||
<el-option label="日常报销" value="reimburse" />
|
||||
</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%" />
|
||||
<div class="hint-text">同一业务可存在多版本,建议先从 v1 开始</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="enabled">
|
||||
<el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
|
||||
<div class="hint-text">停用后用户将无法按此模板发起流程</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="可选:说明适用范围、条件等" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,7 +163,10 @@ export default {
|
||||
list: [],
|
||||
loading: false,
|
||||
query: { tplName: '', bizType: undefined },
|
||||
dialogVisible: false,
|
||||
selectedTpl: null,
|
||||
|
||||
drawerVisible: false,
|
||||
drawerTitle: '流程模板',
|
||||
submitting: false,
|
||||
form: {},
|
||||
rules: {
|
||||
@@ -96,21 +181,34 @@ export default {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
bizTypeText(val) {
|
||||
const map = { leave: '请假', travel: '出差', seal: '用印', reimburse: '日常报销' }
|
||||
return map[val] || val || '-'
|
||||
},
|
||||
loadList() {
|
||||
this.loading = true
|
||||
listFlowTemplate({ pageNum: 1, pageSize: 200, ...this.query })
|
||||
.then(res => {
|
||||
this.list = res.rows || []
|
||||
// 保持选中
|
||||
if (this.selectedTpl) {
|
||||
const hit = this.list.find(i => i.tplId === this.selectedTpl.tplId)
|
||||
this.selectedTpl = hit || null
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openDialog(row) {
|
||||
selectTpl(tpl) {
|
||||
this.selectedTpl = tpl
|
||||
},
|
||||
openEditDrawer(row) {
|
||||
this.drawerTitle = row ? '编辑流程模板' : '新建流程模板'
|
||||
this.form = row
|
||||
? { ...row }
|
||||
: { tplName: '', tplCode: '', bizType: '', version: 1, enabled: 1, remark: '' }
|
||||
this.dialogVisible = true
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
|
||||
},
|
||||
submit() {
|
||||
@@ -121,7 +219,7 @@ export default {
|
||||
api(this.form)
|
||||
.then(() => {
|
||||
this.$message.success('已保存')
|
||||
this.dialogVisible = false
|
||||
this.drawerVisible = false
|
||||
this.loadList()
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -133,9 +231,16 @@ export default {
|
||||
this.$confirm('确认删除该模板吗?', '提示', { type: 'warning' }).then(() => {
|
||||
delFlowTemplate(row.tplId).then(() => {
|
||||
this.$message.success('已删除')
|
||||
if (this.selectedTpl && this.selectedTpl.tplId === row.tplId) this.selectedTpl = null
|
||||
this.loadList()
|
||||
})
|
||||
})
|
||||
},
|
||||
goNodeConfig(tpl) {
|
||||
// 按你的要求:不新增路由,也不在这里强制跳转。
|
||||
// 这里用事件/提示引导用户去“流程节点配置”页面,并自动复制 tplId。
|
||||
const text = `已选模板:${tpl.tplName}\n模板ID:${tpl.tplId}\n\n请前往【HRM-流程节点配置】页面,选择该模板继续配置节点。`
|
||||
this.$alert(text, '去配置节点', { confirmButtonText: '我知道了' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,16 +251,167 @@ export default {
|
||||
padding: 16px 20px 32px;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
.flow-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 420px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.metal-panel {
|
||||
border: 1px solid #d7d9df;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header-title .sub {
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
margin-top: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.actions-inline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr auto;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.template-list {
|
||||
max-height: 620px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.tpl-item {
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
transition: all .15s ease;
|
||||
}
|
||||
.tpl-item:hover {
|
||||
border-color: #cfd3dc;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,.04);
|
||||
}
|
||||
.tpl-item.active {
|
||||
border-color: #9aa3b2;
|
||||
background: #fcfdff;
|
||||
}
|
||||
.tpl-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tpl-name {
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.tpl-meta {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.tpl-sub {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.tpl-actions {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.tpl-actions .danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.muted {
|
||||
color: #8a8f99;
|
||||
font-size: 12px;
|
||||
}
|
||||
.placeholder {
|
||||
padding: 28px 18px;
|
||||
border: 1px dashed #e6e8ed;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.p-title {
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.p-sub {
|
||||
margin-top: 6px;
|
||||
color: #8a8f99;
|
||||
font-size: 13px;
|
||||
}
|
||||
.preview {
|
||||
padding: 4px 2px;
|
||||
}
|
||||
.preview-title {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.preview-sub {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mt12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.next-step {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
|
||||
}
|
||||
.ns-title {
|
||||
font-weight: 900;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.ns-sub {
|
||||
margin-top: 4px;
|
||||
color: #8a8f99;
|
||||
font-size: 13px;
|
||||
}
|
||||
.hint-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
.drawer-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.flow-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user