|
|
@@ -4,6 +4,7 @@
|
|
|
<template #header>
|
|
|
<div class="card-header">
|
|
|
<span>我的待办</span>
|
|
|
+ <el-tag v-if="total > 0" type="danger" effect="dark" round>{{ total }}</el-tag>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -17,18 +18,95 @@
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
- <el-table v-loading="loading" :data="tableData" border>
|
|
|
- <el-table-column type="index" label="序号" width="60" />
|
|
|
- <el-table-column prop="definitionName" label="流程名称" />
|
|
|
- <el-table-column prop="nodeName" label="当前节点" />
|
|
|
- <el-table-column prop="createTime" label="到达时间" />
|
|
|
- <el-table-column label="操作" width="200">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button link type="primary" @click="handleApprove(row)">审批</el-button>
|
|
|
- <el-button link type="warning" @click="handleAddSign(row)">加签</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <el-radio-group v-model="activeUrgency" class="urgency-tabs" @change="handleUrgencyChange">
|
|
|
+ <el-radio-button label="all">全部</el-radio-button>
|
|
|
+ <el-radio-button label="overdue">已超时</el-radio-button>
|
|
|
+ <el-radio-button label="warning">即将超时</el-radio-button>
|
|
|
+ <el-radio-button label="normal">正常</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+
|
|
|
+ <!-- 批量操作工具条 -->
|
|
|
+ <div v-if="selectedIds.length > 0" class="batch-toolbar">
|
|
|
+ <div class="batch-info">
|
|
|
+ <el-checkbox :model-value="true" @change="clearSelection" />
|
|
|
+ <span>已选择 <strong>{{ selectedIds.length }}</strong> 项</span>
|
|
|
+ </div>
|
|
|
+ <div class="batch-actions">
|
|
|
+ <el-button type="success" :icon="Check" size="small" @click="openBatchDrawer('pass')">批量通过</el-button>
|
|
|
+ <el-button type="danger" :icon="Close" size="small" @click="openBatchDrawer('reject')">批量拒绝</el-button>
|
|
|
+ <el-button size="small" @click="clearSelection">取消选择</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-loading="loading" class="task-card-list">
|
|
|
+ <el-empty v-if="!loading && tableData.length === 0" description="暂无待办任务" />
|
|
|
+ <el-card
|
|
|
+ v-for="row in filteredTableData"
|
|
|
+ :key="row.id"
|
|
|
+ class="task-card"
|
|
|
+ shadow="hover"
|
|
|
+ :class="{ 'is-selected': isSelected(row.id) }"
|
|
|
+ >
|
|
|
+ <div class="task-card-main" @click="toggleSelect(row)">
|
|
|
+ <div class="task-card-header">
|
|
|
+ <el-checkbox
|
|
|
+ :model-value="isSelected(row.id)"
|
|
|
+ @click.stop
|
|
|
+ @change="(val: any) => handleSelectChange(val, row)"
|
|
|
+ />
|
|
|
+ <div class="task-title">{{ row.definitionName || '未命名流程' }}</div>
|
|
|
+ <el-tag v-if="row.urgency === 2" type="danger" size="small" effect="dark">已超时</el-tag>
|
|
|
+ <el-tag v-else-if="row.urgency === 1" type="warning" size="small" effect="dark">即将超时</el-tag>
|
|
|
+ <el-tag v-else-if="row.timeoutTime" type="info" size="small">正常</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="task-card-body">
|
|
|
+ <div class="task-meta">
|
|
|
+ <span class="meta-label">当前节点</span>
|
|
|
+ <el-tag type="warning" size="small">{{ row.nodeName }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="task-meta">
|
|
|
+ <span class="meta-label">到达时间</span>
|
|
|
+ <span>{{ row.createTime }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="row.timeoutTime" class="task-meta">
|
|
|
+ <span class="meta-label">剩余时间</span>
|
|
|
+ <span :class="{ 'text-danger': row.urgency === 2, 'text-warning': row.urgency === 1 }">
|
|
|
+ {{ formatRemaining(row) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div v-if="row.instanceTitle" class="task-meta">
|
|
|
+ <span class="meta-label">实例标题</span>
|
|
|
+ <el-tooltip :content="row.instanceTitle" placement="top">
|
|
|
+ <span class="meta-value ellipsis">{{ row.instanceTitle }}</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <div v-if="row.instanceNo" class="task-meta">
|
|
|
+ <span class="meta-label">实例编号</span>
|
|
|
+ <span class="meta-value">{{ row.instanceNo }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 悬浮快捷操作 -->
|
|
|
+ <div class="task-card-actions">
|
|
|
+ <el-tooltip content="通过" placement="top">
|
|
|
+ <el-button circle type="success" size="small" :icon="Check" @click.stop="openQuickApprove(row, 'pass')" />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="拒绝" placement="top">
|
|
|
+ <el-button circle type="danger" size="small" :icon="Close" @click.stop="openQuickApprove(row, 'reject')" />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="转办" placement="top">
|
|
|
+ <el-button circle type="warning" size="small" :icon="Switch" @click.stop="openTransfer(row)" />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="加签" placement="top">
|
|
|
+ <el-button circle type="info" size="small" :icon="Plus" @click.stop="handleAddSign(row)" />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="详情" placement="top">
|
|
|
+ <el-button circle type="primary" size="small" :icon="View" @click.stop="handleDetail(row)" />
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
|
|
|
<el-pagination
|
|
|
v-model:current-page="queryParams.pageNum"
|
|
|
@@ -37,46 +115,75 @@
|
|
|
:page-sizes="[10, 20, 50]"
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
class="pagination"
|
|
|
- @change="loadData"
|
|
|
+ @current-change="loadData"
|
|
|
+ @size-change="loadData"
|
|
|
/>
|
|
|
</el-card>
|
|
|
|
|
|
- <!-- 审批对话框 -->
|
|
|
- <el-dialog v-model="dialogVisible" title="审批处理" width="500px">
|
|
|
- <el-form ref="formRef" :model="form" label-width="80px">
|
|
|
- <el-form-item label="审批操作" prop="action">
|
|
|
- <el-radio-group v-model="form.action">
|
|
|
- <el-radio label="pass">通过</el-radio>
|
|
|
- <el-radio label="reject">拒绝</el-radio>
|
|
|
- <el-radio label="rollback">回退</el-radio>
|
|
|
- <el-radio label="transfer">转办</el-radio>
|
|
|
- </el-radio-group>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="转办人" v-if="form.action === 'transfer'">
|
|
|
- <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 label="审批意见">
|
|
|
- <el-input v-model="form.comment" type="textarea" placeholder="请输入审批意见" />
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="submitApprove">确认</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
+ <!-- 批量/快速审批抽屉 -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="approveDrawerVisible"
|
|
|
+ :title="drawerTitle"
|
|
|
+ direction="rtl"
|
|
|
+ size="420px"
|
|
|
+ :destroy-on-close="true"
|
|
|
+ @closed="resetApproveForm"
|
|
|
+ >
|
|
|
+ <div class="drawer-content">
|
|
|
+ <el-alert
|
|
|
+ v-if="batchMode && currentTasks.length > 0"
|
|
|
+ :title="`将对 ${currentTasks.length} 个待办任务执行「${drawerActionText}」操作`"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ class="batch-alert"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-else-if="!batchMode && currentTask"
|
|
|
+ :title="`流程:${currentTask.definitionName} · 节点:${currentTask.nodeName}`"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ class="batch-alert"
|
|
|
+ />
|
|
|
+ <el-form :model="form" label-width="80px">
|
|
|
+ <el-form-item v-if="!batchMode && form.action === 'transfer'" label="转办人">
|
|
|
+ <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="附件">
|
|
|
+ <div v-if="!batchMode && previousAttachments.length > 0" class="previous-attachments">
|
|
|
+ <div class="previous-title">历史附件</div>
|
|
|
+ <div v-for="att in previousAttachments" :key="att.id" class="previous-item">
|
|
|
+ <el-link type="primary" size="small" @click="openPreview(att.fileUrl)">
|
|
|
+ {{ att.fileName }}
|
|
|
+ </el-link>
|
|
|
+ <span class="previous-meta">{{ att.nodeName || '发起' }} · {{ att.uploaderName || '未知' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-upload
|
|
|
+ v-model:file-list="attachmentList"
|
|
|
+ action="#"
|
|
|
+ :http-request="handleUpload"
|
|
|
+ :before-upload="beforeFileUpload"
|
|
|
+ :before-remove="() => true"
|
|
|
+ :on-preview="handleFilePreview"
|
|
|
+ multiple
|
|
|
+ >
|
|
|
+ <el-button type="primary" size="small">上传附件</el-button>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="审批意见">
|
|
|
+ <el-input v-model="form.comment" type="textarea" :rows="4" placeholder="请输入审批意见" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <el-button @click="approveDrawerVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitApprove">确认{{ drawerActionText }}</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
|
|
|
<!-- 加签对话框 -->
|
|
|
<el-dialog v-model="addSignVisible" title="任务加签" width="400px">
|
|
|
@@ -89,21 +196,35 @@
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="addSignVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="submitAddSign">确认</el-button>
|
|
|
+ <el-button type="primary" :loading="addSignLoading" @click="submitAddSign">确认</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
+ <!-- 流程详情抽屉 -->
|
|
|
+ <InstanceDetail
|
|
|
+ v-model="detailVisible"
|
|
|
+ :instance-id="currentInstanceId"
|
|
|
+ @approved="loadData"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 附件预览 -->
|
|
|
+ <FilePreview v-model="previewVisible" :url="previewUrl" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted } from 'vue'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import type { FormInstance } from 'element-plus'
|
|
|
-import { listTodo, approveTask, rejectTask, returnTask, transferTask, addSignTask } from '@/api/flow/task'
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { Check, Close, Switch, Plus, View } from '@element-plus/icons-vue'
|
|
|
+import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask } from '@/api/flow/task'
|
|
|
import { listUser } from '@/api/system/user'
|
|
|
import { uploadFile } from '@/api/file'
|
|
|
-import type { FlowTask, ApprovalAction } from '@/types/flow'
|
|
|
+import { getInstanceAttachments } from '@/api/flow/instance'
|
|
|
+import { beforeFileUpload, getUploadUrl, getFileName } from '@/utils/file'
|
|
|
+import type { FlowTask, ApprovalAction, Attachment } from '@/types/flow'
|
|
|
import type { User } from '@/types/system'
|
|
|
+import InstanceDetail from '@/views/flow/execute/InstanceDetail.vue'
|
|
|
+import FilePreview from '@/components/FilePreview/index.vue'
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const tableData = ref<FlowTask[]>([])
|
|
|
@@ -114,60 +235,195 @@ const queryParams = reactive({
|
|
|
processName: undefined as string | undefined
|
|
|
})
|
|
|
|
|
|
+const activeUrgency = ref<'all' | 'overdue' | 'warning' | 'normal'>('all')
|
|
|
+
|
|
|
+const urgencyText = (urgency?: number) => {
|
|
|
+ const map: Record<number, string> = { 0: '正常', 1: '即将超时', 2: '已超时' }
|
|
|
+ return map[urgency ?? 0] || '正常'
|
|
|
+}
|
|
|
+
|
|
|
+const urgencyType = (urgency?: number) => {
|
|
|
+ const map: Record<number, string> = { 0: 'info', 1: 'warning', 2: 'danger' }
|
|
|
+ return map[urgency ?? 0] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+function formatRemaining(row: FlowTask) {
|
|
|
+ if (row.urgency === 2) {
|
|
|
+ const hours = Math.abs(Math.ceil((row.remainingMinutes ?? 0) / 60))
|
|
|
+ return `已超时 ${hours} 小时`
|
|
|
+ }
|
|
|
+ const hours = Math.ceil((row.remainingMinutes ?? 0) / 60)
|
|
|
+ return `剩余 ${hours} 小时`
|
|
|
+}
|
|
|
+
|
|
|
+function handleUrgencyChange() {
|
|
|
+ queryParams.pageNum = 1
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+const filteredTableData = computed(() => {
|
|
|
+ if (activeUrgency.value === 'all') return tableData.value
|
|
|
+ const map: Record<string, number> = { overdue: 2, warning: 1, normal: 0 }
|
|
|
+ return tableData.value.filter(row => row.urgency === map[activeUrgency.value])
|
|
|
+})
|
|
|
+
|
|
|
const userList = ref<User[]>([])
|
|
|
|
|
|
-const dialogVisible = ref(false)
|
|
|
+// 选择相关
|
|
|
+const selectedIds = ref<number[]>([])
|
|
|
+
|
|
|
+function isSelected(id: number) {
|
|
|
+ return selectedIds.value.includes(id)
|
|
|
+}
|
|
|
+
|
|
|
+function toggleSelect(row: FlowTask) {
|
|
|
+ const idx = selectedIds.value.indexOf(row.id)
|
|
|
+ if (idx > -1) {
|
|
|
+ selectedIds.value.splice(idx, 1)
|
|
|
+ } else {
|
|
|
+ selectedIds.value.push(row.id)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSelectChange(val: boolean, row: FlowTask) {
|
|
|
+ if (val) {
|
|
|
+ if (!selectedIds.value.includes(row.id)) {
|
|
|
+ selectedIds.value.push(row.id)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const idx = selectedIds.value.indexOf(row.id)
|
|
|
+ if (idx > -1) selectedIds.value.splice(idx, 1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function clearSelection() {
|
|
|
+ selectedIds.value = []
|
|
|
+}
|
|
|
+
|
|
|
+// 抽屉审批相关
|
|
|
+const approveDrawerVisible = ref(false)
|
|
|
+const batchMode = ref(false)
|
|
|
const currentTask = ref<FlowTask | null>(null)
|
|
|
-const formRef = ref<FormInstance>()
|
|
|
-const form = reactive<ApprovalAction & { transferTo?: string | number }>({
|
|
|
+const currentTasks = ref<FlowTask[]>([])
|
|
|
+const submitting = ref(false)
|
|
|
+const form = reactive<ApprovalAction & { transferTo?: number }>({
|
|
|
action: 'pass',
|
|
|
comment: '',
|
|
|
transferTo: undefined
|
|
|
})
|
|
|
const attachmentList = ref<any[]>([])
|
|
|
+const previousAttachments = ref<Attachment[]>([])
|
|
|
+const previewVisible = ref(false)
|
|
|
+const previewUrl = ref('')
|
|
|
|
|
|
-const addSignVisible = ref(false)
|
|
|
-const addSignTaskId = ref<number>(0)
|
|
|
-const addSignUserId = ref<number | undefined>(undefined)
|
|
|
+const drawerActionText = computed(() => {
|
|
|
+ const map: Record<string, string> = { pass: '通过', reject: '拒绝', transfer: '转办' }
|
|
|
+ return map[form.action] || '审批'
|
|
|
+})
|
|
|
|
|
|
-async function loadData() {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const res = await listTodo(queryParams)
|
|
|
- tableData.value = res.list
|
|
|
- total.value = res.total
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
+const drawerTitle = computed(() => {
|
|
|
+ if (batchMode.value) return `批量${drawerActionText.value}`
|
|
|
+ return `${drawerActionText.value}审批`
|
|
|
+})
|
|
|
+
|
|
|
+function resetApproveForm() {
|
|
|
+ form.action = 'pass'
|
|
|
+ form.comment = ''
|
|
|
+ form.transferTo = undefined
|
|
|
+ attachmentList.value = []
|
|
|
+ previousAttachments.value = []
|
|
|
+ currentTask.value = null
|
|
|
+ currentTasks.value = []
|
|
|
+ batchMode.value = false
|
|
|
}
|
|
|
|
|
|
-async function loadUsers() {
|
|
|
- try {
|
|
|
- const res = await listUser({ pageNum: 1, pageSize: 9999 })
|
|
|
- userList.value = res.list
|
|
|
- } catch {
|
|
|
- // ignore
|
|
|
+function buildActionData(): ApprovalAction {
|
|
|
+ const data: ApprovalAction = {
|
|
|
+ action: form.action,
|
|
|
+ comment: form.comment
|
|
|
+ }
|
|
|
+ if (form.action === 'transfer' && form.transferTo) {
|
|
|
+ data.transferTo = Number(form.transferTo)
|
|
|
}
|
|
|
+ if (attachmentList.value.length > 0) {
|
|
|
+ data.attachmentUrls = JSON.stringify(attachmentList.value.map((f: any) => f.response || f.url).filter(Boolean))
|
|
|
+ }
|
|
|
+ return data
|
|
|
}
|
|
|
|
|
|
-function handleQuery() {
|
|
|
- queryParams.pageNum = 1
|
|
|
- loadData()
|
|
|
+async function openQuickApprove(row: FlowTask, action: 'pass' | 'reject') {
|
|
|
+ resetApproveForm()
|
|
|
+ currentTask.value = row
|
|
|
+ form.action = action
|
|
|
+ batchMode.value = false
|
|
|
+ await loadPreviousAttachments(row.instanceId)
|
|
|
+ approveDrawerVisible.value = true
|
|
|
}
|
|
|
|
|
|
-function resetQuery() {
|
|
|
- queryParams.processName = undefined
|
|
|
- queryParams.pageNum = 1
|
|
|
- loadData()
|
|
|
+function openBatchDrawer(action: 'pass' | 'reject') {
|
|
|
+ if (selectedIds.value.length === 0) {
|
|
|
+ ElMessage.warning('请先选择待办任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ resetApproveForm()
|
|
|
+ currentTasks.value = tableData.value.filter(r => selectedIds.value.includes(r.id))
|
|
|
+ form.action = action
|
|
|
+ batchMode.value = true
|
|
|
+ approveDrawerVisible.value = true
|
|
|
}
|
|
|
|
|
|
-function handleApprove(row: FlowTask) {
|
|
|
+async function openTransfer(row: FlowTask) {
|
|
|
+ resetApproveForm()
|
|
|
currentTask.value = row
|
|
|
- form.action = 'pass'
|
|
|
- form.comment = ''
|
|
|
- form.transferTo = undefined
|
|
|
- attachmentList.value = []
|
|
|
- dialogVisible.value = true
|
|
|
+ form.action = 'transfer'
|
|
|
+ batchMode.value = false
|
|
|
+ await loadPreviousAttachments(row.instanceId)
|
|
|
+ approveDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function submitApprove() {
|
|
|
+ if (batchMode.value) {
|
|
|
+ if (selectedIds.value.length === 0) return
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const data = buildActionData()
|
|
|
+ if (form.action === 'pass') {
|
|
|
+ await batchApproveTask(selectedIds.value, data)
|
|
|
+ } else if (form.action === 'reject') {
|
|
|
+ await batchRejectTask(selectedIds.value, data)
|
|
|
+ }
|
|
|
+ ElMessage.success('批量审批成功')
|
|
|
+ approveDrawerVisible.value = false
|
|
|
+ selectedIds.value = []
|
|
|
+ loadData()
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!currentTask.value) return
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const data = buildActionData()
|
|
|
+ const taskId = currentTask.value.id
|
|
|
+ switch (form.action) {
|
|
|
+ case 'pass':
|
|
|
+ await approveTask(taskId, data)
|
|
|
+ break
|
|
|
+ case 'reject':
|
|
|
+ await rejectTask(taskId, data)
|
|
|
+ break
|
|
|
+ case 'transfer':
|
|
|
+ await transferTask(taskId, data)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ ElMessage.success('审批成功')
|
|
|
+ approveDrawerVisible.value = false
|
|
|
+ loadData()
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async function handleUpload(options: any) {
|
|
|
@@ -179,6 +435,48 @@ async function handleUpload(options: any) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+async function loadPreviousAttachments(instanceId?: number) {
|
|
|
+ if (!instanceId) {
|
|
|
+ previousAttachments.value = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ previousAttachments.value = await getInstanceAttachments(instanceId)
|
|
|
+ } catch {
|
|
|
+ previousAttachments.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openPreview(url: string) {
|
|
|
+ if (!url) return
|
|
|
+ previewUrl.value = url
|
|
|
+ previewVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleFilePreview(file: any) {
|
|
|
+ const url = getUploadUrl(file)
|
|
|
+ if (url) openPreview(url)
|
|
|
+}
|
|
|
+
|
|
|
+// 详情抽屉
|
|
|
+const detailVisible = ref(false)
|
|
|
+const currentInstanceId = ref(0)
|
|
|
+
|
|
|
+function handleDetail(row: FlowTask) {
|
|
|
+ if (!row.instanceId) {
|
|
|
+ ElMessage.warning('该任务没有关联流程实例')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ currentInstanceId.value = row.instanceId
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 加签
|
|
|
+const addSignVisible = ref(false)
|
|
|
+const addSignTaskId = ref<number>(0)
|
|
|
+const addSignUserId = ref<number | undefined>(undefined)
|
|
|
+const addSignLoading = ref(false)
|
|
|
+
|
|
|
function handleAddSign(row: FlowTask) {
|
|
|
addSignTaskId.value = row.id
|
|
|
addSignUserId.value = undefined
|
|
|
@@ -190,41 +488,51 @@ async function submitAddSign() {
|
|
|
ElMessage.warning('请选择加签人')
|
|
|
return
|
|
|
}
|
|
|
- await addSignTask(addSignTaskId.value, addSignUserId.value)
|
|
|
- ElMessage.success('加签成功')
|
|
|
- addSignVisible.value = false
|
|
|
- loadData()
|
|
|
+ addSignLoading.value = true
|
|
|
+ try {
|
|
|
+ await addSignTask(addSignTaskId.value, addSignUserId.value)
|
|
|
+ ElMessage.success('加签成功')
|
|
|
+ addSignVisible.value = false
|
|
|
+ addSignUserId.value = undefined
|
|
|
+ loadData()
|
|
|
+ } finally {
|
|
|
+ addSignLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-async function submitApprove() {
|
|
|
- if (!currentTask.value) return
|
|
|
- const data: ApprovalAction = {
|
|
|
- action: form.action,
|
|
|
- comment: form.comment
|
|
|
- }
|
|
|
- if (form.action === 'transfer' && form.transferTo) {
|
|
|
- data.transferTo = Number(form.transferTo)
|
|
|
+async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await listTodo(queryParams)
|
|
|
+ tableData.value = res.list
|
|
|
+ total.value = res.total
|
|
|
+ // 清除已选择中不在当前页的数据
|
|
|
+ const pageIds = new Set(res.list.map(r => r.id))
|
|
|
+ selectedIds.value = selectedIds.value.filter(id => pageIds.has(id))
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
}
|
|
|
- if (attachmentList.value.length > 0) {
|
|
|
- data.attachmentUrls = JSON.stringify(attachmentList.value.map((f: any) => f.response || f.url).filter(Boolean))
|
|
|
+}
|
|
|
+
|
|
|
+async function loadUsers() {
|
|
|
+ try {
|
|
|
+ // 静默加载用户列表,避免普通用户进入待办页时被提示“权限不足”
|
|
|
+ const res = await listUser({ pageNum: 1, pageSize: 9999 }, { silent: true })
|
|
|
+ userList.value = res.list
|
|
|
+ } catch {
|
|
|
+ userList.value = []
|
|
|
}
|
|
|
- const taskId = currentTask.value.id
|
|
|
- switch (form.action) {
|
|
|
- case 'pass':
|
|
|
- await approveTask(taskId, data)
|
|
|
- break
|
|
|
- case 'reject':
|
|
|
- await rejectTask(taskId, data)
|
|
|
- break
|
|
|
- case 'rollback':
|
|
|
- await returnTask(taskId, data)
|
|
|
- break
|
|
|
- case 'transfer':
|
|
|
- await transferTask(taskId, data)
|
|
|
- break
|
|
|
- }
|
|
|
- ElMessage.success('审批成功')
|
|
|
- dialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+function handleQuery() {
|
|
|
+ queryParams.pageNum = 1
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+function resetQuery() {
|
|
|
+ queryParams.processName = undefined
|
|
|
+ activeUrgency.value = 'all'
|
|
|
+ queryParams.pageNum = 1
|
|
|
loadData()
|
|
|
}
|
|
|
|
|
|
@@ -239,12 +547,154 @@ onMounted(() => {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.search-form {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+.urgency-tabs {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.text-danger {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+.text-warning {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+.batch-toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.batch-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.batch-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.task-card-list {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+.task-card {
|
|
|
+ position: relative;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.task-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+.task-card.is-selected {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #f0f9ff;
|
|
|
+}
|
|
|
+.task-card-main {
|
|
|
+ padding-right: 48px;
|
|
|
+}
|
|
|
+.task-card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.task-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.task-card-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.task-meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.meta-label {
|
|
|
+ color: #909399;
|
|
|
+ width: 70px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.meta-value {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.task-card-actions {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ opacity: 0.3;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
+}
|
|
|
+.task-card:hover .task-card-actions,
|
|
|
+.task-card.is-selected .task-card-actions {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+.task-card-actions .el-button {
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
.pagination {
|
|
|
margin-top: 20px;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
-.search-form {
|
|
|
- margin-bottom: 20px;
|
|
|
+.drawer-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.batch-alert {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.drawer-footer {
|
|
|
+ margin-top: auto;
|
|
|
+ padding-top: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.previous-attachments {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.previous-title {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+.previous-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+.previous-meta {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
}
|
|
|
</style>
|