Files
zhgh_whcp/target/classes/static/components/plugins/snaker/snakerFlow.vue
T
2026-01-08 14:58:16 +08:00

776 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="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>{{ 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="isNewApplication">
<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 }}</div>
</div>
<div class="info-row">
<div class="info-label">流程说明</div>
<div class="info-value">
<div v-html="processDefinition.description"></div>
</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>
<div class="info-row">
<div class="info-label">流程进度</div>
<div class="info-value">
<span @click="showProcessDiagram" style="color: var(--color-primary); cursor: pointer">查看流程图</span>
</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 type="text/javascript">
class PjaxSync {
constructor() {
this.isLoading = false
this.queue = []
}
async request(url, options = {}) {
return new Promise((resolve, reject) => {
const requestData = { url, options, resolve, reject }
if (this.isLoading) {
this.queue.push(requestData)
return
}
this._executeRequest(requestData)
})
}
_executeRequest({ url, options, resolve, reject }) {
this.isLoading = true
const defaultOptions = {
timeout: 10000,
push: false,
replace: false,
...options
}
const successHandler = (event, data, status, xhr) => {
this._cleanup()
resolve({ data, status, xhr })
this._processQueue()
}
const errorHandler = (event, xhr, textStatus, errorThrown) => {
this._cleanup()
reject(new Error(textStatus || "PJAX request failed"))
this._processQueue()
}
$(document).one("pjax:success", successHandler)
$(document).one("pjax:error", errorHandler)
$.pjax({
url: url,
...defaultOptions
})
}
_cleanup() {
this.isLoading = false
$(document).off("pjax:success pjax:error")
}
_processQueue() {
if (this.queue.length > 0) {
const nextRequest = this.queue.shift()
this._executeRequest(nextRequest)
}
}
}
// 创建全局实例
const pjaxSync = new PjaxSync()
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() {
return this.instanceId !== null
},
// 是否显示任务表单
shouldShowTaskForm() {
return this.taskId != null
},
// 判断是否为新申请
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() {
debugger
if (this.isNewApplication && this.processDefinition.id) {
return `/flow/define/preview?defineId=${this.processDefinition.id}`
} else if (this.isExistingProcess && this.processInstance.processDefineId) {
return `/flow/define/preview?defineId=${this.processInstance.processDefineId}&instanceId=${this.instanceId}`
}
return ""
}
},
created() {
this.initComponent()
// 监听消息
new BroadcastChannel("zhgh-global-channel").addEventListener("message", (event) => {
if (event.data.type === "task-complete") {
this.initComponent()
}
})
},
beforeDestroy() {
console.log("beforeDestroy")
},
methods: {
// 初始化组件
async initComponent() {
this.loading = true
if (this.isNewApplication) {
// 新申请:加载流程定义信息
await this.loadProcessDefinition()
// 加载流程申请表单
await this.loadApplicationForm()
} else if (this.isExistingProcess) {
// 已有流程:加载流程实例信息
await this.loadProcessInstance()
if (this.processInstance && this.processInstance.state === 30) {
// 流程被撤回了 此时加载申请表单
// 加载流程申请表单
await this.loadApplicationForm()
}
// 加载任务信息
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,
instanceId: this.instanceId
})
if (response.code === 0) {
this.processDefinition = response.data
}
} catch (error) {
console.error("加载流程定义信息失败:", error)
}
},
// 加载申请表单
async loadApplicationForm() {
if (!this.defineKey || !this.processDefinition.instanceUrl) return
this.pjaxLoading.apply = true
await pjaxSync.request(this.processDefinition.instanceUrl, {
container: "#application-form-container"
})
this.pjaxLoading.apply = false
},
// 加载流程实例信息
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.processDefinition = data.processInstance?.jsonObject
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
await pjaxSync.request(this.currentTask.taskModel.form, {
container: "#task-form-container"
})
},
// 加载历史办理过程
async loadHistoryProcess() {
if (!this.businessId) return
const { instanceViewUrl } = this.processInstance.jsonObject
if (instanceViewUrl) {
this.pjaxLoading.historyProcess = true
await pjaxSync.request(`${instanceViewUrl}?businessId=${this.businessId}&instanceId=${this.instanceId}`, {
container: "#history-process-container"
})
this.pjaxLoading.historyProcess = false
}
},
// 显示流程图
showProcessDiagram() {
debugger
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: 19000;
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>