first commit
This commit is contained in:
520
src/views/system/approval/index.vue
Normal file
520
src/views/system/approval/index.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="approval-page">
|
||||
<a-page-header title="审批流程管理" sub-title="配置和管理系统审批流程模板">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showCreateDrawer">
|
||||
<PlusOutlined /> 新建流程
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-1">
|
||||
<div class="stat-icon"><ApartmentOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.totalTemplates }}</div>
|
||||
<div class="stat-title">流程模板</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-2">
|
||||
<div class="stat-icon"><CheckCircleOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.enabledTemplates }}</div>
|
||||
<div class="stat-title">已启用</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-3">
|
||||
<div class="stat-icon"><FileTextOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.totalInstances }}</div>
|
||||
<div class="stat-title">审批实例</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-4">
|
||||
<div class="stat-icon"><ClockCircleOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.inProgressInstances }}</div>
|
||||
<div class="stat-title">审批中</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-5">
|
||||
<div class="stat-icon"><LikeOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.approvedInstances }}</div>
|
||||
<div class="stat-title">已通过</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="stat-card stat-6">
|
||||
<div class="stat-icon"><DislikeOutlined /></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.rejectedInstances }}</div>
|
||||
<div class="stat-title">已拒绝</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 流程模板列表 -->
|
||||
<a-card class="main-card" title="流程模板" :loading="loading">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="filterScenario"
|
||||
placeholder="适用场景"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
@change="loadTemplates"
|
||||
>
|
||||
<a-select-option v-for="(label, key) in ApprovalScenarioMap" :key="key" :value="key">
|
||||
{{ label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="templateList"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="template-name">
|
||||
<ApartmentOutlined class="template-icon" />
|
||||
<div>
|
||||
<div class="name">{{ record.name }}</div>
|
||||
<div class="desc">{{ record.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'scenario'">
|
||||
<a-tag :color="getScenarioColor(record.scenario)">
|
||||
{{ ApprovalScenarioMap[record.scenario as ApprovalScenario] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'nodes'">
|
||||
<div class="nodes-preview">
|
||||
<a-tooltip v-for="(node, index) in record.nodes" :key="node.id" :title="node.name">
|
||||
<div class="node-item">
|
||||
<span class="node-num">{{ (index as number) + 1 }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
<RightOutlined v-if="(index as number) < record.nodes.length - 1" class="node-arrow" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<a-switch
|
||||
:checked="record.enabled"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="handleToggle(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'updatedAt'">
|
||||
{{ formatDateTime(record.updatedAt) }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="showEditDrawer(record as ApprovalTemplate)">
|
||||
<EditOutlined /> 编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此流程模板?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined /> 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 流程设计抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="editingTemplate ? '编辑流程' : '新建流程'"
|
||||
width="100%"
|
||||
:body-style="{ padding: 0 }"
|
||||
:header-style="{ padding: '12px 24px' }"
|
||||
placement="right"
|
||||
destroyOnClose
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="drawerVisible = false">取消</a-button>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||
保存流程
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="drawer-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-section">
|
||||
<a-form ref="formRef" :model="formData" layout="inline" class="basic-form">
|
||||
<a-form-item label="流程名称" name="name" :rules="[{ required: true, message: '请输入' }]">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入流程名称" style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="适用场景" name="scenario" :rules="[{ required: true, message: '请选择' }]">
|
||||
<a-select v-model:value="formData.scenario" placeholder="请选择" style="width: 140px">
|
||||
<a-select-option v-for="(label, key) in ApprovalScenarioMap" :key="key" :value="key">
|
||||
{{ label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="流程描述">
|
||||
<a-input v-model:value="formData.description" placeholder="请输入描述" style="width: 300px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch v-model:checked="formData.enabled" checked-children="启用" un-checked-children="禁用" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 可视化流程设计器 -->
|
||||
<div class="flow-section">
|
||||
<FlowEditor ref="flowEditorRef" v-model="formData.nodes" />
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ApartmentOutlined,
|
||||
CheckCircleOutlined,
|
||||
FileTextOutlined,
|
||||
ClockCircleOutlined,
|
||||
LikeOutlined,
|
||||
DislikeOutlined,
|
||||
RightOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ApprovalTemplate, ApprovalScenario, ApprovalNode } from '@/types'
|
||||
import { ApprovalScenarioMap } from '@/types'
|
||||
import {
|
||||
mockGetApprovalStats,
|
||||
mockGetApprovalTemplateList,
|
||||
mockToggleApprovalTemplate,
|
||||
mockDeleteApprovalTemplate
|
||||
} from '@/mock'
|
||||
import { formatDateTime } from '@/utils/common'
|
||||
import FlowEditor from '@/components/FlowEditor/index.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const templateList = ref<ApprovalTemplate[]>([])
|
||||
const filterScenario = ref<ApprovalScenario | undefined>()
|
||||
const flowEditorRef = ref<InstanceType<typeof FlowEditor>>()
|
||||
|
||||
const stats = reactive({
|
||||
totalTemplates: 0,
|
||||
enabledTemplates: 0,
|
||||
totalInstances: 0,
|
||||
pendingInstances: 0,
|
||||
inProgressInstances: 0,
|
||||
approvedInstances: 0,
|
||||
rejectedInstances: 0
|
||||
})
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const editingTemplate = ref<ApprovalTemplate | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<{
|
||||
name: string
|
||||
scenario: ApprovalScenario | undefined
|
||||
description: string
|
||||
nodes: ApprovalNode[]
|
||||
enabled: boolean
|
||||
}>({
|
||||
name: '',
|
||||
scenario: undefined,
|
||||
description: '',
|
||||
nodes: [],
|
||||
enabled: true
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '流程名称', key: 'name', width: 280 },
|
||||
{ title: '适用场景', key: 'scenario', width: 120 },
|
||||
{ title: '审批节点', key: 'nodes', width: 350 },
|
||||
{ title: '状态', key: 'enabled', width: 100 },
|
||||
{ title: '更新时间', key: 'updatedAt', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 140 }
|
||||
]
|
||||
|
||||
function getScenarioColor(scenario: ApprovalScenario): string {
|
||||
const colors: Record<ApprovalScenario, string> = {
|
||||
project_publish: '#1890ff',
|
||||
withdrawal: '#52c41a',
|
||||
contract: '#722ed1',
|
||||
certification: '#faad14',
|
||||
content: '#13c2c2',
|
||||
expense_reimbursement: '#eb2f96',
|
||||
payment_request: '#fa541c',
|
||||
purchase_request: '#fa8c16',
|
||||
budget_adjustment: '#2f54eb',
|
||||
invoice_apply: '#722ed1'
|
||||
}
|
||||
return colors[scenario]
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await mockGetApprovalStats()
|
||||
Object.assign(stats, data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mockGetApprovalTemplateList({
|
||||
scenario: filterScenario.value
|
||||
})
|
||||
templateList.value = res.list
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateDrawer() {
|
||||
editingTemplate.value = null
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
scenario: undefined,
|
||||
description: '',
|
||||
nodes: [],
|
||||
enabled: true
|
||||
})
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
function showEditDrawer(template: ApprovalTemplate) {
|
||||
editingTemplate.value = template
|
||||
Object.assign(formData, {
|
||||
name: template.name,
|
||||
scenario: template.scenario,
|
||||
description: template.description || '',
|
||||
nodes: template.nodes.map(n => ({ ...n })),
|
||||
enabled: template.enabled
|
||||
})
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
submitting.value = true
|
||||
|
||||
// 从流程编辑器获取节点数据
|
||||
if (flowEditorRef.value) {
|
||||
formData.nodes = flowEditorRef.value.toApprovalNodes()
|
||||
}
|
||||
|
||||
// 这里应该调用创建/更新API
|
||||
message.success(editingTemplate.value ? '更新成功' : '创建成功')
|
||||
drawerVisible.value = false
|
||||
loadTemplates()
|
||||
} catch {
|
||||
// 验证失败
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(id: number) {
|
||||
try {
|
||||
await mockToggleApprovalTemplate(id)
|
||||
message.success('状态已更新')
|
||||
loadTemplates()
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await mockDeleteApprovalTemplate(id)
|
||||
message.success('删除成功')
|
||||
loadTemplates()
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.stats-row { margin-bottom: 0; }
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
|
||||
|
||||
.stat-1 { background: linear-gradient(135deg, #667eea, #764ba2); }
|
||||
.stat-2 { background: linear-gradient(135deg, #11998e, #38ef7d); }
|
||||
.stat-3 { background: linear-gradient(135deg, #4facfe, #00f2fe); }
|
||||
.stat-4 { background: linear-gradient(135deg, #fa709a, #fee140); }
|
||||
.stat-5 { background: linear-gradient(135deg, #a8edea, #fed6e3); color: #333; }
|
||||
.stat-6 { background: linear-gradient(135deg, #ff416c, #ff4b2b); }
|
||||
|
||||
.stat-icon { font-size: 28px; opacity: 0.9; }
|
||||
.stat-content { flex: 1; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; line-height: 1.2; }
|
||||
.stat-title { font-size: 13px; opacity: 0.85; margin-top: 2px; }
|
||||
|
||||
.main-card { border-radius: 12px; }
|
||||
|
||||
.template-name {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
font-size: 24px;
|
||||
color: #1890ff;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.template-name .name {
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.template-name .desc {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.nodes-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.node-num {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.node-arrow {
|
||||
font-size: 10px;
|
||||
color: #bfbfbf;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
/* 抽屉内容 */
|
||||
.drawer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.basic-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.basic-form :deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.flow-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flow-section :deep(.flow-editor) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
413
src/views/system/approval/instances.vue
Normal file
413
src/views/system/approval/instances.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<div class="approval-instances-page">
|
||||
<a-page-header title="审批记录" sub-title="查看所有审批实例及其处理状态" />
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<a-card class="filter-card">
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索业务标题 / 发起人"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-select
|
||||
v-model:value="filterScenario"
|
||||
placeholder="业务场景"
|
||||
allow-clear
|
||||
style="width: 130px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<a-select-option v-for="(label, key) in ApprovalScenarioMap" :key="key" :value="key">
|
||||
{{ label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-select
|
||||
v-model:value="filterStatus"
|
||||
placeholder="审批状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<a-select-option v-for="(label, key) in ApprovalInstanceStatusMap" :key="key" :value="key">
|
||||
{{ label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
style="width: 240px"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<a-card class="main-card" :loading="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="instanceList"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'business'">
|
||||
<div class="business-cell">
|
||||
<a-tag :color="getScenarioColor(record.scenario)" size="small">
|
||||
{{ ApprovalScenarioMap[record.scenario as ApprovalScenario] }}
|
||||
</a-tag>
|
||||
<span class="business-title">{{ record.businessTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'initiator'">
|
||||
<div class="user-cell">
|
||||
<a-avatar :src="record.initiatorAvatar" :size="28" />
|
||||
<span>{{ record.initiatorName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'currentNode'">
|
||||
<span v-if="record.currentNodeName" class="current-node">
|
||||
<ClockCircleOutlined /> {{ record.currentNodeName }}
|
||||
</span>
|
||||
<span v-else class="no-node">-</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="ApprovalInstanceStatusBadgeMap[record.status as ApprovalInstanceStatus]"
|
||||
:text="ApprovalInstanceStatusMap[record.status as ApprovalInstanceStatus]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'submittedAt'">
|
||||
{{ formatDateTime(record.submittedAt) }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="showDetail(record as ApprovalInstance)">
|
||||
<EyeOutlined /> 详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="detailVisible"
|
||||
title="审批详情"
|
||||
width="640"
|
||||
destroy-on-close
|
||||
>
|
||||
<template v-if="currentInstance">
|
||||
<!-- 业务信息 -->
|
||||
<a-descriptions :column="2" bordered size="small" title="业务信息">
|
||||
<a-descriptions-item label="业务类型">
|
||||
<a-tag :color="getScenarioColor(currentInstance.scenario)">
|
||||
{{ ApprovalScenarioMap[currentInstance.scenario] }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审批状态">
|
||||
<a-badge
|
||||
:status="ApprovalInstanceStatusBadgeMap[currentInstance.status]"
|
||||
:text="ApprovalInstanceStatusMap[currentInstance.status]"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="业务标题" :span="2">
|
||||
{{ currentInstance.businessTitle }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用流程">
|
||||
{{ currentInstance.templateName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前节点">
|
||||
{{ currentInstance.currentNodeName || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 发起人 -->
|
||||
<a-divider>发起人</a-divider>
|
||||
<div class="initiator-info">
|
||||
<a-avatar :src="currentInstance.initiatorAvatar" :size="48" />
|
||||
<div class="info">
|
||||
<div class="name">{{ currentInstance.initiatorName }}</div>
|
||||
<div class="time">提交于 {{ formatDateTime(currentInstance.submittedAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批记录 -->
|
||||
<a-divider>审批记录</a-divider>
|
||||
<a-timeline v-if="currentInstance.records.length">
|
||||
<a-timeline-item
|
||||
v-for="(record, index) in currentInstance.records"
|
||||
:key="index"
|
||||
:color="getActionColor(record.action)"
|
||||
>
|
||||
<div class="record-item">
|
||||
<div class="record-header">
|
||||
<a-avatar :src="record.approverAvatar" :size="32" />
|
||||
<div class="record-info">
|
||||
<div class="approver">
|
||||
{{ record.approverName }}
|
||||
<a-tag :color="getActionColor(record.action)" size="small">
|
||||
{{ getActionText(record.action) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="node-name">{{ record.nodeName }}</div>
|
||||
</div>
|
||||
<div class="record-time">{{ formatDateTime(record.operatedAt) }}</div>
|
||||
</div>
|
||||
<div v-if="record.comment" class="record-comment">
|
||||
<MessageOutlined /> {{ record.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty v-else description="暂无审批记录" :image-style="{ height: '40px' }" />
|
||||
</template>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
ClockCircleOutlined,
|
||||
MessageOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
|
||||
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
|
||||
import { mockGetApprovalInstanceList } from '@/mock'
|
||||
import { formatDateTime } from '@/utils/common'
|
||||
|
||||
const loading = ref(false)
|
||||
const instanceList = ref<ApprovalInstance[]>([])
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const filterScenario = ref<ApprovalScenario | undefined>()
|
||||
const filterStatus = ref<ApprovalInstanceStatus | undefined>()
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>()
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const currentInstance = ref<ApprovalInstance | null>(null)
|
||||
|
||||
const columns = [
|
||||
{ title: '业务信息', key: 'business', width: 280 },
|
||||
{ title: '发起人', key: 'initiator', width: 140 },
|
||||
{ title: '当前节点', key: 'currentNode', width: 130 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '提交时间', key: 'submittedAt', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 100 }
|
||||
]
|
||||
|
||||
function getScenarioColor(scenario: ApprovalScenario): string {
|
||||
const colors: Record<ApprovalScenario, string> = {
|
||||
project_publish: '#1890ff',
|
||||
withdrawal: '#52c41a',
|
||||
contract: '#722ed1',
|
||||
certification: '#faad14',
|
||||
content: '#13c2c2',
|
||||
expense_reimbursement: '#eb2f96',
|
||||
payment_request: '#fa541c',
|
||||
purchase_request: '#fa8c16',
|
||||
budget_adjustment: '#2f54eb',
|
||||
invoice_apply: '#722ed1'
|
||||
}
|
||||
return colors[scenario]
|
||||
}
|
||||
|
||||
function getActionColor(action: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
approve: 'green',
|
||||
reject: 'red',
|
||||
transfer: 'blue',
|
||||
return: 'orange'
|
||||
}
|
||||
return colors[action] || 'gray'
|
||||
}
|
||||
|
||||
function getActionText(action: string): string {
|
||||
const texts: Record<string, string> = {
|
||||
approve: '通过',
|
||||
reject: '拒绝',
|
||||
transfer: '转交',
|
||||
return: '退回'
|
||||
}
|
||||
return texts[action] || action
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mockGetApprovalInstanceList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
scenario: filterScenario.value,
|
||||
status: filterStatus.value,
|
||||
startDate: dateRange.value?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value?.[1]?.format('YYYY-MM-DD')
|
||||
})
|
||||
instanceList.value = res.list
|
||||
pagination.total = res.total
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchKeyword.value = ''
|
||||
filterScenario.value = undefined
|
||||
filterStatus.value = undefined
|
||||
dateRange.value = undefined
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: { current?: number; pageSize?: number }) {
|
||||
pagination.current = pag.current || 1
|
||||
pagination.pageSize = pag.pageSize || 10
|
||||
loadData()
|
||||
}
|
||||
|
||||
function showDetail(instance: ApprovalInstance) {
|
||||
currentInstance.value = instance
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-instances-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.filter-card, .main-card { border-radius: 12px; }
|
||||
|
||||
.filter-card :deep(.ant-card-body) { padding: 16px; }
|
||||
|
||||
.search-form { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.search-form :deep(.ant-form-item) { margin-bottom: 0; margin-right: 0; }
|
||||
|
||||
.business-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.business-title {
|
||||
font-weight: 500;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.current-node {
|
||||
color: #faad14;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-node {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 详情样式 */
|
||||
.initiator-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.initiator-info .info { flex: 1; }
|
||||
.initiator-info .name { font-size: 15px; font-weight: 600; }
|
||||
.initiator-info .time { font-size: 12px; color: #8c8c8c; margin-top: 2px; }
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-info { flex: 1; }
|
||||
.record-info .approver {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.record-info .node-name { font-size: 12px; color: #8c8c8c; margin-top: 2px; }
|
||||
.record-time { font-size: 12px; color: #8c8c8c; }
|
||||
|
||||
.record-comment {
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-left: 44px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user