feat: 新增发起流程页面,优化流程发起体验
- 新增 /workflow/start 页面,卡片展示所有已发布且启用的流程定义 - 点击卡片即可直接发起流程,无需手动填写编码和变量 - 流程定义页面增加"发起"按钮 - 流程实例页面移除旧的发起表单,改为跳转到发起页
This commit is contained in:
parent
49d8abf933
commit
d513c76cd6
@ -10,6 +10,15 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'Workflow',
|
||||
path: '/workflow',
|
||||
children: [
|
||||
{
|
||||
name: 'WorkflowStart',
|
||||
path: '/workflow/start',
|
||||
component: () => import('#/views/workflow/start/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:play-circle',
|
||||
title: '发起流程',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WorkflowDefinitions',
|
||||
path: '/workflow/definitions',
|
||||
@ -20,21 +29,12 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WorkflowPending',
|
||||
path: '/workflow/pending',
|
||||
component: () => import('#/views/workflow/pending/index.vue'),
|
||||
name: 'WorkflowTasks',
|
||||
path: '/workflow/tasks',
|
||||
component: () => import('#/views/workflow/tasks/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:clock',
|
||||
title: '我的待办',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WorkflowHistory',
|
||||
path: '/workflow/history',
|
||||
component: () => import('#/views/workflow/history/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:check-circle',
|
||||
title: '我的已办',
|
||||
title: '我的任务',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
disableWorkflowDefinitionApi,
|
||||
getWorkflowDefinitionsApi,
|
||||
publishWorkflowDefinitionApi,
|
||||
startWorkflowInstanceApi,
|
||||
updateWorkflowDefinitionApi,
|
||||
DefinitionStatus,
|
||||
type WorkflowDefinitionDto,
|
||||
@ -146,6 +147,18 @@ async function handleDisable(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStart(def: WorkflowDefinitionDto) {
|
||||
try {
|
||||
await startWorkflowInstanceApi({
|
||||
definitionCode: def.code,
|
||||
title: def.name,
|
||||
});
|
||||
message.success(`「${def.name}」流程已发起`);
|
||||
} catch {
|
||||
message.error('发起失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
try {
|
||||
await deleteWorkflowDefinitionApi(id);
|
||||
@ -177,6 +190,14 @@ onMounted(() => loadData());
|
||||
<template #bodyCell="{ column, record: record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<Space>
|
||||
<Button
|
||||
v-if="(record as WorkflowDefinitionDto).status === DefinitionStatus.Published && (record as WorkflowDefinitionDto).isEnabled"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleStart(record as WorkflowDefinitionDto)"
|
||||
>
|
||||
发起
|
||||
</Button>
|
||||
<Button v-if="(record as WorkflowDefinitionDto).status === DefinitionStatus.Draft" size="small" type="primary" @click="handlePublish((record as WorkflowDefinitionDto).id)">发布</Button>
|
||||
<Button v-if="(record as WorkflowDefinitionDto).status === DefinitionStatus.Published" size="small" @click="handleDisable((record as WorkflowDefinitionDto).id)">禁用</Button>
|
||||
<Button size="small" @click="openEdit(record as WorkflowDefinitionDto)">编辑</Button>
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { message, Table, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getHistoryTasksApi,
|
||||
TaskStatus,
|
||||
type WorkflowTaskListItemDto,
|
||||
} from '#/api/core';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.userInfo?.userId ?? '';
|
||||
|
||||
const loading = ref(false);
|
||||
const data = ref<WorkflowTaskListItemDto[]>([]);
|
||||
const total = ref(0);
|
||||
const pageIndex = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
const taskStatusMap: Record<number, { color: string; text: string }> = {
|
||||
[TaskStatus.Approved]: { color: 'green', text: '已通过' },
|
||||
[TaskStatus.Rejected]: { color: 'red', text: '已拒绝' },
|
||||
[TaskStatus.Transferred]: { color: 'blue', text: '已转办' },
|
||||
[TaskStatus.Delegated]: { color: 'purple', text: '已委派' },
|
||||
[TaskStatus.Pending]: { color: 'orange', text: '待处理' },
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: { record: WorkflowTaskListItemDto }) => {
|
||||
const s = taskStatusMap[record.status];
|
||||
return h(Tag, { color: s?.color }, () => s?.text ?? '未知');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'assigneeId',
|
||||
key: 'assigneeId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'completedAt',
|
||||
key: 'completedAt',
|
||||
width: 180,
|
||||
customRender: ({ text }: { text: string | null }) => text ? new Date(text).toLocaleString() : '-',
|
||||
},
|
||||
];
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getHistoryTasksApi({
|
||||
userId,
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
data.value = res.items ?? [];
|
||||
total.value = res.total ?? 0;
|
||||
} catch {
|
||||
message.error('加载已办任务失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTableChange(pagination: any) {
|
||||
pageIndex.value = pagination.current ?? 1;
|
||||
pageSize.value = pagination.pageSize ?? 20;
|
||||
loadData();
|
||||
}
|
||||
|
||||
onMounted(() => loadData());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold">我的已办</h3>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:pagination="{ current: pageIndex, pageSize, total, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` }"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,14 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
@ -20,12 +18,12 @@ import {
|
||||
getWorkflowInstancesApi,
|
||||
InstanceStatus,
|
||||
resumeWorkflowInstanceApi,
|
||||
startWorkflowInstanceApi,
|
||||
suspendWorkflowInstanceApi,
|
||||
withdrawWorkflowInstanceApi,
|
||||
type WorkflowInstanceDto,
|
||||
} from '#/api/core';
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const data = ref<WorkflowInstanceDto[]>([]);
|
||||
const total = ref(0);
|
||||
@ -33,9 +31,6 @@ const pageIndex = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const statusFilter = ref<InstanceStatus | undefined>(undefined);
|
||||
|
||||
const startVisible = ref(false);
|
||||
const startForm = ref({ definitionCode: '', title: '', variables: '' });
|
||||
|
||||
const instanceStatusMap: Record<number, { color: string; text: string }> = {
|
||||
[InstanceStatus.Pending]: { color: 'blue', text: '待启动' },
|
||||
[InstanceStatus.Running]: { color: 'green', text: '运行中' },
|
||||
@ -117,30 +112,6 @@ function onFilterChange() {
|
||||
loadData();
|
||||
}
|
||||
|
||||
function openStart() {
|
||||
startForm.value = { definitionCode: '', title: '', variables: '' };
|
||||
startVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleStart() {
|
||||
if (!startForm.value.definitionCode || !startForm.value.title) {
|
||||
message.warning('请填写流程编码和标题');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await startWorkflowInstanceApi({
|
||||
definitionCode: startForm.value.definitionCode,
|
||||
title: startForm.value.title,
|
||||
variables: startForm.value.variables || undefined,
|
||||
});
|
||||
message.success('流程已发起');
|
||||
startVisible.value = false;
|
||||
loadData();
|
||||
} catch {
|
||||
message.error('发起失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSuspend(id: string) {
|
||||
try {
|
||||
await suspendWorkflowInstanceApi(id);
|
||||
@ -186,7 +157,7 @@ onMounted(() => loadData());
|
||||
@change="onFilterChange"
|
||||
/>
|
||||
</Space>
|
||||
<Button type="primary" @click="openStart">发起流程</Button>
|
||||
<Button type="primary" @click="router.push('/workflow/start')">发起流程</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
@ -214,18 +185,5 @@ onMounted(() => loadData());
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<Modal v-model:open="startVisible" title="发起流程" @ok="handleStart">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="流程编码" required>
|
||||
<Input v-model:value="startForm.definitionCode" placeholder="如 leave-approval" />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程标题" required>
|
||||
<Input v-model:value="startForm.title" placeholder="如 张三请假申请" />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程变量(JSON)">
|
||||
<Input.TextArea v-model:value="startForm.variables" placeholder='{"days": 2}' :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
approveTaskApi,
|
||||
delegateTaskApi,
|
||||
getPendingTasksApi,
|
||||
rejectTaskApi,
|
||||
transferTaskApi,
|
||||
type WorkflowTaskListItemDto,
|
||||
} from '#/api/core';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => userStore.userInfo?.userId ?? '');
|
||||
import { computed } from 'vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const data = ref<WorkflowTaskListItemDto[]>([]);
|
||||
const total = ref(0);
|
||||
const pageIndex = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
const approveVisible = ref(false);
|
||||
const rejectVisible = ref(false);
|
||||
const delegateVisible = ref(false);
|
||||
const transferVisible = ref(false);
|
||||
const taskId = ref('');
|
||||
const commentText = ref('');
|
||||
const targetUserId = ref('');
|
||||
|
||||
const columns = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: () => h(Tag, { color: 'orange' }, () => '待处理'),
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'assigneeId',
|
||||
key: 'assigneeId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{ title: '操作', key: 'actions', width: 280 },
|
||||
];
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getPendingTasksApi({
|
||||
userId: userId.value,
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
data.value = res.items ?? [];
|
||||
total.value = res.total ?? 0;
|
||||
} catch {
|
||||
message.error('加载待办任务失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTableChange(pagination: any) {
|
||||
pageIndex.value = pagination.current ?? 1;
|
||||
pageSize.value = pagination.pageSize ?? 20;
|
||||
loadData();
|
||||
}
|
||||
|
||||
function openApprove(id: string) {
|
||||
taskId.value = id;
|
||||
commentText.value = '';
|
||||
approveVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
try {
|
||||
await approveTaskApi(taskId.value, userId.value, commentText.value || undefined);
|
||||
message.success('审批通过');
|
||||
approveVisible.value = false;
|
||||
loadData();
|
||||
} catch {
|
||||
message.error('审批失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(id: string) {
|
||||
taskId.value = id;
|
||||
commentText.value = '';
|
||||
rejectVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
try {
|
||||
await rejectTaskApi(taskId.value, userId.value, commentText.value || undefined);
|
||||
message.success('已拒绝');
|
||||
rejectVisible.value = false;
|
||||
loadData();
|
||||
} catch {
|
||||
message.error('拒绝失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openDelegate(id: string) {
|
||||
taskId.value = id;
|
||||
targetUserId.value = '';
|
||||
delegateVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelegate() {
|
||||
if (!targetUserId.value) { message.warning('请输入目标用户 ID'); return; }
|
||||
try {
|
||||
await delegateTaskApi(taskId.value, userId.value, targetUserId.value);
|
||||
message.success('已委派');
|
||||
delegateVisible.value = false;
|
||||
loadData();
|
||||
} catch {
|
||||
message.error('委派失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openTransfer(id: string) {
|
||||
taskId.value = id;
|
||||
targetUserId.value = '';
|
||||
transferVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleTransfer() {
|
||||
if (!targetUserId.value) { message.warning('请输入目标用户 ID'); return; }
|
||||
try {
|
||||
await transferTaskApi(taskId.value, userId.value, targetUserId.value);
|
||||
message.success('已转办');
|
||||
transferVisible.value = false;
|
||||
loadData();
|
||||
} catch {
|
||||
message.error('转办失败');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadData());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold">我的待办</h3>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:pagination="{ current: pageIndex, pageSize, total, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` }"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<Space>
|
||||
<Button size="small" type="primary" @click="openApprove(record.id)">审批</Button>
|
||||
<Button size="small" danger @click="openReject(record.id)">拒绝</Button>
|
||||
<Button size="small" @click="openDelegate(record.id)">委派</Button>
|
||||
<Button size="small" @click="openTransfer(record.id)">转办</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<Modal v-model:open="approveVisible" title="审批通过" @ok="handleApprove">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="审批意见">
|
||||
<Input.TextArea v-model:value="commentText" placeholder="可选" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model:open="rejectVisible" title="拒绝任务" @ok="handleReject">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="拒绝原因">
|
||||
<Input.TextArea v-model:value="commentText" placeholder="可选" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model:open="delegateVisible" title="委派任务" @ok="handleDelegate">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="目标用户 ID" required>
|
||||
<Input v-model:value="targetUserId" placeholder="请输入用户 GUID" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model:open="transferVisible" title="转办任务" @ok="handleTransfer">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="目标用户 ID" required>
|
||||
<Input v-model:value="targetUserId" placeholder="请输入用户 GUID" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
94
apps/web-antd/src/views/workflow/start/index.vue
Normal file
94
apps/web-antd/src/views/workflow/start/index.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
message,
|
||||
Spin,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getWorkflowDefinitionsApi,
|
||||
startWorkflowInstanceApi,
|
||||
DefinitionStatus,
|
||||
type WorkflowDefinitionDto,
|
||||
} from '#/api/core';
|
||||
|
||||
const loading = ref(false);
|
||||
const definitions = ref<WorkflowDefinitionDto[]>([]);
|
||||
|
||||
const statusMap: Record<number, { color: string; text: string }> = {
|
||||
[DefinitionStatus.Draft]: { color: 'blue', text: '草稿' },
|
||||
[DefinitionStatus.Published]: { color: 'green', text: '已发布' },
|
||||
[DefinitionStatus.Disabled]: { color: 'red', text: '已禁用' },
|
||||
};
|
||||
|
||||
async function loadDefinitions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getWorkflowDefinitionsApi({ pageSize: 100 });
|
||||
definitions.value = (res.items ?? []).filter(
|
||||
(d) => d.status === DefinitionStatus.Published && d.isEnabled,
|
||||
);
|
||||
} catch {
|
||||
message.error('加载流程定义失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStart(def: WorkflowDefinitionDto) {
|
||||
try {
|
||||
await startWorkflowInstanceApi({
|
||||
definitionCode: def.code,
|
||||
title: def.name,
|
||||
});
|
||||
message.success(`「${def.name}」流程已发起`);
|
||||
} catch {
|
||||
message.error('发起失败');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadDefinitions());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold">发起流程</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">点击已发布的流程即可发起</p>
|
||||
</div>
|
||||
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="definitions.length > 0" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card
|
||||
v-for="def in definitions"
|
||||
:key="def.id"
|
||||
hoverable
|
||||
class="cursor-pointer transition-shadow hover:shadow-md"
|
||||
@click="handleStart(def)"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-base">{{ def.name }}</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Tag color="green">v{{ def.version }}</Tag>
|
||||
</template>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ def.description || '暂无描述' }}
|
||||
</p>
|
||||
<div class="mt-3 text-right">
|
||||
<Button type="primary" size="small">发起</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="py-12 text-center text-gray-400">
|
||||
暂无可发起的流程
|
||||
</div>
|
||||
</Spin>
|
||||
</Page>
|
||||
</template>
|
||||
374
apps/web-antd/src/views/workflow/tasks/index.vue
Normal file
374
apps/web-antd/src/views/workflow/tasks/index.vue
Normal file
@ -0,0 +1,374 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
approveTaskApi,
|
||||
delegateTaskApi,
|
||||
getHistoryTasksApi,
|
||||
getPendingTasksApi,
|
||||
rejectTaskApi,
|
||||
TaskStatus,
|
||||
transferTaskApi,
|
||||
type WorkflowTaskListItemDto,
|
||||
} from '#/api/core';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => userStore.userInfo?.userId ?? '');
|
||||
|
||||
const activeTab = ref('pending');
|
||||
|
||||
// ── Pending ──
|
||||
|
||||
const pendingLoading = ref(false);
|
||||
const pendingData = ref<WorkflowTaskListItemDto[]>([]);
|
||||
const pendingTotal = ref(0);
|
||||
const pendingPageIndex = ref(1);
|
||||
const pendingPageSize = ref(20);
|
||||
|
||||
const pendingColumns = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: () => h(Tag, { color: 'orange' }, () => '待处理'),
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'assigneeId',
|
||||
key: 'assigneeId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{ title: '操作', key: 'actions', width: 280 },
|
||||
];
|
||||
|
||||
async function loadPending() {
|
||||
pendingLoading.value = true;
|
||||
try {
|
||||
const res = await getPendingTasksApi({
|
||||
userId: userId.value,
|
||||
pageIndex: pendingPageIndex.value,
|
||||
pageSize: pendingPageSize.value,
|
||||
});
|
||||
pendingData.value = res.items ?? [];
|
||||
pendingTotal.value = res.total ?? 0;
|
||||
} catch {
|
||||
message.error('加载待办任务失败');
|
||||
} finally {
|
||||
pendingLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePendingTableChange(pagination: any) {
|
||||
pendingPageIndex.value = pagination.current ?? 1;
|
||||
pendingPageSize.value = pagination.pageSize ?? 20;
|
||||
loadPending();
|
||||
}
|
||||
|
||||
// ── History ──
|
||||
|
||||
const historyLoading = ref(false);
|
||||
const historyData = ref<WorkflowTaskListItemDto[]>([]);
|
||||
const historyTotal = ref(0);
|
||||
const historyPageIndex = ref(1);
|
||||
const historyPageSize = ref(20);
|
||||
|
||||
const taskStatusMap: Record<number, { color: string; text: string }> = {
|
||||
[TaskStatus.Approved]: { color: 'green', text: '已通过' },
|
||||
[TaskStatus.Rejected]: { color: 'red', text: '已拒绝' },
|
||||
[TaskStatus.Transferred]: { color: 'blue', text: '已转办' },
|
||||
[TaskStatus.Delegated]: { color: 'purple', text: '已委派' },
|
||||
[TaskStatus.Pending]: { color: 'orange', text: '待处理' },
|
||||
};
|
||||
|
||||
const historyColumns = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: { record: WorkflowTaskListItemDto }) => {
|
||||
const s = taskStatusMap[record.status];
|
||||
return h(Tag, { color: s?.color }, () => s?.text ?? '未知');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'assigneeId',
|
||||
key: 'assigneeId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'completedAt',
|
||||
key: 'completedAt',
|
||||
width: 180,
|
||||
customRender: ({ text }: { text: string | null }) =>
|
||||
text ? new Date(text).toLocaleString() : '-',
|
||||
},
|
||||
];
|
||||
|
||||
async function loadHistory() {
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const res = await getHistoryTasksApi({
|
||||
userId: userId.value,
|
||||
pageIndex: historyPageIndex.value,
|
||||
pageSize: historyPageSize.value,
|
||||
});
|
||||
historyData.value = res.items ?? [];
|
||||
historyTotal.value = res.total ?? 0;
|
||||
} catch {
|
||||
message.error('加载已办任务失败');
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleHistoryTableChange(pagination: any) {
|
||||
historyPageIndex.value = pagination.current ?? 1;
|
||||
historyPageSize.value = pagination.pageSize ?? 20;
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
// ── Tab switch ──
|
||||
|
||||
function handleTabChange(key: string) {
|
||||
activeTab.value = key;
|
||||
if (key === 'pending' && pendingData.value.length === 0) loadPending();
|
||||
if (key === 'history' && historyData.value.length === 0) loadHistory();
|
||||
}
|
||||
|
||||
// ── Action modals ──
|
||||
|
||||
const approveVisible = ref(false);
|
||||
const rejectVisible = ref(false);
|
||||
const delegateVisible = ref(false);
|
||||
const transferVisible = ref(false);
|
||||
const taskId = ref('');
|
||||
const commentText = ref('');
|
||||
const targetUserId = ref('');
|
||||
|
||||
function openApprove(id: string) {
|
||||
taskId.value = id;
|
||||
commentText.value = '';
|
||||
approveVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
try {
|
||||
await approveTaskApi(
|
||||
taskId.value,
|
||||
userId.value,
|
||||
commentText.value || undefined,
|
||||
);
|
||||
message.success('审批通过');
|
||||
approveVisible.value = false;
|
||||
loadPending();
|
||||
} catch {
|
||||
message.error('审批失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(id: string) {
|
||||
taskId.value = id;
|
||||
commentText.value = '';
|
||||
rejectVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
try {
|
||||
await rejectTaskApi(
|
||||
taskId.value,
|
||||
userId.value,
|
||||
commentText.value || undefined,
|
||||
);
|
||||
message.success('已拒绝');
|
||||
rejectVisible.value = false;
|
||||
loadPending();
|
||||
} catch {
|
||||
message.error('拒绝失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openDelegate(id: string) {
|
||||
taskId.value = id;
|
||||
targetUserId.value = '';
|
||||
delegateVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelegate() {
|
||||
if (!targetUserId.value) {
|
||||
message.warning('请输入目标用户 ID');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await delegateTaskApi(taskId.value, userId.value, targetUserId.value);
|
||||
message.success('已委派');
|
||||
delegateVisible.value = false;
|
||||
loadPending();
|
||||
} catch {
|
||||
message.error('委派失败');
|
||||
}
|
||||
}
|
||||
|
||||
function openTransfer(id: string) {
|
||||
taskId.value = id;
|
||||
targetUserId.value = '';
|
||||
transferVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleTransfer() {
|
||||
if (!targetUserId.value) {
|
||||
message.warning('请输入目标用户 ID');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await transferTaskApi(taskId.value, userId.value, targetUserId.value);
|
||||
message.success('已转办');
|
||||
transferVisible.value = false;
|
||||
loadPending();
|
||||
} catch {
|
||||
message.error('转办失败');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadPending());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||
<Tabs.TabPane key="pending" tab="我的待办">
|
||||
<Table
|
||||
:columns="pendingColumns"
|
||||
:data-source="pendingData"
|
||||
:loading="pendingLoading"
|
||||
:pagination="{
|
||||
current: pendingPageIndex,
|
||||
pageSize: pendingPageSize,
|
||||
total: pendingTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handlePendingTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openApprove(record.id)"
|
||||
>
|
||||
审批
|
||||
</Button>
|
||||
<Button size="small" danger @click="openReject(record.id)">
|
||||
拒绝
|
||||
</Button>
|
||||
<Button size="small" @click="openDelegate(record.id)">
|
||||
委派
|
||||
</Button>
|
||||
<Button size="small" @click="openTransfer(record.id)">
|
||||
转办
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="history" tab="我的已办">
|
||||
<Table
|
||||
:columns="historyColumns"
|
||||
:data-source="historyData"
|
||||
:loading="historyLoading"
|
||||
:pagination="{
|
||||
current: historyPageIndex,
|
||||
pageSize: historyPageSize,
|
||||
total: historyTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleHistoryTableChange"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Modal v-model:open="approveVisible" title="审批通过" @ok="handleApprove">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="审批意见">
|
||||
<Input.TextArea
|
||||
v-model:value="commentText"
|
||||
placeholder="可选"
|
||||
:rows="3"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model:open="rejectVisible" title="拒绝任务" @ok="handleReject">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="拒绝原因">
|
||||
<Input.TextArea
|
||||
v-model:value="commentText"
|
||||
placeholder="可选"
|
||||
:rows="3"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="delegateVisible"
|
||||
title="委派任务"
|
||||
@ok="handleDelegate"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="目标用户 ID" required>
|
||||
<Input
|
||||
v-model:value="targetUserId"
|
||||
placeholder="请输入用户 GUID"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="transferVisible"
|
||||
title="转办任务"
|
||||
@ok="handleTransfer"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="目标用户 ID" required>
|
||||
<Input
|
||||
v-model:value="targetUserId"
|
||||
placeholder="请输入用户 GUID"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
Loading…
x
Reference in New Issue
Block a user