776 lines
22 KiB
Vue
776 lines
22 KiB
Vue
<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>
|