perf: infra table action

This commit is contained in:
xingyu4j
2025-05-20 16:45:38 +08:00
parent 302bcc25fb
commit 0a40bdf276
36 changed files with 925 additions and 1128 deletions

View File

@@ -1,12 +1,15 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraJobApi } from '#/api/infra/job';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { useAccess } from '@vben/access';
import { h } from 'vue';
import { DICT_TYPE, getDictOptions, InfraJobStatusEnum } from '#/utils';
import { formatDateTime } from '@vben/utils';
const { hasAccessByCodes } = useAccess();
import { Timeline } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -124,9 +127,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 表格列配置 */
export function useGridColumns<T = InfraJobApi.Job>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@@ -163,58 +164,77 @@ export function useGridColumns<T = InfraJobApi.Job>(
minWidth: 120,
},
{
field: 'operation',
title: '操作',
width: 280,
width: 240,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '任务',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:job:update']),
},
{
code: 'update-status',
text: '开启',
show: (row: any) =>
hasAccessByCodes(['infra:job:update']) &&
row.status === InfraJobStatusEnum.STOP,
},
{
code: 'update-status',
text: '暂停',
show: (row: any) =>
hasAccessByCodes(['infra:job:update']) &&
row.status === InfraJobStatusEnum.NORMAL,
},
{
code: 'trigger',
text: '执行',
show: hasAccessByCodes(['infra:job:trigger']),
},
// TODO @芋艿:增加一个“更多”选项
{
code: 'detail',
text: '详细',
show: hasAccessByCodes(['infra:job:query']),
},
{
code: 'log',
text: '日志',
show: hasAccessByCodes(['infra:job:query']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:job:delete']),
},
],
slots: { default: 'actions' },
},
];
}
/** 详情的配置 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{
field: 'id',
label: '任务编号',
},
{
field: 'name',
label: '任务名称',
},
{
field: 'status',
label: '任务状态',
content: (data) =>
h(DictTag, {
type: DICT_TYPE.INFRA_JOB_STATUS,
value: data?.status,
}),
},
{
field: 'handlerName',
label: '处理器的名字',
},
{
field: 'handlerParam',
label: '处理器的参数',
},
{
field: 'cronExpression',
label: 'CRON 表达式',
},
{
field: 'retryCount',
label: '重试次数',
},
{
field: 'retryInterval',
label: '重试间隔',
},
{
field: 'monitorTimeout',
label: '监控超时时间',
content: (data) =>
data?.monitorTimeout && data.monitorTimeout > 0
? `${data.monitorTimeout} 毫秒`
: '未开启',
},
{
field: 'nextTimes',
label: '后续执行时间',
content: (data) => {
if (!data?.nextTimes) {
return '无后续执行时间';
}
if (data.nextTimes.length === 0) {
return '无后续执行时间';
}
return h(Timeline, {}, () =>
data.nextTimes.map((time: any) =>
h(Timeline.Item, {}, () => formatDateTime(time)?.toString()),
),
);
},
},
];

View File

@@ -1,19 +1,15 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraJobApi } from '#/api/infra/job';
import { useRouter } from 'vue-router';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { Download, History, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteJob,
exportJob,
@@ -47,28 +43,28 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportJob(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '定时任务.xls', source: data });
}
/** 创建任务 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑任务 */
function onEdit(row: InfraJobApi.Job) {
function handleEdit(row: InfraJobApi.Job) {
formModalApi.setData(row).open();
}
/** 查看任务详情 */
function onDetail(row: InfraJobApi.Job) {
function handleDetail(row: InfraJobApi.Job) {
detailModalApi.setData({ id: row.id }).open();
}
/** 更新任务状态 */
async function onUpdateStatus(row: InfraJobApi.Job) {
async function handleUpdateStatus(row: InfraJobApi.Job) {
const status =
row.status === InfraJobStatusEnum.STOP
? InfraJobStatusEnum.NORMAL
@@ -86,7 +82,7 @@ async function onUpdateStatus(row: InfraJobApi.Job) {
}
/** 执行一次任务 */
async function onTrigger(row: InfraJobApi.Job) {
async function handleTrigger(row: InfraJobApi.Job) {
confirm({
content: `确定执行一次 ${row.name} 吗?`,
}).then(async () => {
@@ -96,7 +92,7 @@ async function onTrigger(row: InfraJobApi.Job) {
}
/** 跳转到任务日志 */
function onLog(row?: InfraJobApi.Job) {
function handleLog(row?: InfraJobApi.Job) {
push({
name: 'InfraJobLog',
query: row?.id ? { id: row.id } : {},
@@ -104,7 +100,7 @@ function onLog(row?: InfraJobApi.Job) {
}
/** 删除任务 */
async function onDelete(row: InfraJobApi.Job) {
async function handleDelete(row: InfraJobApi.Job) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
@@ -119,42 +115,12 @@ async function onDelete(row: InfraJobApi.Job) {
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<InfraJobApi.Job>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'detail': {
onDetail(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'log': {
onLog(row);
break;
}
case 'trigger': {
onTrigger(row);
break;
}
case 'update-status': {
onUpdateStatus(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -191,32 +157,91 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal />
<Grid table-title="定时任务列表">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['infra:job:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['任务']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:job:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onLog(undefined)"
v-access:code="['infra:job:query']"
>
<History class="size-5" />
执行日志
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['任务']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['infra:job:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['infra:job:export'],
onClick: handleExport,
},
{
label: '执行日志',
type: 'primary',
icon: 'lucide:history',
auth: ['infra:job:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['infra:job:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '开启',
type: 'link',
icon: 'lucide:circle-play',
auth: ['infra:job:update'],
ifShow: () => row.status === InfraJobStatusEnum.STOP,
onClick: handleUpdateStatus.bind(null, row),
},
{
label: '暂停',
type: 'link',
icon: 'lucide:circle-pause',
auth: ['infra:job:update'],
ifShow: () => row.status === InfraJobStatusEnum.NORMAL,
onClick: handleUpdateStatus.bind(null, row),
},
{
label: '执行',
type: 'link',
icon: 'lucide:clock-plus',
auth: ['infra:job:trigger'],
onClick: handleTrigger.bind(null, row),
},
]"
:drop-down-actions="[
{
label: $t('common.detail'),
type: 'link',
auth: ['infra:job:query'],
onClick: handleDetail.bind(null, row),
},
{
label: '日志',
type: 'link',
auth: ['infra:job:query'],
onClick: handleLog.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
auth: ['infra:job:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@@ -1,16 +1,16 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraJobLogApi } from '#/api/infra/job-log';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { h } from 'vue';
import { useAccess } from '@vben/access';
import { formatDateTime } from '@vben/utils';
import dayjs from 'dayjs';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@@ -65,9 +65,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 表格列配置 */
export function useGridColumns<T = InfraJobLogApi.JobLog>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@@ -120,26 +118,61 @@ export function useGridColumns<T = InfraJobLogApi.JobLog>(
},
},
{
field: 'operation',
title: '操作',
width: 80,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '日志',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详细',
show: hasAccessByCodes(['infra:job:query']),
},
],
},
slots: { default: 'actions' },
},
];
}
/** 详情的配置 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{
field: 'id',
label: '日志编号',
},
{
field: 'jobId',
label: '任务编号',
},
{
field: 'handlerName',
label: '处理器的名字',
},
{
field: 'handlerParam',
label: '处理器的参数',
},
{
field: 'executeIndex',
label: '第几次执行',
},
{
field: 'beginTime',
label: '执行时间',
},
{
field: 'endTime',
label: '结束时间',
},
{
field: 'duration',
label: '执行时长',
},
{
field: 'status',
label: '任务状态',
content: (data) =>
h(DictTag, {
type: DICT_TYPE.INFRA_JOB_LOG_STATUS,
value: data?.status,
}),
},
{
field: 'result',
label: '执行结果',
},
];
}

View File

@@ -1,19 +1,13 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraJobLogApi } from '#/api/infra/job-log';
import { useRoute } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportJobLog, getJobLogPage } from '#/api/infra/job-log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@@ -29,29 +23,16 @@ const [DetailModal, detailModalApi] = useVbenModal({
});
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportJobLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '任务日志.xls', source: data });
}
/** 查看日志详情 */
function onDetail(row: InfraJobLogApi.JobLog) {
function handleDetail(row: InfraJobLogApi.JobLog) {
detailModalApi.setData({ id: row.id }).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraJobLogApi.JobLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
// 获取表单schema并设置默认jobId
const formSchema = useGridFormSchema();
@@ -60,7 +41,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: formSchema,
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -97,15 +78,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal />
<Grid table-title="任务日志列表">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:job:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['infra:job:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['infra:job:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@@ -4,13 +4,11 @@ import type { InfraJobLogApi } from '#/api/infra/job-log';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions } from 'ant-design-vue';
import { getJobLog } from '#/api/infra/job-log';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
const formData = ref<InfraJobLogApi.JobLog>();
@@ -33,6 +31,15 @@ const [Modal, modalApi] = useVbenModal({
}
},
});
const [Description] = useDescription({
componentProps: {
bordered: true,
column: 1,
class: 'mx-4',
},
schema: useDetailSchema(),
});
</script>
<template>
@@ -42,44 +49,6 @@ const [Modal, modalApi] = useVbenModal({
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions
:column="1"
bordered
size="middle"
class="mx-4"
:label-style="{ width: '140px' }"
>
<Descriptions.Item label="日志编号">
{{ formData?.id }}
</Descriptions.Item>
<Descriptions.Item label="任务编号">
{{ formData?.jobId }}
</Descriptions.Item>
<Descriptions.Item label="处理器的名字">
{{ formData?.handlerName }}
</Descriptions.Item>
<Descriptions.Item label="处理器的参数">
{{ formData?.handlerParam }}
</Descriptions.Item>
<Descriptions.Item label="第几次执行">
{{ formData?.executeIndex }}
</Descriptions.Item>
<Descriptions.Item label="执行时间">
{{ formData?.beginTime ? formatDateTime(formData.beginTime) : '' }} ~
{{ formData?.endTime ? formatDateTime(formData.endTime) : '' }}
</Descriptions.Item>
<Descriptions.Item label="执行时长">
{{ formData?.duration ? `${formData.duration} 毫秒` : '' }}
</Descriptions.Item>
<Descriptions.Item label="任务状态">
<DictTag
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
:value="formData?.status"
/>
</Descriptions.Item>
<Descriptions.Item label="执行结果">
{{ formData?.result }}
</Descriptions.Item>
</Descriptions>
<Description :data="formData" />
</Modal>
</template>

View File

@@ -4,13 +4,11 @@ import type { InfraJobApi } from '#/api/infra/job';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions, Timeline } from 'ant-design-vue';
import { getJob, getJobNextTimes } from '#/api/infra/job';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
const formData = ref<InfraJobApi.Job>(); // 任务详情
const nextTimes = ref<Date[]>([]); // 下一次执行时间
@@ -31,11 +29,21 @@ const [Modal, modalApi] = useVbenModal({
formData.value = await getJob(data.id);
// 获取下一次执行时间
nextTimes.value = await getJobNextTimes(data.id);
formData.value.nextTimes = nextTimes.value;
} finally {
modalApi.unlock();
}
},
});
const [Description] = useDescription({
componentProps: {
bordered: true,
column: 1,
class: 'mx-4',
},
schema: useDetailSchema(),
});
</script>
<template>
@@ -45,57 +53,6 @@ const [Modal, modalApi] = useVbenModal({
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions
:column="1"
bordered
size="middle"
class="mx-4"
:label-style="{ width: '140px' }"
>
<Descriptions.Item label="任务编号">
{{ formData?.id }}
</Descriptions.Item>
<Descriptions.Item label="任务名称">
{{ formData?.name }}
</Descriptions.Item>
<Descriptions.Item label="任务状态">
<DictTag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="formData?.status" />
</Descriptions.Item>
<Descriptions.Item label="处理器的名字">
{{ formData?.handlerName }}
</Descriptions.Item>
<Descriptions.Item label="处理器的参数">
{{ formData?.handlerParam }}
</Descriptions.Item>
<Descriptions.Item label="Cron 表达式">
{{ formData?.cronExpression }}
</Descriptions.Item>
<Descriptions.Item label="重试次数">
{{ formData?.retryCount }}
</Descriptions.Item>
<Descriptions.Item label="重试间隔">
{{
formData?.retryInterval ? `${formData.retryInterval} 毫秒` : '无间隔'
}}
</Descriptions.Item>
<Descriptions.Item label="监控超时时间">
{{
formData?.monitorTimeout && formData.monitorTimeout > 0
? `${formData.monitorTimeout} 毫秒`
: '未开启'
}}
</Descriptions.Item>
<Descriptions.Item label="后续执行时间">
<Timeline class="h-[180px]">
<Timeline.Item
v-for="(nextTime, index) in nextTimes"
:key="index"
color="blue"
>
{{ index + 1 }} {{ formatDateTime(nextTime.toString()) }}
</Timeline.Item>
</Timeline>
</Descriptions.Item>
</Descriptions>
<Description :data="formData" />
</Modal>
</template>