|
@@ -2,7 +2,7 @@
|
|
|
<el-dialog
|
|
<el-dialog
|
|
|
v-model="visible"
|
|
v-model="visible"
|
|
|
title="流程详情"
|
|
title="流程详情"
|
|
|
- width="850px"
|
|
|
|
|
|
|
+ width="900px"
|
|
|
:close-on-click-modal="false"
|
|
:close-on-click-modal="false"
|
|
|
@close="handleClose"
|
|
@close="handleClose"
|
|
|
>
|
|
>
|
|
@@ -25,6 +25,36 @@
|
|
|
</el-descriptions>
|
|
</el-descriptions>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 表单数据 -->
|
|
|
|
|
+ <div v-if="formDataDisplay" class="form-data-section">
|
|
|
|
|
+ <h4>表单数据</h4>
|
|
|
|
|
+ <el-descriptions :column="2" size="small" border>
|
|
|
|
|
+ <el-descriptions-item v-for="(value, key) in formDataDisplay" :key="key" :label="String(key)">
|
|
|
|
|
+ {{ value }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 附件列表 -->
|
|
|
|
|
+ <div v-if="instanceAttachments.length > 0" class="attachment-section">
|
|
|
|
|
+ <h4>附件列表</h4>
|
|
|
|
|
+ <div class="attachment-list">
|
|
|
|
|
+ <div v-for="(url, idx) in instanceAttachments" :key="idx" class="attachment-item">
|
|
|
|
|
+ <el-link type="primary" :href="getFileUrl(url)" target="_blank">
|
|
|
|
|
+ <el-icon><Document /></el-icon>
|
|
|
|
|
+ {{ getFileName(url) }}
|
|
|
|
|
+ </el-link>
|
|
|
|
|
+ <el-image
|
|
|
|
|
+ v-if="isImage(url)"
|
|
|
|
|
+ :src="getFileUrl(url)"
|
|
|
|
|
+ :preview-src-list="imagePreviewList"
|
|
|
|
|
+ fit="cover"
|
|
|
|
|
+ style="width: 60px; height: 60px; margin-left: 8px;"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<el-divider />
|
|
<el-divider />
|
|
|
|
|
|
|
|
<div class="detail-body">
|
|
<div class="detail-body">
|
|
@@ -75,14 +105,38 @@
|
|
|
<el-radio label="transfer">转办</el-radio>
|
|
<el-radio label="transfer">转办</el-radio>
|
|
|
</el-radio-group>
|
|
</el-radio-group>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
|
|
+ <el-form-item v-if="form.action === 'rollback'" label="回退到">
|
|
|
|
|
+ <el-select v-model="form.targetNodeId" filterable placeholder="请选择回退节点" style="width: 100%">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="node in rollbackNodeOptions"
|
|
|
|
|
+ :key="node.nodeId"
|
|
|
|
|
+ :label="node.nodeName"
|
|
|
|
|
+ :value="node.nodeId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
<el-form-item v-if="form.action === 'transfer'" label="转给人">
|
|
<el-form-item v-if="form.action === 'transfer'" label="转给人">
|
|
|
- <el-input v-model="form.transferTo" placeholder="请输入用户ID" />
|
|
|
|
|
|
|
+ <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
|
|
|
|
|
+ <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="附件">
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ v-model:file-list="attachmentList"
|
|
|
|
|
+ action="#"
|
|
|
|
|
+ :http-request="handleUpload"
|
|
|
|
|
+ :before-remove="() => true"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button type="primary" size="small">上传附件</el-button>
|
|
|
|
|
+ </el-upload>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="意见">
|
|
<el-form-item label="意见">
|
|
|
<el-input v-model="form.comment" type="textarea" placeholder="请输入审批意见" />
|
|
<el-input v-model="form.comment" type="textarea" placeholder="请输入审批意见" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
<el-form-item>
|
|
|
<el-button type="primary" @click="submitApprove">提交</el-button>
|
|
<el-button type="primary" @click="submitApprove">提交</el-button>
|
|
|
|
|
+ <el-button @click="handleAddSign">加签</el-button>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
</template>
|
|
</template>
|
|
@@ -108,6 +162,18 @@
|
|
|
<span class="record-time">{{ record.createTime }}</span>
|
|
<span class="record-time">{{ record.createTime }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="record.comment" class="record-comment">{{ record.comment }}</div>
|
|
<div v-if="record.comment" class="record-comment">{{ record.comment }}</div>
|
|
|
|
|
+ <div v-if="record.attachmentUrls" class="record-attachments">
|
|
|
|
|
+ <el-link
|
|
|
|
|
+ v-for="(url, idx) in parseAttachments(record.attachmentUrls)"
|
|
|
|
|
+ :key="idx"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ :href="getFileUrl(url)"
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ >
|
|
|
|
|
+ 附件{{ idx + 1 }}: {{ getFileName(url) }}
|
|
|
|
|
+ </el-link>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<el-empty v-else description="暂无审批记录" />
|
|
<el-empty v-else description="暂无审批记录" />
|
|
@@ -121,9 +187,12 @@
|
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { getProgress } from '@/api/flow/instance'
|
|
import { getProgress } from '@/api/flow/instance'
|
|
|
-import { approveTask } from '@/api/flow/task'
|
|
|
|
|
-import type { ProcessProgress, FlowTask } from '@/types/flow'
|
|
|
|
|
-import { Check, CircleCheck, Clock, Warning } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
+import { approveTask, rejectTask, returnTask, transferTask, addSignTask } from '@/api/flow/task'
|
|
|
|
|
+import { listUser } from '@/api/system/user'
|
|
|
|
|
+import { uploadFile } from '@/api/file'
|
|
|
|
|
+import type { ProcessProgress, FlowTask, NodeProgress } from '@/types/flow'
|
|
|
|
|
+import type { User } from '@/types/system'
|
|
|
|
|
+import { Check, CircleCheck, Clock, Warning, Document } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
const props = defineProps<{
|
|
|
modelValue: boolean
|
|
modelValue: boolean
|
|
@@ -146,9 +215,88 @@ const progress = ref<ProcessProgress | null>(null)
|
|
|
const form = reactive({
|
|
const form = reactive({
|
|
|
action: 'pass',
|
|
action: 'pass',
|
|
|
comment: '',
|
|
comment: '',
|
|
|
- transferTo: ''
|
|
|
|
|
|
|
+ transferTo: undefined as number | undefined,
|
|
|
|
|
+ targetNodeId: undefined as string | undefined
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const attachmentList = ref<any[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const rollbackNodeOptions = computed<NodeProgress[]>(() => {
|
|
|
|
|
+ if (!progress.value) return []
|
|
|
|
|
+ return progress.value.nodes.filter(n => n.status === 'completed')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const userList = ref<User[]>([])
|
|
|
|
|
+
|
|
|
|
|
+async function loadUsers() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await listUser({ pageNum: 1, pageSize: 9999 })
|
|
|
|
|
+ userList.value = res.list
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // ignore
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析表单数据
|
|
|
|
|
+const formDataDisplay = computed(() => {
|
|
|
|
|
+ const formDataStr = (progress.value?.instance as any)?.formData
|
|
|
|
|
+ if (!formDataStr) return null
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(formDataStr)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return { 原始数据: formDataStr }
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 实例附件列表
|
|
|
|
|
+const instanceAttachments = computed(() => {
|
|
|
|
|
+ const urlsStr = (progress.value?.instance as any)?.attachmentUrls
|
|
|
|
|
+ if (!urlsStr) return []
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(urlsStr) as string[]
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 图片预览列表
|
|
|
|
|
+const imagePreviewList = computed(() => {
|
|
|
|
|
+ return instanceAttachments.value.filter(isImage).map(getFileUrl)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function isImage(url: string): boolean {
|
|
|
|
|
+ return /\.(png|jpe?g|gif|webp|bmp)$/i.test(url)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFileUrl(url: string): string {
|
|
|
|
|
+ if (url.startsWith('http')) return url
|
|
|
|
|
+ // 相对路径,拼接当前页面 origin(通过 /uploads 代理到后端)
|
|
|
|
|
+ return window.location.origin + url
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFileName(url: string): string {
|
|
|
|
|
+ const parts = url.split('/')
|
|
|
|
|
+ return parts[parts.length - 1] || url
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseAttachments(urlsStr?: string): string[] {
|
|
|
|
|
+ if (!urlsStr) return []
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(urlsStr) as string[]
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return urlsStr ? [urlsStr] : []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleUpload(options: any) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await uploadFile(options.file)
|
|
|
|
|
+ options.onSuccess(res)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ options.onError(e)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 查找当前登录用户需要处理的待办任务
|
|
// 查找当前登录用户需要处理的待办任务
|
|
|
const myPendingTask = computed<FlowTask | null>(() => {
|
|
const myPendingTask = computed<FlowTask | null>(() => {
|
|
|
if (!progress.value) return null
|
|
if (!progress.value) return null
|
|
@@ -163,11 +311,19 @@ const myPendingTask = computed<FlowTask | null>(() => {
|
|
|
|
|
|
|
|
function statusText(status?: number) {
|
|
function statusText(status?: number) {
|
|
|
if (status === undefined) return '未知'
|
|
if (status === undefined) return '未知'
|
|
|
- return ['运行中', '已完成', '已终止'][status] || '未知'
|
|
|
|
|
|
|
+ const map: Record<number, string> = {
|
|
|
|
|
+ 0: '待接收', 1: '运行中', 2: '已通过', 3: '已拒绝',
|
|
|
|
|
+ 4: '已回退', 5: '已完成', 6: '已撤回', 7: '已终止'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[status] || '未知'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function statusTagType(status?: number) {
|
|
function statusTagType(status?: number) {
|
|
|
- return ['primary', 'success', 'danger'][status ?? 0] || 'info'
|
|
|
|
|
|
|
+ const map: Record<number, any> = {
|
|
|
|
|
+ 0: 'info', 1: 'primary', 2: 'success', 3: 'danger',
|
|
|
|
|
+ 4: 'warning', 5: 'success', 6: 'info', 7: 'danger'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[status ?? 0] || 'info'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function timelineType(status: string) {
|
|
function timelineType(status: string) {
|
|
@@ -220,24 +376,57 @@ async function loadProgress() {
|
|
|
async function submitApprove() {
|
|
async function submitApprove() {
|
|
|
const task = myPendingTask.value
|
|
const task = myPendingTask.value
|
|
|
if (!task) return
|
|
if (!task) return
|
|
|
- const data = {
|
|
|
|
|
- action: form.action as any,
|
|
|
|
|
|
|
+ const data: any = {
|
|
|
|
|
+ action: form.action,
|
|
|
comment: form.comment
|
|
comment: form.comment
|
|
|
}
|
|
}
|
|
|
if (form.action === 'transfer' && form.transferTo) {
|
|
if (form.action === 'transfer' && form.transferTo) {
|
|
|
- (data as any).transferTo = Number(form.transferTo)
|
|
|
|
|
|
|
+ data.transferTo = Number(form.transferTo)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (form.action === 'rollback' && form.targetNodeId) {
|
|
|
|
|
+ data.targetNodeId = form.targetNodeId
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attachmentList.value.length > 0) {
|
|
|
|
|
+ data.attachmentUrls = JSON.stringify(attachmentList.value.map((f: any) => f.response || f.url).filter(Boolean))
|
|
|
|
|
+ }
|
|
|
|
|
+ switch (form.action) {
|
|
|
|
|
+ case 'pass':
|
|
|
|
|
+ await approveTask(task.id, data)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'reject':
|
|
|
|
|
+ await rejectTask(task.id, data)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'rollback':
|
|
|
|
|
+ await returnTask(task.id, data)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'transfer':
|
|
|
|
|
+ await transferTask(task.id, data)
|
|
|
|
|
+ break
|
|
|
}
|
|
}
|
|
|
- await approveTask(task.id, data)
|
|
|
|
|
ElMessage.success('审批提交成功')
|
|
ElMessage.success('审批提交成功')
|
|
|
form.action = 'pass'
|
|
form.action = 'pass'
|
|
|
form.comment = ''
|
|
form.comment = ''
|
|
|
- form.transferTo = ''
|
|
|
|
|
|
|
+ form.transferTo = undefined
|
|
|
|
|
+ form.targetNodeId = undefined
|
|
|
|
|
+ attachmentList.value = []
|
|
|
|
|
+ // 刷新进度展示最新状态,不关闭弹窗
|
|
|
|
|
+ await loadProgress()
|
|
|
emit('approved')
|
|
emit('approved')
|
|
|
- loadProgress()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleClose() {
|
|
function handleClose() {
|
|
|
progress.value = null
|
|
progress.value = null
|
|
|
|
|
+ attachmentList.value = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleAddSign() {
|
|
|
|
|
+ const task = myPendingTask.value
|
|
|
|
|
+ if (!task) return
|
|
|
|
|
+ const assigneeId = window.prompt('请输入加签人用户ID')
|
|
|
|
|
+ if (!assigneeId) return
|
|
|
|
|
+ await addSignTask(task.id, Number(assigneeId))
|
|
|
|
|
+ ElMessage.success('加签成功')
|
|
|
|
|
+ loadProgress()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
watch(() => props.instanceId, (id) => {
|
|
watch(() => props.instanceId, (id) => {
|
|
@@ -249,6 +438,7 @@ watch(() => props.instanceId, (id) => {
|
|
|
watch(() => props.modelValue, (val) => {
|
|
watch(() => props.modelValue, (val) => {
|
|
|
if (val && props.instanceId) {
|
|
if (val && props.instanceId) {
|
|
|
loadProgress()
|
|
loadProgress()
|
|
|
|
|
+ loadUsers()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
@@ -261,6 +451,28 @@ watch(() => props.modelValue, (val) => {
|
|
|
.info-section {
|
|
.info-section {
|
|
|
margin-bottom: 10px;
|
|
margin-bottom: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
+.form-data-section,
|
|
|
|
|
+.attachment-section {
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.form-data-section h4,
|
|
|
|
|
+.attachment-section h4 {
|
|
|
|
|
+ margin: 0 0 8px 0;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+.attachment-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.attachment-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 6px 10px;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
.detail-body {
|
|
.detail-body {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: 24px;
|
|
gap: 24px;
|
|
@@ -344,5 +556,11 @@ watch(() => props.modelValue, (val) => {
|
|
|
background: #f5f7fa;
|
|
background: #f5f7fa;
|
|
|
padding: 6px 10px;
|
|
padding: 6px 10px;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+.record-attachments {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|