feat: 新增发起流程页面,优化流程发起体验

- 新增 /workflow/start 页面,卡片展示所有已发布且启用的流程定义
- 点击卡片即可直接发起流程,无需手动填写编码和变量
- 流程定义页面增加"发起"按钮
- 流程实例页面移除旧的发起表单,改为跳转到发起页
This commit is contained in:
向宁 2026-05-20 20:44:54 +08:00
parent 49d8abf933
commit d513c76cd6
7 changed files with 506 additions and 380 deletions

View File

@ -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: '我的任务',
},
},
{

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>