This commit is contained in:
那些花儿
2025-07-28 10:48:38 +08:00
parent 030cf7cc4c
commit 3b51ed7a51
450 changed files with 82310 additions and 1213 deletions
@@ -0,0 +1,648 @@
<script>
module.exports = {
name: "formButton",
props: {
task_id: {
type: String,
required: true
},
// 是否显示审核意见框
show_comment: {
type: Boolean,
default: true
},
// 自定义按钮配置
custom_buttons: {
type: Array,
default: () => []
},
// 是否显示流程图
show_process_diagram: {
type: Boolean,
default: true
},
// 是否显示办理记录
show_process_records: {
type: Boolean,
default: true
},
// 办理记录展示模式:timeline, table
record_display_mode: {
type: String,
default: "timeline",
validator: (value) => {
return ["timeline", "table"].includes(value)
}
},
// 业务ID,用于获取流程记录
business_id: {
type: String,
default: ""
}
},
data() {
return {
loading: false,
comment: "", // 审核意见
nextNodeInfo: null, // 下一节点信息
currentTaskName: "部门审核", // 当前办理节点名称
availableActions: [], // 可用操作
sequenceFlows: [], // 目标节点
selectedAction: "", // 选中的操作类型
rejectTaskOptions: [], // 驳回到指定节点的选项
selectedRejectTask: "", // 选中的驳回节点
assigneeOptions: [], // 执行人选项
selectedAssignee: "", // 选中的执行人
// 控制显示
showProcessDiagram: true, // 是否显示流程图
showProcessRecords: true, // 是否显示办理记录
recordDisplayMode: 'timeline', // 办理记录展示模式:timeline, table
// 模拟数据 - 办理记录
mockProcessRecords: [
{
nodeName: "发起申请",
userName: "张三",
time: "2023-05-10 09:30:00",
action: "提交申请",
comment: "请审批",
type: "primary"
},
{
nodeName: "部门审核",
userName: "李四",
time: "2023-05-10 11:20:00",
action: "处理中",
comment: "",
type: "warning"
}
]
}
},
created() {
if (this.task_id) {
this.getNextNodeInfo()
}
// 初始化显示状态
this.showProcessDiagram = this.show_process_diagram
this.showProcessRecords = this.show_process_records
this.recordDisplayMode = this.record_display_mode
// 模拟获取流程记录
this.mockGetProcessRecords()
},
watch: {
// 监听props变化
show_process_diagram(val) {
this.showProcessDiagram = val
},
show_process_records(val) {
this.showProcessRecords = val
},
record_display_mode(val) {
this.recordDisplayMode = val
},
business_id: {
handler(val) {
if (val) {
// 清空现有记录
this.mockProcessRecords = []
// 重新获取流程记录
this.mockGetProcessRecords()
}
},
immediate: true
}
},
methods: {
// 获取下一节点信息
getNextNodeInfo() {
this.loading = true
this.$axios
.post("/easy-flowable/task/nextNodeVariables/" + this.task_id)
.then((res) => {
if (res.code === 200) {
this.nextNodeInfo = res.result
// 获取当前办理节点名称
if (res.result.task && res.result.task.name) {
this.currentTaskName = res.result.task.name
}
// 处理可用操作
if (res.result.attributes && res.result.attributes.actions) {
this.availableActions = res.result.attributes.actions
}
// 处理目标节点
if (res.result.sequenceFlow) {
this.sequenceFlows = res.result.sequenceFlow
}
} else {
this.$message.error(res.msg || "获取下一节点信息失败")
}
})
.catch((error) => {
console.error("获取下一节点信息失败", error)
this.$message.error("获取下一节点信息失败")
})
.finally(() => {
this.loading = false
})
},
// 获取可驳回的节点列表
getRejectTaskList() {
this.loading = true
this.$axios
.get("/easy-flowable/task/flowBackNodes/" + this.task_id)
.then((res) => {
if (res.code === 0 && res.result) {
this.rejectTaskOptions = res.result.map((item) => ({
label: item.nodeName,
value: item.nodeId
}))
if (this.rejectTaskOptions.length > 0) {
this.selectedRejectTask = this.rejectTaskOptions[0].value
}
} else {
this.$message.error(res.msg || "获取可驳回节点失败")
}
})
.catch((error) => {
console.error("获取可驳回节点失败", error)
this.$message.error("获取可驳回节点失败")
})
.finally(() => {
this.loading = false
})
},
// 处理按钮点击
handleAction(action) {
this.selectedAction = action
// 如果是驳回到指定节点,需要获取可驳回的节点列表
if (action === "REJECT_TO_TASK") {
this.getRejectTaskList()
return
}
// 执行操作
this.submitAction()
},
// 提交操作
submitAction() {
if (this.show_comment && !this.comment && ["AGREE", "REJECT", "REJECT_TO_TASK", "REBUT"].includes(this.selectedAction)) {
this.$message.warning("请填写审核意见")
return
}
const params = {
taskId: this.task_id,
comment: this.comment,
type: this.getActionCode(this.selectedAction)
}
// 添加目标节点变量(自动选择第一个)
if (this.sequenceFlows.length > 0 && ["AGREE"].includes(this.selectedAction)) {
params.variables = {
sequenceFlow: this.sequenceFlows[0].value
}
}
// 添加驳回节点信息
if (this.selectedAction === "REJECT_TO_TASK" && this.selectedRejectTask) {
params.targetTaskDefinitionKey = this.selectedRejectTask
}
this.loading = true
this.$axios
.post("/easy-flowable/task/complete", params)
.then((res) => {
if (res.code === 0) {
this.$message.success("操作成功")
this.$emit("success", {
action: this.selectedAction,
result: res.result
})
} else {
this.$message.error(res.msg || "操作失败")
}
})
.catch((error) => {
console.error("操作失败", error)
this.$message.error("操作失败")
})
.finally(() => {
this.loading = false
})
},
// 获取操作类型代码
getActionCode(action) {
const actionMap = {
START: "0",
AGREE: "1",
REBUT: "2",
REVOCATION: "3",
REJECT: "4",
REJECT_TO_TASK: "5",
DELEGATE: "6",
ASSIGN: "7",
STOP: "8",
BEFORE_SIGN: "9",
AFTER_SIGN: "10",
CANCELLATION: "11",
ADD_COMMENT: "12",
DEL_COMMENT: "13",
RESUBMIT: "14"
}
return actionMap[action] || "1"
},
// 获取操作按钮文本
getActionText(action) {
const actionTextMap = {
START: "启动流程",
AGREE: "同意",
REBUT: "拒绝",
REVOCATION: "撤回",
REJECT: "驳回",
REJECT_TO_TASK: "驳回到指定节点",
DELEGATE: "委派",
ASSIGN: "转办",
STOP: "终止",
BEFORE_SIGN: "前加签",
AFTER_SIGN: "后加签",
CANCELLATION: "作废",
ADD_COMMENT: "添加评论",
DEL_COMMENT: "删除评论",
RESUBMIT: "重新提交"
}
return actionTextMap[action] || action
},
// 获取操作按钮类型
getActionType(action) {
const typeMap = {
AGREE: "primary",
REBUT: "danger",
REJECT: "warning",
REJECT_TO_TASK: "warning"
}
return typeMap[action] || "default"
},
// 模拟获取流程记录
mockGetProcessRecords() {
// 这里可以根据实际需求调整模拟数据
// 实际项目中,可以通过API获取真实数据
// 例如:this.$axios.post("/platform/wf/processCommon/approvalRecord", { businessNo: this.business_id })
// 模拟异步获取数据
setTimeout(() => {
// 如果已经有模拟数据,则不再添加
if (this.mockProcessRecords.length <= 2) {
// 添加更多模拟数据
this.mockProcessRecords.push(
{
nodeName: "主管审核",
userName: "王五",
time: "2023-05-09 14:30:00",
action: "同意",
comment: "同意申请,请总经理审批",
type: "success"
},
{
nodeName: "发起申请",
userName: "张三",
time: "2023-05-09 10:15:00",
action: "提交申请",
comment: "请审批我的申请",
type: "info"
}
)
}
}, 1000)
}
}
}
</script>
<template>
<div class="form-button-container">
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 表单信息插槽 -->
<div class="form-info-container">
<slot name="form-info"></slot>
</div>
<!-- 流程图展示 -->
<div class="process-diagram-container" v-if="showProcessDiagram">
<div class="section-title">流程图</div>
<div class="process-diagram">
<!-- 模拟流程图 -->
<div class="mock-process-diagram">
<div class="mock-node start">开始</div>
<div class="mock-arrow"></div>
<div class="mock-node" :class="{ active: currentTaskName === '部门审核' }">部门审核</div>
<div class="mock-arrow"></div>
<div class="mock-node">主管审核</div>
<div class="mock-arrow"></div>
<div class="mock-node">总经理审核</div>
<div class="mock-arrow"></div>
<div class="mock-node end">结束</div>
</div>
</div>
</div>
<!-- 办理记录 -->
<div class="process-records-container" v-if="showProcessRecords">
<div class="section-title">
<span>办理记录</span>
<div class="display-mode-selector">
<el-radio-group v-model="recordDisplayMode" size="small">
<el-radio-button label="timeline">时间线</el-radio-button>
<el-radio-button label="table">表格</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="process-records">
<!-- 时间线模式 -->
<el-timeline v-if="recordDisplayMode === 'timeline'">
<el-timeline-item v-for="(record, index) in mockProcessRecords" :key="index" :timestamp="record.time" :type="record.type">
<div class="record-content">
<div class="record-title">{{ record.nodeName }} - {{ record.userName }}</div>
<div class="record-action">{{ record.action }}</div>
<div class="record-comment" v-if="record.comment">{{ record.comment }}</div>
</div>
</el-timeline-item>
</el-timeline>
<!-- 表格模式 -->
<el-table v-else-if="recordDisplayMode === 'table'" :data="mockProcessRecords" stripe style="width: 100%">
<el-table-column prop="nodeName" label="节点名称" width="120"></el-table-column>
<el-table-column prop="userName" label="操作人" width="100"></el-table-column>
<el-table-column prop="time" label="操作时间" width="160"></el-table-column>
<el-table-column prop="action" label="操作类型" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.type">{{ scope.row.action }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="comment" label="操作意见">
<template slot-scope="scope">
<span v-if="scope.row.comment">{{ scope.row.comment }}</span>
<span v-else class="no-comment"></span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 底部固定操作区域 -->
<div class="footer-actions">
<!-- 当前办理节点 -->
<div v-if="currentTaskName" class="current-task-container">
<div class="current-task-label">当前节点</div>
<div class="current-task-info">{{ currentTaskName }}</div>
</div>
<!-- 目标节点展示 -->
<div v-if="sequenceFlows.length > 0" class="flow-container">
<div class="flow-label">目标节点</div>
<div class="target-node-info">
<span v-for="item in sequenceFlows" :key="item.value">{{ item.label }}</span>
</div>
</div>
<!-- 审核意见 -->
<div v-if="show_comment" class="comment-container">
<div class="comment-label">审核意见</div>
<el-input v-model="comment" type="textarea" :rows="3" placeholder="请输入审核意见"></el-input>
</div>
<!-- 驳回到指定节点选择 -->
<div v-if="selectedAction === 'REJECT_TO_TASK' && rejectTaskOptions.length > 0" class="reject-container">
<div class="reject-label">驳回到</div>
<el-select v-model="selectedRejectTask" placeholder="请选择驳回节点">
<el-option v-for="item in rejectTaskOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-button type="primary" @click="submitAction">确认</el-button>
<el-button @click="selectedAction = ''">取消</el-button>
</div>
<!-- 操作按钮 -->
<div class="button-container">
<!-- 系统预设按钮 -->
<template v-for="action in availableActions">
<el-button
:key="action"
:type="getActionType(action)"
:loading="loading && selectedAction === action"
@click="handleAction(action)"
>
{{ getActionText(action) }}
</el-button>
</template>
<!-- 自定义按钮 -->
<template v-for="(button, index) in custom_buttons">
<el-button
:key="'custom-' + index"
:type="button.type || 'default'"
:loading="loading && selectedAction === button.action"
@click="handleAction(button.action)"
>
{{ button.text }}
</el-button>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.form-button-container {
/*display: flex;
//flex-direction: column;*/
min-height: 500px;
position: relative;
height: 100vh; /* 使用视口高度 */
}
/* 主要内容区域 */
.main-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
/* 表单信息容器 */
.form-info-container {
margin-bottom: 20px;
}
/* 流程图和办理记录共用样式 */
.process-diagram-container,
.process-records-container {
margin-bottom: 30px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
background-color: #fff;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.display-mode-selector {
margin-left: auto;
}
/* 流程图样式 */
.process-diagram {
min-height: 120px;
}
.mock-process-diagram {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
padding: 20px 0;
}
.mock-node {
padding: 10px 15px;
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin: 0 5px;
position: relative;
}
.mock-node.start {
background-color: #f0f9eb;
border-color: #e1f3d8;
color: #67c23a;
}
.mock-node.end {
background-color: #f0f2f5;
border-color: #dcdfe6;
color: #909399;
}
.mock-node.active {
background-color: #ecf5ff;
border-color: #d9ecff;
color: #409eff;
font-weight: bold;
}
.mock-arrow {
width: 30px;
height: 2px;
background-color: #dcdfe6;
position: relative;
}
.mock-arrow:after {
content: "";
position: absolute;
right: 0;
top: -3px;
width: 0;
height: 0;
border-style: solid;
border-width: 4px 0 4px 6px;
border-color: transparent transparent transparent #dcdfe6;
}
/* 办理记录样式 */
.process-records {
padding: 10px 0;
}
/* 时间线模式样式 */
.record-content {
padding: 5px 0;
}
.record-title {
font-weight: bold;
margin-bottom: 5px;
}
.record-action {
color: #409eff;
margin-bottom: 5px;
}
.record-comment {
color: #606266;
background-color: #f5f7fa;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
}
/* 表格模式样式 */
.no-comment {
color: #909399;
font-style: italic;
}
/* 底部固定操作区域 */
.footer-actions {
border-top: 1px solid #ebeef5;
padding: 20px;
background-color: #fff;
//position: sticky;
//bottom: 0;
z-index: 10;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.comment-container,
.current-task-container,
.flow-container,
.reject-container {
margin-bottom: 15px;
}
.comment-label,
.current-task-label,
.flow-label,
.reject-label {
margin-bottom: 5px;
font-weight: bold;
}
.current-task-info {
padding: 5px 0;
line-height: 1.5;
}
.target-node-info {
padding: 5px 0;
line-height: 1.5;
}
.button-container {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
</style>
@@ -0,0 +1,728 @@
<template>
<div class="flow-container">
<!-- 页面标题栏 -->
<div class="page-header">
<div class="header-content">
<div class="title-section">
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="page-breadcrumb">
<span>工作流管理</span>
<span class="separator">></span>
<span>{{ isNewApplication ? "发起申请" : "任务处理" }}</span>
</div>
</div>
<div class="status-section">
<span class="status-label">状态</span>
<span class="status-badge" :class="statusClass">{{ processStatusText }}</span>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 左侧内容 -->
<div class="content-left">
<!-- 流程基本信息 -->
<div class="info-panel">
<div class="panel-header">
<h3 class="panel-title">流程信息</h3>
<div class="panel-actions">
<el-button size="small" type="text" @click="showProcessDiagram" v-if="canShowDiagram">
<i class="el-icon-view"></i>
查看流程图
</el-button>
</div>
</div>
<div class="panel-content">
<div class="info-table">
<!-- 新申请场景 -->
<template v-if="isNewApplication">
<div class="info-row">
<div class="info-label">流程名称</div>
<div class="info-value">{{ processDefinition.displayName || processDefinition.name }}</div>
</div>
<div class="info-row">
<div class="info-label">流程说明</div>
<div class="info-value">{{ processDefinition.description }}</div>
</div>
</template>
<!-- 已有流程场景 -->
<template v-else-if="isExistingProcess">
<div class="info-row">
<div class="info-label">流程名称</div>
<div class="info-value">{{ processInstance.displayName }}</div>
</div>
<div class="info-row">
<div class="info-label">发起人</div>
<div class="info-value">{{ processInstance?.ext?.initiatorName || "-" }}</div>
</div>
<div class="info-row">
<div class="info-label">发起时间</div>
<div class="info-value">{{ formatDate(processInstance.createdAt) }}</div>
</div>
</template>
</div>
</div>
</div>
<!-- 历史办理过程面板 -->
<div v-if="shouldShowHistoryProcess" class="history-panel">
<div class="panel-header">
<h3 class="panel-title">办理过程</h3>
</div>
<div class="panel-content">
<div id="history-process-container" class="form-content">
<div v-if="pjaxLoading.historyProcess" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载历史办理过程...</span>
</div>
</div>
</div>
</div>
<!-- 表单处理区域 -->
<div class="form-panel" v-if="isNewApplication || todoTasks.map((v) => v.id).includes(taskId)">
<div class="panel-header">
<h3 class="panel-title">{{ formPanelTitle }}</h3>
</div>
<div class="panel-content">
<!-- 新申请表单 -->
<div v-if="isNewApplication" id="application-form-container" class="form-content">
<div v-if="pjaxLoading.apply" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载申请表单...</span>
</div>
</div>
<!-- 当前任务表单 -->
<div v-if="shouldShowTaskForm" id="task-form-container" class="form-content">
<div v-if="pjaxLoading.taskForm" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载任务表单...</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧审批记录 -->
<!-- <div class="content-right" v-if="isExistingProcess">-->
<!-- <snaker-flow-history-approval :task_id="taskId"></snaker-flow-history-approval>-->
<!-- </div>-->
</div>
<!-- 流程图对话框 -->
<el-dialog title="流程图预览" :visible.sync="designVisible" width="80%" top="8vh">
<div class="diagram-viewer">
<iframe v-if="designVisible" :src="designUrl" frameborder="0"></iframe>
</div>
</el-dialog>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlow",
data() {
return {
// URL参数
taskId: GetQueryString("taskId") ? parseInt(GetQueryString("taskId")) : null,
instanceId: GetQueryString("instanceId"),
businessId: GetQueryString("businessId"),
defineKey: GetQueryString("defineKey"), // 新增:流程定义KEY
processInstance: {},
//流程定义信息
processDefinition: {},
todoTasks: [],
currentTask: {},
// 加载状态
loading: false,
// 错误信息
error: null,
// PJAX 加载状态
pjaxLoading: {
apply: false,
taskForm: false,
historyProcess: false
},
// 显示历史记录
showHistory: false,
// 流程图相关
designVisible: false,
designUrl: "",
// 流程状态枚举映射
processStatusMap: {
10: { text: "进行中", class: "doing" },
20: { text: "已完成", class: "finished" },
30: { text: "已撤回", class: "withdraw" },
40: { text: "强行终止", class: "interrupt" },
45: { text: "已拒绝", class: "reject" },
50: { text: "挂起", class: "pending" },
99: { text: "已废弃", class: "abandon" }
},
// 任务状态枚举
taskStateEnum: {
DOING: 10,
FINISHED: 20,
WITHDRAW: 30,
INTERRUPT: 40,
PENDING: 50,
ABANDON: 99
},
// 任务类型枚举
performTypeEnum: {
NORMAL: 0, // 普通任务
COUNTERSIGN: 1 // 会签任务
}
}
},
computed: {
// 页面标题
pageTitle() {
if (this.isNewApplication) {
return this.processDefinition.displayName || this.processDefinition.name || "发起申请"
} else {
return this.processInstance.displayName || "任务处理"
}
},
// 流程状态文本
processStatusText() {
if (this.isNewApplication) {
return "待提交"
} else {
return this.processStatusMap[this.processInstance.state]?.text || "未知状态"
}
},
// 状态样式类
statusClass() {
if (this.isNewApplication) {
return "status-pending"
} else {
const statusClass = this.processStatusMap[this.processInstance.state]?.class
return `status-${statusClass || "unknown"}`
}
},
// 表单面板标题
formPanelTitle() {
if (this.isNewApplication) {
return "申请表单"
} else if (this.shouldShowTaskForm) {
return this.currentTask.displayName || "任务表单"
} else if (this.shouldShowHistoryProcess) {
return "办理过程"
} else {
return "表单信息"
}
},
// 是否显示历史办理过程
shouldShowHistoryProcess() {
// debugger
// return this.businessId && this.isTaskInProgress && !this.isFirstTaskNode
return true
},
// 是否显示任务表单
shouldShowTaskForm() {
debugger
return this.taskId != null
},
// 是否可以显示流程图
canShowDiagram() {
return (this.isNewApplication && this.processDefinition.id) || (this.isExistingProcess && this.processInstance.processId)
},
// 判断是否为新申请
isNewApplication() {
return !this.taskId && !this.instanceId && this.defineKey
},
// 判断是否为已有流程实例
isExistingProcess() {
return this.taskId || this.instanceId
},
// 判断任务是否进行中
isTaskInProgress() {
return this.currentTask && this.currentTask.taskState === this.taskStateEnum.DOING
},
// 判断是否为第一个任务节点
isFirstTaskNode() {
return this.currentTask && this.currentTask.ext && this.currentTask.ext.isFirstTaskNode
},
// 流程图预览URL
processDesignUrl() {
if (this.isNewApplication && this.processDefinition.id) {
return `/flow/define/preview?id=${this.processDefinition.id}`
} else if (this.isExistingProcess && this.processInstance.processId) {
return `/flow/define/preview?id=${this.processInstance.processId}`
}
return ""
}
},
created() {
this.initComponent()
// 监听
window.addEventListener("message", (event) => {
if (event.data.type === "task-complete") {
this.initComponent()
}
})
},
methods: {
// 初始化组件
async initComponent() {
this.loading = true
debugger
if (this.isNewApplication) {
// 新申请:加载流程定义信息
await this.loadProcessDefinition()
// 加载流程申请表单
await this.loadApplicationForm()
} else if (this.isExistingProcess) {
// 已有流程:加载流程实例信息
await this.loadProcessInstance()
// 加载任务信息
await this.loadCurrentTask()
// 如果有businessId且不是第一个任务节点,加载历史办理过程
if (this.businessId) {
await this.loadHistoryProcess()
}
} else {
console.warn("URL中缺少必要的参数(defineKey、taskId或instanceId),无法初始化组件")
this.loading = false
return
}
this.loading = false
},
// 加载流程定义信息
async loadProcessDefinition() {
if (!this.defineKey) return
try {
const response = await $.get("/flow/common/defineInfo", { defineKey: this.defineKey })
if (response.code === 0) {
this.processDefinition = response.data
}
} catch (error) {
console.error("加载流程定义信息失败:", error)
}
},
// 加载申请表单
async loadApplicationForm() {
debugger
if (!this.defineKey || !this.processDefinition.instanceUrl) return
this.pjaxLoading.apply = true
$.pjax({
url: this.processDefinition.instanceUrl,
container: "#application-form-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.apply = false
})
.fail(() => {
this.pjaxLoading.apply = false
console.log(this.processDefinition.instanceUrl, "申请表单加载失败")
})
},
// 加载流程实例信息
async loadProcessInstance() {
if (!this.instanceId) return
const { code, data, msg } = await $.get("/flow/common/instanceInfo", { instanceId: this.instanceId })
if (code === 0) {
this.processInstance = data.processInstance
this.todoTasks = data.todoTasks
}
},
// 加载任务信息
async loadCurrentTask() {
if (!this.taskId) return
$.get("/flow/common/taskInfo", { taskId: this.taskId }, (response) => {
if (response.code === 0) {
this.currentTask = response.data
this.loadTaskForm()
}
})
},
// 加载任务表单
async loadTaskForm() {
if (!this.taskId || !this.currentTask?.taskModel?.form) return
if (!this.todoTasks.map((v) => v.id).includes(this.taskId)) {
return
}
this.pjaxLoading.taskForm = true
$.pjax({
url: this.currentTask.taskModel.form,
container: "#task-form-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.taskForm = false
})
.fail(() => {
this.pjaxLoading.taskForm = false
console.log(this.currentTask.taskModel.form, "任务表单加载失败")
})
},
// 加载历史办理过程
async loadHistoryProcess() {
if (!this.businessId) return
this.pjaxLoading.historyProcess = true
$.pjax({
url: `/platform/article/common/fullForm?businessId=${this.businessId}`,
container: "#history-process-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.historyProcess = false
})
.fail(() => {
this.pjaxLoading.historyProcess = false
console.log("历史办理过程加载失败")
})
},
// 显示流程图
showProcessDiagram() {
if (this.processDesignUrl) {
this.designUrl = this.processDesignUrl
this.designVisible = true
}
},
// 获取状态文本
getStatusText(state) {
return this.processStatusMap[state]?.text || "未知状态"
},
// 获取状态样式类
getStatusClass(state) {
return `status-${this.processStatusMap[state]?.class || "unknown"}`
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return "-"
return new Date(dateStr).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
}
}
}
</script>
<style scoped>
/* 简洁OA风格样式 */
.flow-container {
background-color: #f5f6fa;
min-height: 100vh;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
/* 页面头部 - 固定在顶部 */
.page-header {
background: white;
border-bottom: 1px solid #e4e7ed;
padding: 16px 0;
position: fixed;
top: 64px;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 主要内容区域 - 添加顶部间距 */
.main-content {
max-width: 1200px;
margin: 0 auto;
margin-top: 80px; /* 为固定头部留出空间 */
padding: 20px;
display: flex;
gap: 20px;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-section .page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin: 0 0 4px 0;
}
.page-breadcrumb {
font-size: 12px;
color: #909399;
}
.separator {
margin: 0 6px;
}
.status-section {
display: flex;
align-items: center;
gap: 8px;
}
.status-label {
font-size: 14px;
color: #606266;
}
.status-badge {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fdf6ec;
color: #e6a23c;
}
.status-doing {
background: #ecf5ff;
color: var(--color-primary);
}
.status-finished {
background: #f0f9ff;
color: #67c23a;
}
.status-withdraw {
background: #f4f4f5;
color: #909399;
}
.status-interrupt {
background: #fef0f0;
color: #f56c6c;
}
.status-reject {
background: #fef0f0;
color: #f56c6c;
}
.content-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.content-right {
width: 320px;
flex-shrink: 0;
}
/* 面板样式 */
.info-panel,
.form-panel,
.history-panel,
.approval-panel {
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.panel-header {
background: #fafafa;
border-bottom: 1px solid #e4e7ed;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin: 0;
}
.panel-actions {
display: flex;
gap: 8px;
}
.panel-content {
padding: 16px;
}
.panel-content.no-padding {
padding: 0;
}
/* 信息表格 */
.info-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f5f7fa;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #606266;
font-size: 14px;
min-width: 80px;
font-weight: 500;
}
.info-value {
color: #303133;
font-size: 14px;
flex: 1;
line-height: 1.5;
}
/* 表单内容 */
.form-content {
min-height: 200px;
position: relative;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
gap: 8px;
}
.loading-state i {
font-size: 20px;
animation: spin 1s linear infinite;
}
.loading-state span {
font-size: 14px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 流程图查看器 */
.diagram-viewer {
height: 65vh;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.diagram-viewer iframe {
width: 100%;
height: 100%;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
.content-right {
width: 100%;
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.main-content {
padding: 16px;
}
.panel-content {
padding: 12px;
}
.info-row {
flex-direction: column;
gap: 4px;
}
.info-label {
min-width: auto;
}
}
</style>
@@ -0,0 +1,380 @@
<template>
<div class="approval-sidebar">
<div class="sidebar-header">
<h4>审批记录</h4>
</div>
<div class="timeline-container" v-if="historyApprovalRecords.length > 0">
<div class="timeline-wrapper">
<div
v-for="(record, index) in historyApprovalRecords"
:key="record.id"
class="timeline-item">
<!-- 时间线节点 -->
<div class="timeline-node">
<div class="timeline-dot" :class="getDotClass(record.taskState)"></div>
<div class="timeline-line" v-if="index < historyApprovalRecords.length - 1"></div>
</div>
<!-- 审批内容 -->
<div class="timeline-content">
<div class="approval-card">
<div class="card-header">
<span class="task-name">{{ record.displayName }}</span>
<span class="approval-time">{{ formatTimestamp(record.finishTime || record.createdAt) }}</span>
</div>
<div class="card-body">
<div class="info-item" v-if="record.operator && record.operator !== 'flow.auto'">
<span class="label">办理人</span>
<span class="value">{{ getOperatorName(record) }}</span>
</div>
<div class="info-item" v-if="record.ext && record.ext.submitType">
<span class="label">操作</span>
<span class="value submit-type" :class="'submit-' + record.ext.submitType">
{{ getSubmitTypeText(record.ext.submitType) }}
</span>
</div>
<div class="info-item" v-if="record.ext && record.ext.opinion">
<span class="label">意见</span>
<span class="value">{{ record.ext.opinion }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-records">
暂无审批记录
</div>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlowHisApproval",
props: {
task_id: {
type: String | Number,
required: true
}
},
data() {
return {
// 历史审批记录
historyApprovalRecords: [],
// 加载状态
loading: false,
// 任务状态枚举
taskStateEnum: {
DOING: 10,
FINISHED: 20,
WITHDRAW: 30,
INTERRUPT: 40,
PENDING: 50,
ABANDON: 99
}
}
},
computed: {
// 获取实例ID(从task_id获取流程实例信息)
instanceId() {
return GetQueryString("instanceId")
}
},
created() {
this.loadHistoryApprovalRecords()
},
watch: {
task_id: {
handler(newVal) {
if (newVal) {
this.loadHistoryApprovalRecords()
}
},
immediate: true
}
},
methods: {
// 加载历史审批记录
async loadHistoryApprovalRecords() {
if (!this.instanceId && !this.task_id) return
this.loading = true
try {
const params = this.instanceId ?
{ instanceId: this.instanceId } :
{ taskId: this.task_id }
const response = await $.post("/flow/common/approvalRecord", params)
if (response.code === 0) {
this.historyApprovalRecords = response.data || []
}
} catch (error) {
console.error("加载历史审批记录失败:", error)
} finally {
this.loading = false
}
},
// 格式化时间戳
formatTimestamp(timestamp) {
if (!timestamp) return ""
const date = new Date(timestamp)
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
},
// 获取操作人姓名
getOperatorName(record) {
if (!record.operator || record.operator === 'flow.auto') {
return "系统自动"
}
if (record.ext && record.ext.operatorName) {
return record.ext.operatorName
}
return record.operator
},
// 获取提交类型文本
getSubmitTypeText(submitType) {
const typeMap = {
'agree': '同意',
'reject': '拒绝',
'back': '退回',
'transfer': '转办',
'delegate': '委托',
'submit': '提交'
}
return typeMap[submitType] || submitType
},
// 获取节点样式类
getDotClass(state) {
switch (state) {
case this.taskStateEnum.FINISHED:
return "dot-success"
case this.taskStateEnum.WITHDRAW:
return "dot-warning"
case this.taskStateEnum.INTERRUPT:
case this.taskStateEnum.ABANDON:
return "dot-danger"
case this.taskStateEnum.PENDING:
return "dot-info"
case this.taskStateEnum.DOING:
return "dot-primary"
default:
return "dot-default"
}
}
}
}
</script>
<style scoped>
.approval-sidebar {
width: 320px;
background: #fff;
border: 1px solid #ddd;
height: fit-content;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.sidebar-header {
background: #f5f5f5;
border-bottom: 1px solid #ddd;
padding: 12px 16px;
flex-shrink: 0;
}
.sidebar-header h4 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
}
.timeline-container {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.timeline-wrapper {
position: relative;
}
.timeline-item {
display: flex;
margin-bottom: 20px;
position: relative;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-node {
position: relative;
margin-right: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid #fff;
position: relative;
z-index: 2;
}
.dot-success {
background: #52c41a;
}
.dot-warning {
background: #faad14;
}
.dot-danger {
background: #f5222d;
}
.dot-primary {
background: #1890ff;
}
.dot-info {
background: #999;
}
.dot-default {
background: #d9d9d9;
}
.timeline-line {
width: 1px;
background: #e8e8e8;
flex: 1;
margin-top: 4px;
min-height: 30px;
}
.timeline-content {
flex: 1;
min-width: 0;
}
.approval-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
}
.card-header {
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.task-name {
font-size: 13px;
font-weight: bold;
color: #333;
flex: 1;
}
.approval-time {
font-size: 12px;
color: #999;
}
.card-body {
padding: 12px;
}
.info-item {
display: flex;
margin-bottom: 6px;
font-size: 12px;
line-height: 1.5;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
color: #666;
min-width: 50px;
flex-shrink: 0;
}
.value {
color: #333;
flex: 1;
word-break: break-all;
}
.submit-type {
font-weight: bold;
}
.submit-agree {
color: #52c41a;
}
.submit-reject {
color: #f5222d;
}
.submit-back {
color: #faad14;
}
.submit-transfer, .submit-delegate, .submit-submit {
color: #1890ff;
}
.no-records {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
}
/* 滚动条样式 */
.timeline-container::-webkit-scrollbar {
width: 6px;
}
.timeline-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.timeline-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
}
.timeline-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -0,0 +1,226 @@
<template>
<div class="snaker-task-action" v-if="!loading || taskInfo">
<!-- 第一个任务节点或新申请显示提交保存草稿取消 -->
<template v-if="isFirstTaskNodeOrNew">
<el-button type="primary" @click="handleSubmit" :loading="actionLoading" icon="el-icon-upload2">提交</el-button>
<!-- 保存草稿只在流程未发起时显示 -->
<el-button @click="handleSaveDraft" v-if="!actionLoading && show_save_draft && !instanceId" icon="el-icon-document">保存草稿</el-button>
<el-button @click="handleCancel" :loading="actionLoading" v-if="show_cancel" icon="el-icon-close">取消</el-button>
</template>
<!-- 其他任务节点显示审批按钮 -->
<template v-else>
<!-- 会签任务按钮 -->
<template v-if="is_countersign_task">
<el-button type="primary" @click="handleAction('AGREE')" :loading="actionLoading" icon="el-icon-check">同意</el-button>
<el-button type="danger" plain @click="handleAction('COUNTERSIGN_DISAGREE')" :loading="actionLoading" icon="el-icon-close">
不同意
</el-button>
</template>
<!-- 普通任务按钮 -->
<template v-else>
<el-button type="primary" @click="handleAction('AGREE')" :loading="actionLoading" icon="el-icon-check">同意</el-button>
<el-button type="danger" plain @click="handleAction('REJECT')" :loading="actionLoading" icon="el-icon-close">拒绝</el-button>
<el-button @click="handleAction('ROLLBACK')" :loading="actionLoading" icon="el-icon-back">退回上一步</el-button>
<el-button @click="handleAction('ROLLBACK_TO_OPERATOR')" :loading="actionLoading" icon="el-icon-d-arrow-left">退回发起人</el-button>
<el-button @click="handleAction('JUMP')" :loading="actionLoading" icon="el-icon-position">跳转</el-button>
</template>
</template>
</div>
<!-- 加载状态 -->
<div v-else class="snaker-task-action loading-state">
<el-button loading icon="el-icon-loading">加载中...</el-button>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlowTaskFormAction",
props: {
// 是否显示保存草稿按钮
show_save_draft: {
type: Boolean,
default: true
},
// 是否显示取消按钮
show_cancel: {
type: Boolean,
default: true
}
},
data() {
return {
// 加载状态
loading: false,
// 操作按钮加载状态
actionLoading: false,
// 任务信息
taskInfo: null,
// 任务操作枚举
actionEnum: {
APPLY: "0", // 发起申请
AGREE: "1", // 同意
REJECT: "2", // 拒绝
COUNTERSIGN_DISAGREE: "20", // 会签不同意 拒绝申请
ROLLBACK: "3", // 退回上一步
ROLLBACK_TO_OPERATOR: "6", // 退回发起人
JUMP: "4", // 跳转
RE_APPLY: "5", // 重新提交
SUBMIT: "0" // 发起申请
},
// 任务类型枚举
performTypeEnum: {
NORMAL: 0, // 普通任务
COUNTERSIGN: 1 // 会签任务
},
taskId: GetQueryString("taskId"),
instanceId: GetQueryString("instanceId"),
defineKey: GetQueryString("defineKey"),
defineId: GetQueryString("defineId"),
businessId: GetQueryString("businessId")
}
},
computed: {
// 是否为会签任务
is_countersign_task() {
return this.taskInfo && this.taskInfo.performType === this.performTypeEnum.COUNTERSIGN
},
// 是否为第一个任务节点或新申请
isFirstTaskNodeOrNew() {
// 没有任务ID,说明是新申请
if (!this.taskId) {
return true
}
// 有任务ID但是任务信息中标识为第一个节点
return this.taskInfo && this.taskInfo.ext && this.taskInfo.ext.isFirstTaskNode
}
},
mounted() {
// 只有当有任务ID时才加载任务信息
if (this.taskId) {
this.loadTaskInfo()
}
},
methods: {
// 加载任务信息
loadTaskInfo() {
if (!this.taskId) return
this.loading = true
this.taskInfo = null
$.get("/flow/common/taskInfo", {
taskId: this.taskId
})
.then((res) => {
if (res.code === 0) {
this.taskInfo = res.data
}
})
.always(() => {
this.loading = false
})
},
// 处理提交操作
handleSubmit() {
// 根据是否有流程实例来确定操作类型
// const submitAction = this.instanceId ? this.actionEnum.RE_APPLY : this.actionEnum.APPLY
const submitAction = this.actionEnum.APPLY
// 触发父组件事件,传递提交操作
this.$emit("task-action", {
submitType: submitAction,
taskId: this.taskId,
instanceId: this.instanceId,
defineId: this.defineId,
defineKey: this.defineKey,
businessId: this.businessId
// currentTask: this.taskInfo
})
},
// 处理任务操作
handleAction(actionKey) {
if (this.actionLoading || !this.taskInfo) return
// 触发父组件事件,传递操作类型和任务信息
this.$emit("task-action", {
submitType: this.actionEnum[actionKey],
taskId: this.taskId,
processTaskId: this.taskId,
instanceId: this.instanceId,
defineId: this.defineId,
defineKey: this.defineKey,
businessId: this.businessId
})
},
// 保存草稿
handleSaveDraft() {
if (this.actionLoading) return
// 保存草稿时不显示加载状态,因为只是保存业务数据
this.$emit("save-draft", {
defineId: this.defineId,
businessId: this.businessId,
isDraft: true // 标识这是草稿保存
})
},
// 取消操作
handleCancel() {
this.$emit("cancel")
},
// 刷新任务信息
refresh() {
if (this.taskId) {
this.loadTaskInfo()
}
}
}
}
</script>
<style scoped>
.snaker-task-action {
text-align: center;
padding: 20px;
background: #fff;
border-top: 1px solid #eee;
margin-top: 20px;
}
.snaker-task-action .el-button {
margin: 0 8px;
min-width: 80px;
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 60px;
}
/* 响应式 */
@media (max-width: 768px) {
.snaker-task-action {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
}
.snaker-task-action .el-button {
width: 100%;
margin: 0;
}
}
</style>
@@ -0,0 +1,84 @@
<template>
<div>
<template v-for="item in options">
<el-tag :key="item[value_key]" v-if="item[value_key] === value" v-bind="$attrs">{{ item[label_key] }}</el-tag>
</template>
</div>
</template>
<script>
// 全局缓存和请求Promise缓存
const enumCache = {}
const requestPromises = {}
module.exports = {
name: "DictTag",
props: {
value: { type: String | Number },
name: { type: String },
value_key: {
type: String,
default: "code"
},
label_key: {
type: String,
default: "name"
}
},
data() {
return {
options: []
}
},
watch: {
code:{
handler(val) {
this.getEnumOptions()
},
immediate: true
}
},
methods: {
getEnumOptions() {
// // 检查数据缓存
// if (enumCache[this.name]) {
// this.options = enumCache[this.name]
// return
// }
//
// // 检查是否已有相同请求在进行中
// if (requestPromises[this.name]) {
// requestPromises[this.name].then(data => {
// this.options = data
// })
// return
// }
// 创建请求Promise并缓存
requestPromises[this.name] = $.get("/open/common/dictEnumOptions", { name: this.name })
.then(res => {
if (res.code === 0) {
// 存入数据缓存
enumCache[this.name] = res.data
// 清除请求Promise缓存
delete requestPromises[this.name]
return res.data
}
})
requestPromises[this.name].then(data => {
this.options = data
})
}
},
created() {
}
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>