feat: 新增发起流程页面,优化流程发起体验
- 新增 /workflow/start 页面,卡片展示所有已发布且启用的流程定义 - 点击卡片即可直接发起流程,无需手动填写编码和变量 - 流程定义页面增加"发起"按钮 - 流程实例页面移除旧的发起表单,改为跳转到发起页
This commit is contained in:
parent
49d8abf933
commit
d513c76cd6
@ -10,6 +10,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'Workflow',
|
name: 'Workflow',
|
||||||
path: '/workflow',
|
path: '/workflow',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'WorkflowStart',
|
||||||
|
path: '/workflow/start',
|
||||||
|
component: () => import('#/views/workflow/start/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:play-circle',
|
||||||
|
title: '发起流程',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'WorkflowDefinitions',
|
name: 'WorkflowDefinitions',
|
||||||
path: '/workflow/definitions',
|
path: '/workflow/definitions',
|
||||||
@ -20,21 +29,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'WorkflowPending',
|
name: 'WorkflowTasks',
|
||||||
path: '/workflow/pending',
|
path: '/workflow/tasks',
|
||||||
component: () => import('#/views/workflow/pending/index.vue'),
|
component: () => import('#/views/workflow/tasks/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'lucide:clock',
|
icon: 'lucide:clock',
|
||||||
title: '我的待办',
|
title: '我的任务',
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WorkflowHistory',
|
|
||||||
path: '/workflow/history',
|
|
||||||
component: () => import('#/views/workflow/history/index.vue'),
|
|
||||||
meta: {
|
|
||||||
icon: 'lucide:check-circle',
|
|
||||||
title: '我的已办',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
disableWorkflowDefinitionApi,
|
disableWorkflowDefinitionApi,
|
||||||
getWorkflowDefinitionsApi,
|
getWorkflowDefinitionsApi,
|
||||||
publishWorkflowDefinitionApi,
|
publishWorkflowDefinitionApi,
|
||||||
|
startWorkflowInstanceApi,
|
||||||
updateWorkflowDefinitionApi,
|
updateWorkflowDefinitionApi,
|
||||||
DefinitionStatus,
|
DefinitionStatus,
|
||||||
type WorkflowDefinitionDto,
|
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) {
|
async function handleDelete(id: string) {
|
||||||
try {
|
try {
|
||||||
await deleteWorkflowDefinitionApi(id);
|
await deleteWorkflowDefinitionApi(id);
|
||||||
@ -177,6 +190,14 @@ onMounted(() => loadData());
|
|||||||
<template #bodyCell="{ column, record: record }">
|
<template #bodyCell="{ column, record: record }">
|
||||||
<template v-if="column.key === 'actions'">
|
<template v-if="column.key === 'actions'">
|
||||||
<Space>
|
<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.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 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>
|
<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>
|
<script lang="ts" setup>
|
||||||
import { h, onMounted, ref } from 'vue';
|
import { h, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
message,
|
message,
|
||||||
Modal,
|
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
@ -20,12 +18,12 @@ import {
|
|||||||
getWorkflowInstancesApi,
|
getWorkflowInstancesApi,
|
||||||
InstanceStatus,
|
InstanceStatus,
|
||||||
resumeWorkflowInstanceApi,
|
resumeWorkflowInstanceApi,
|
||||||
startWorkflowInstanceApi,
|
|
||||||
suspendWorkflowInstanceApi,
|
suspendWorkflowInstanceApi,
|
||||||
withdrawWorkflowInstanceApi,
|
withdrawWorkflowInstanceApi,
|
||||||
type WorkflowInstanceDto,
|
type WorkflowInstanceDto,
|
||||||
} from '#/api/core';
|
} from '#/api/core';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const data = ref<WorkflowInstanceDto[]>([]);
|
const data = ref<WorkflowInstanceDto[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@ -33,9 +31,6 @@ const pageIndex = ref(1);
|
|||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const statusFilter = ref<InstanceStatus | undefined>(undefined);
|
const statusFilter = ref<InstanceStatus | undefined>(undefined);
|
||||||
|
|
||||||
const startVisible = ref(false);
|
|
||||||
const startForm = ref({ definitionCode: '', title: '', variables: '' });
|
|
||||||
|
|
||||||
const instanceStatusMap: Record<number, { color: string; text: string }> = {
|
const instanceStatusMap: Record<number, { color: string; text: string }> = {
|
||||||
[InstanceStatus.Pending]: { color: 'blue', text: '待启动' },
|
[InstanceStatus.Pending]: { color: 'blue', text: '待启动' },
|
||||||
[InstanceStatus.Running]: { color: 'green', text: '运行中' },
|
[InstanceStatus.Running]: { color: 'green', text: '运行中' },
|
||||||
@ -117,30 +112,6 @@ function onFilterChange() {
|
|||||||
loadData();
|
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) {
|
async function handleSuspend(id: string) {
|
||||||
try {
|
try {
|
||||||
await suspendWorkflowInstanceApi(id);
|
await suspendWorkflowInstanceApi(id);
|
||||||
@ -186,7 +157,7 @@ onMounted(() => loadData());
|
|||||||
@change="onFilterChange"
|
@change="onFilterChange"
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Button type="primary" @click="openStart">发起流程</Button>
|
<Button type="primary" @click="router.push('/workflow/start')">发起流程</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
@ -214,18 +185,5 @@ onMounted(() => loadData());
|
|||||||
</template>
|
</template>
|
||||||
</Table>
|
</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>
|
</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