566 lines
14 KiB
Vue
566 lines
14 KiB
Vue
<!-- 已办任务 -->
|
|
<template>
|
|
<div class="page-container">
|
|
<header class="page-head">
|
|
<h1>已办任务</h1>
|
|
<p class="page-desc">查看已处理的审批任务及审批结果。</p>
|
|
</header>
|
|
|
|
<section class="panel">
|
|
<div class="panel-top">
|
|
<h2>已办列表</h2>
|
|
</div>
|
|
|
|
<div class="filter-block">
|
|
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
|
<div class="filter-grid">
|
|
<el-form-item label="关键词">
|
|
<el-input
|
|
v-model="searchForm.keyword"
|
|
placeholder="流程名称"
|
|
clearable
|
|
@keyup.enter="handleSearch"
|
|
/>
|
|
</el-form-item>
|
|
</div>
|
|
<div class="filter-actions-row">
|
|
<span class="filter-hint">按流程名称模糊查询。</span>
|
|
<div class="filter-actions-end">
|
|
<el-button class="btn-ghost" @click="handleReset"
|
|
><el-icon><RefreshLeft /></el-icon>重置</el-button
|
|
>
|
|
<el-button class="btn-main" type="primary" @click="handleSearch"
|
|
><el-icon><Search /></el-icon>搜索</el-button
|
|
>
|
|
</div>
|
|
</div>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="list-body list-wrap">
|
|
<el-table
|
|
class="list-table"
|
|
:data="tableData"
|
|
v-loading="loading"
|
|
stripe
|
|
row-key="id"
|
|
@row-dblclick="handleViewDetail"
|
|
>
|
|
<el-table-column
|
|
type="index"
|
|
label="序号"
|
|
min-width="68"
|
|
align="center"
|
|
header-align="center"
|
|
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
|
/>
|
|
<el-table-column
|
|
prop="processName"
|
|
label="流程名称"
|
|
min-width="160"
|
|
align="center"
|
|
header-align="center"
|
|
/>
|
|
<el-table-column
|
|
prop="businessTypeLabel"
|
|
label="业务类型"
|
|
min-width="120"
|
|
align="center"
|
|
header-align="center"
|
|
/>
|
|
<el-table-column
|
|
prop="businessId"
|
|
label="业务单号"
|
|
min-width="200"
|
|
align="center"
|
|
header-align="center"
|
|
show-overflow-tooltip
|
|
/>
|
|
<el-table-column
|
|
prop="starterUserName"
|
|
label="发起人"
|
|
min-width="100"
|
|
align="center"
|
|
header-align="center"
|
|
/>
|
|
<el-table-column label="审批结果" min-width="100" align="center" header-align="center">
|
|
<template #default="{ row }">
|
|
<ProcessStatusTag :status="row.status || ''" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column
|
|
prop="commentText"
|
|
label="审批意见"
|
|
min-width="150"
|
|
align="center"
|
|
header-align="center"
|
|
show-overflow-tooltip
|
|
/>
|
|
<el-table-column
|
|
prop="updateTime"
|
|
label="审批时间"
|
|
min-width="180"
|
|
align="center"
|
|
header-align="center"
|
|
/>
|
|
<el-table-column
|
|
label="操作"
|
|
min-width="260"
|
|
fixed="right"
|
|
align="center"
|
|
header-align="center"
|
|
>
|
|
<template #default="{ row }">
|
|
<el-button class="btn-op-item primary" @click="handleViewDetail(row)"
|
|
><el-icon><View /></el-icon>查看详情</el-button
|
|
>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div class="pagination list-pager-bar">
|
|
<el-pagination
|
|
:current-page="pagination.page"
|
|
:page-size="pagination.pageSize"
|
|
:total="pagination.total"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next"
|
|
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
|
@current-change="(val) => ((pagination.page = val), loadData())"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 详情弹窗 -->
|
|
<el-dialog
|
|
v-model="detailVisible"
|
|
width="720px"
|
|
class="workflow-module-dialog"
|
|
align-center
|
|
destroy-on-close
|
|
:show-close="false"
|
|
style="backdrop-filter: none"
|
|
>
|
|
<div class="dialog-scroll" v-loading="detailLoading">
|
|
<template v-if="detailData">
|
|
<div class="view-hero">
|
|
<div class="view-hero-icon">
|
|
<el-icon :size="28"><Document /></el-icon>
|
|
</div>
|
|
<div class="view-hero-main">
|
|
<h2>{{ detailData.processName || '流程详情' }}</h2>
|
|
<div class="view-hero-tags">
|
|
<ProcessStatusTag :status="detailData.status || ''" />
|
|
<el-tag size="small" type="info">{{
|
|
detailData.businessTypeLabel || detailData.businessType || '--'
|
|
}}</el-tag>
|
|
</div>
|
|
<p class="view-hero-meta">
|
|
发起人:{{ detailData.starterUserName || '--' }}
|
|
<span class="dot">·</span>
|
|
业务单号:{{ detailData.businessId || '--' }}
|
|
<span class="dot">·</span>
|
|
{{ detailData.createTime || '--' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="detailData.businessFormFields && detailData.businessFormFields.length"
|
|
class="view-section"
|
|
>
|
|
<h3>申请信息</h3>
|
|
<dl class="view-kv-grid">
|
|
<div
|
|
v-for="field in detailData.businessFormFields"
|
|
:key="field.label"
|
|
class="view-kv"
|
|
>
|
|
<dt>{{ field.label }}</dt>
|
|
<dd>{{ field.value || '--' }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div v-if="detailData.tasks && detailData.tasks.length" class="view-section">
|
|
<h3>审批节点</h3>
|
|
<div class="htl-scroll">
|
|
<div class="htl">
|
|
<div
|
|
v-for="(task, idx) in detailData.tasks"
|
|
:key="idx"
|
|
class="htl-item"
|
|
:class="getTimelineStatusClass(task.status)"
|
|
>
|
|
<div class="htl-rail">
|
|
<span class="htl-node" />
|
|
<span v-if="idx < detailData.tasks.length - 1" class="htl-line" />
|
|
</div>
|
|
<div class="htl-body">
|
|
<div class="htl-name">{{ task.nodeName || '--' }}</div>
|
|
<div class="htl-user">{{ task.assigneeUserName || '--' }}</div>
|
|
<div class="htl-status">
|
|
<ProcessStatusTag :status="task.status || ''" />
|
|
</div>
|
|
<div v-if="task.updateTime" class="htl-time">{{ task.updateTime }}</div>
|
|
<div v-if="task.commentText" class="htl-comment">{{ task.commentText }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<template #footer>
|
|
<el-button class="btn-ghost" @click="detailVisible = false"
|
|
><el-icon><Close /></el-icon>关闭</el-button
|
|
>
|
|
</template>
|
|
</el-dialog>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { fetchCompletedTasks, fetchInstanceDetail } from '@/api/workflow'
|
|
import ProcessStatusTag from '../components/ProcessStatusTag.vue'
|
|
import { Document, View, Search, RefreshLeft, Close } from '@element-plus/icons-vue'
|
|
|
|
defineOptions({ name: 'WorkflowTaskCompleted' })
|
|
|
|
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
|
const tableData = ref<Api.Workflow.TaskItem[]>([])
|
|
const loading = ref(false)
|
|
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
|
|
|
// 详情
|
|
const detailVisible = ref(false)
|
|
const detailLoading = ref(false)
|
|
const detailData = ref<Api.Workflow.InstanceDetail | null>(null)
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const res = await fetchCompletedTasks({
|
|
page: pagination.page,
|
|
pageSize: pagination.pageSize,
|
|
keyword: searchForm.keyword || undefined
|
|
})
|
|
tableData.value = res.list || []
|
|
pagination.total = res.total ?? 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
pagination.page = 1
|
|
loadData()
|
|
}
|
|
|
|
function handleReset() {
|
|
searchForm.keyword = ''
|
|
pagination.page = 1
|
|
loadData()
|
|
}
|
|
|
|
async function handleViewDetail(row: Api.Workflow.TaskItem) {
|
|
detailVisible.value = true
|
|
detailLoading.value = true
|
|
detailData.value = null
|
|
try {
|
|
const data = await fetchInstanceDetail(row.instanceId)
|
|
detailData.value = {
|
|
...data,
|
|
processName: row.processName,
|
|
businessType: row.businessType,
|
|
businessTypeLabel: row.businessTypeLabel,
|
|
businessId: row.businessId,
|
|
starterUserId: row.starterUserId,
|
|
starterUserName: row.starterUserName
|
|
} as Api.Workflow.InstanceDetail & {
|
|
processName?: string
|
|
businessType?: string
|
|
businessTypeLabel?: string
|
|
businessId?: string
|
|
starterUserId?: string
|
|
starterUserName?: string
|
|
}
|
|
} catch {
|
|
// 错误已由全局处理
|
|
} finally {
|
|
detailLoading.value = false
|
|
}
|
|
}
|
|
|
|
function getTimelineStatusClass(status: string) {
|
|
if (['approved', 'completed'].includes(status)) return 'htl-done'
|
|
if (['running', 'pending'].includes(status)) return 'htl-active'
|
|
if (status === 'rejected') return 'htl-reject'
|
|
return 'htl-muted'
|
|
}
|
|
// 输入检索条件时自动搜索(防抖 300ms)
|
|
watchDebounced(
|
|
searchForm,
|
|
() => {
|
|
pagination.page = 1
|
|
loadData()
|
|
},
|
|
{ debounce: 300, deep: true }
|
|
)
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.dialog-scroll {
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 120px);
|
|
}
|
|
|
|
.view-hero {
|
|
display: flex;
|
|
gap: 16px;
|
|
padding: 20px 22px;
|
|
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 55%);
|
|
border-bottom: 1px solid #d9e8e2;
|
|
align-items: center;
|
|
}
|
|
|
|
.view-hero-icon {
|
|
width: 72px;
|
|
height: 72px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
background: rgba(16, 185, 129, 0.08);
|
|
color: #059669;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.view-hero-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
|
|
h2 {
|
|
margin: 0 0 8px;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
}
|
|
|
|
.view-hero-tags {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.view-hero-meta {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
line-height: 1.5;
|
|
|
|
.dot {
|
|
margin: 0 6px;
|
|
opacity: 0.7;
|
|
}
|
|
}
|
|
|
|
.view-section {
|
|
padding: 18px 22px;
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
h3 {
|
|
margin: 0 0 14px;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
color: #64748b;
|
|
}
|
|
}
|
|
|
|
.view-kv-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px 28px;
|
|
}
|
|
|
|
.view-kv {
|
|
display: grid;
|
|
grid-template-columns: 96px 1fr;
|
|
gap: 8px 12px;
|
|
font-size: 13px;
|
|
align-items: start;
|
|
|
|
dt {
|
|
margin: 0;
|
|
color: #64748b;
|
|
font-weight: 500;
|
|
}
|
|
|
|
dd {
|
|
margin: 0;
|
|
color: #1e293b;
|
|
word-break: break-word;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.view-kv-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.view-kv {
|
|
grid-template-columns: 1fr;
|
|
gap: 4px;
|
|
}
|
|
}
|
|
|
|
/* ── horizontal timeline ── */
|
|
.htl-scroll {
|
|
overflow-x: auto;
|
|
padding: 0 0 4px 12px;
|
|
|
|
&::-webkit-scrollbar {
|
|
height: 4px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
background: #d1d5db;
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
|
|
.htl {
|
|
display: inline-flex;
|
|
min-width: 100%;
|
|
}
|
|
|
|
.htl-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 140px;
|
|
flex: 1 0 140px;
|
|
}
|
|
|
|
/* rail: node circle + connecting line */
|
|
.htl-rail {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
height: 22px;
|
|
}
|
|
|
|
.htl-node {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
background: #10b981;
|
|
border: 2px solid #fff;
|
|
box-shadow: 0 0 0 1px #10b981;
|
|
}
|
|
|
|
.htl-line {
|
|
flex: 1;
|
|
height: 2px;
|
|
background: #d1d5db;
|
|
margin-left: 0;
|
|
}
|
|
|
|
.htl-body {
|
|
padding: 8px 4px 0 0;
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.htl-name {
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.htl-user {
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.htl-status {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.htl-time {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.htl-comment {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* status variants */
|
|
.htl-active .htl-node {
|
|
background: #fff;
|
|
border: 3px solid #10b981;
|
|
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
|
|
}
|
|
|
|
.htl-reject .htl-node {
|
|
background: #ef4444;
|
|
border-color: #fff;
|
|
box-shadow: 0 0 0 1px #ef4444;
|
|
}
|
|
|
|
.htl-muted .htl-node {
|
|
background: #d1d5db;
|
|
border-color: #fff;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.htl-done + .htl-active .htl-line,
|
|
.htl-done + .htl-done .htl-line {
|
|
background: #10b981;
|
|
}
|
|
|
|
.htl-active + .htl-done .htl-line {
|
|
background: #10b981;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.workflow-module-dialog {
|
|
.el-dialog__header {
|
|
display: none;
|
|
}
|
|
|
|
.el-dialog__body {
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.el-dialog__footer {
|
|
padding: 12px 22px;
|
|
border-top: 1px solid #d9e8e2;
|
|
background: #f8fafc;
|
|
}
|
|
}
|
|
</style>
|