feat: 中药导入模块修复、部门财务的优化、队列的优化管理
This commit is contained in:
16
apps/web-antd/src/views/log/queue-job-business/index.vue
Normal file
16
apps/web-antd/src/views/log/queue-job-business/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 业务队列任务列表入口(菜单 name 须与 xk_menu.name 一致)
|
||||
*/
|
||||
import QueueJobList from '../queue-job/components/QueueJobList.vue';
|
||||
import { QUEUE_JOB_CATEGORY } from '../queue-job/config/constants';
|
||||
|
||||
defineOptions({ name: 'QueueJobBusiness' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QueueJobList
|
||||
:category="QUEUE_JOB_CATEGORY.BUSINESS"
|
||||
title="业务队列"
|
||||
/>
|
||||
</template>
|
||||
16
apps/web-antd/src/views/log/queue-job-infra/index.vue
Normal file
16
apps/web-antd/src/views/log/queue-job-infra/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 基础设施队列任务列表入口(菜单 name 须与 xk_menu.name 一致)
|
||||
*/
|
||||
import QueueJobList from '../queue-job/components/QueueJobList.vue';
|
||||
import { QUEUE_JOB_CATEGORY } from '../queue-job/config/constants';
|
||||
|
||||
defineOptions({ name: 'QueueJobInfra' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QueueJobList
|
||||
:category="QUEUE_JOB_CATEGORY.INFRA"
|
||||
title="基础设施队列"
|
||||
/>
|
||||
</template>
|
||||
85
apps/web-antd/src/views/log/queue-job-monitor/api/index.ts
Normal file
85
apps/web-antd/src/views/log/queue-job-monitor/api/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 队列任务监控大屏 API
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'queue-job/';
|
||||
|
||||
export type QueueJobMonitorSummary = {
|
||||
total: number;
|
||||
pending: number;
|
||||
pending_total: number;
|
||||
delayed: number;
|
||||
running: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
cancelled: number;
|
||||
fail_rate: number | null;
|
||||
};
|
||||
|
||||
export type QueueJobMonitorTrendItem = {
|
||||
label: string;
|
||||
start_at: number;
|
||||
created: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
};
|
||||
|
||||
export type QueueJobMonitorResult = {
|
||||
now: number;
|
||||
hours: number;
|
||||
summary: QueueJobMonitorSummary;
|
||||
by_category: Array<{
|
||||
category: number;
|
||||
category_txt: string;
|
||||
total: number;
|
||||
pending: number;
|
||||
running: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
}>;
|
||||
trend: QueueJobMonitorTrendItem[];
|
||||
top_failed_jobs: Array<{
|
||||
job_name: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}>;
|
||||
recent_failures: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
job_name: string;
|
||||
job_name_txt: string;
|
||||
category: number;
|
||||
category_txt: string;
|
||||
error_message: string;
|
||||
updated_at: string | number;
|
||||
created_at: string | number;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拉取监控统计;hours 为趋势窗口(默认 24)
|
||||
*/
|
||||
export async function getQueueJobStats(hours = 24) {
|
||||
return requestClient.get<QueueJobMonitorResult>(`${prefix}stats`, {
|
||||
params: { hours },
|
||||
});
|
||||
}
|
||||
|
||||
export type QueueJobProbeResult = {
|
||||
normal_id: number;
|
||||
delayed_id: number;
|
||||
fail_id: number;
|
||||
delay_seconds: number;
|
||||
available_at: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 超管测试探针:普通 / 延迟 / 失败 各投一条
|
||||
*/
|
||||
export async function createQueueJobProbe(data: {
|
||||
delay_amount: number;
|
||||
delay_unit: 'second' | 'minute' | 'hour' | 'day';
|
||||
}) {
|
||||
return requestClient.post<QueueJobProbeResult>(`${prefix}create-probe`, data);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 业务 / 基础设施任务量占比
|
||||
*/
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
data: Array<{ category_txt: string; total: number; failed: number }>;
|
||||
}>();
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts, resize } = useEcharts(chartRef);
|
||||
|
||||
async function renderChart() {
|
||||
await renderEcharts({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 0 },
|
||||
series: [
|
||||
{
|
||||
name: '任务量',
|
||||
type: 'pie',
|
||||
radius: ['42%', '68%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { formatter: '{b}\n{c}' },
|
||||
data: props.data.map((i) => ({
|
||||
name: i.category_txt,
|
||||
value: i.total,
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
await nextTick();
|
||||
resize();
|
||||
}
|
||||
|
||||
watch(() => props.data, renderChart, { deep: true, immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" class="h-[320px] w-full" />
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 超管队列探针弹窗:投递普通 / 延迟 / 失败各一条,用于验证监控与 available_at
|
||||
*/
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
FormItem,
|
||||
InputNumber,
|
||||
Select,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { createQueueJobProbe } from '../api';
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
delay_amount: 1,
|
||||
delay_unit: 'minute' as 'second' | 'minute' | 'hour' | 'day',
|
||||
});
|
||||
|
||||
const unitOptions = [
|
||||
{ label: '秒', value: 'second' },
|
||||
{ label: '分钟', value: 'minute' },
|
||||
{ label: '小时', value: 'hour' },
|
||||
{ label: '天', value: 'day' },
|
||||
];
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '创建测试队列任务',
|
||||
draggable: true,
|
||||
confirmText: '投递 3 条测试任务',
|
||||
async onConfirm() {
|
||||
if (!form.delay_amount || form.delay_amount < 1) {
|
||||
message.warning('请填写有效的延迟数值');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const res = await createQueueJobProbe({
|
||||
delay_amount: Number(form.delay_amount),
|
||||
delay_unit: form.delay_unit,
|
||||
});
|
||||
const data = res?.data || res || {};
|
||||
const availableAt = Number(data.available_at || 0);
|
||||
const availableTxt = availableAt
|
||||
? new Date(availableAt * 1000).toLocaleString()
|
||||
: '-';
|
||||
message.success(
|
||||
`已投递:普通#${data.normal_id}、延迟#${data.delayed_id}(执行 ${availableTxt})、失败#${data.fail_id}`,
|
||||
);
|
||||
emit('success');
|
||||
modalApi.close();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[520px]">
|
||||
<Alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="将同时投递 3 条业务队列探针"
|
||||
description="① 普通立即执行 ② 延迟执行(下方自定义时间,写入 available_at) ③ 故意失败(可验证失败统计/OA)。请确认 queue:work 正在消费。"
|
||||
/>
|
||||
<Form layout="vertical">
|
||||
<FormItem label="延迟时间" required>
|
||||
<div class="flex gap-2">
|
||||
<InputNumber
|
||||
v-model:value="form.delay_amount"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
class="flex-1"
|
||||
placeholder="数值"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="form.delay_unit"
|
||||
:options="unitOptions"
|
||||
class="w-[120px]"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 队列任务近 N 小时:新建 / 成功 / 失败趋势
|
||||
*/
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
import type { QueueJobMonitorTrendItem } from '../api';
|
||||
|
||||
const props = defineProps<{
|
||||
data: QueueJobMonitorTrendItem[];
|
||||
}>();
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts, resize } = useEcharts(chartRef);
|
||||
|
||||
async function renderChart() {
|
||||
const labels = props.data.map((item) => item.label);
|
||||
await renderEcharts({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['新建', '成功', '失败'] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels,
|
||||
axisLabel: { rotate: labels.length > 12 ? 45 : 0 },
|
||||
},
|
||||
yAxis: { type: 'value', minInterval: 1 },
|
||||
series: [
|
||||
{
|
||||
name: '新建',
|
||||
type: 'bar',
|
||||
data: props.data.map((i) => i.created),
|
||||
itemStyle: { color: '#1677ff' },
|
||||
},
|
||||
{
|
||||
name: '成功',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: props.data.map((i) => i.success),
|
||||
itemStyle: { color: '#52c41a' },
|
||||
},
|
||||
{
|
||||
name: '失败',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: props.data.map((i) => i.failed),
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
},
|
||||
],
|
||||
});
|
||||
await nextTick();
|
||||
resize();
|
||||
}
|
||||
|
||||
watch(() => props.data, renderChart, { deep: true, immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" class="h-[320px] w-full" />
|
||||
</template>
|
||||
272
apps/web-antd/src/views/log/queue-job-monitor/index.vue
Normal file
272
apps/web-antd/src/views/log/queue-job-monitor/index.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 队列任务监控大屏:实时快照 + 趋势 + 失败 TOP / 最近失败
|
||||
* 菜单 name 须与 xk_menu.name=QueueJobMonitor 一致
|
||||
*/
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AnalysisChartCard, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Radio,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getQueueJobStats, type QueueJobMonitorResult } from './api';
|
||||
import CategoryPieChart from './components/CategoryPieChart.vue';
|
||||
import ProbeModal from './components/ProbeModal.vue';
|
||||
import StatusTrendChart from './components/StatusTrendChart.vue';
|
||||
|
||||
defineOptions({ name: 'QueueJobMonitor' });
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
/** 仅超管 role_id=1 可见测试按钮,与后端 SUPPER_ADMIN 一致 */
|
||||
const isSuperAdmin = computed(() => {
|
||||
const roleId = Number(
|
||||
userStore.userInfo?.role_id ?? userStore.userInfo?.roles?.id,
|
||||
);
|
||||
return roleId === 1;
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const hours = ref(24);
|
||||
const data = ref<QueueJobMonitorResult | null>(null);
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const [ProbeModalComp, probeModalApi] = useVbenModal({
|
||||
connectedComponent: ProbeModal,
|
||||
});
|
||||
|
||||
function openProbeModal() {
|
||||
probeModalApi.open();
|
||||
}
|
||||
|
||||
const summaryCards = computed(() => {
|
||||
const s = data.value?.summary;
|
||||
if (!s) return [];
|
||||
return [
|
||||
{
|
||||
key: 'total',
|
||||
title: '总任务',
|
||||
value: s.total,
|
||||
tip: '未删除全部记录',
|
||||
color: '#1677ff',
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
title: '排队中',
|
||||
value: s.pending,
|
||||
tip: '已就绪待消费',
|
||||
color: '#faad14',
|
||||
},
|
||||
{
|
||||
key: 'delayed',
|
||||
title: '延迟队列',
|
||||
value: s.delayed,
|
||||
tip: 'available_at > 当前时间',
|
||||
color: '#722ed1',
|
||||
},
|
||||
{
|
||||
key: 'running',
|
||||
title: '正在执行',
|
||||
value: s.running,
|
||||
tip: 'status=1',
|
||||
color: '#13c2c2',
|
||||
},
|
||||
{
|
||||
key: 'success',
|
||||
title: '已完成',
|
||||
value: s.success,
|
||||
tip: 'status=2',
|
||||
color: '#52c41a',
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
title: '失败',
|
||||
value: s.failed,
|
||||
tip:
|
||||
s.fail_rate == null ? 'status=3' : `失败率 ${s.fail_rate}%`,
|
||||
color: '#ff4d4f',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const failColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '标题', dataIndex: 'title', ellipsis: true },
|
||||
{ title: '任务类型', dataIndex: 'job_name_txt', width: 140 },
|
||||
{ title: '分类', dataIndex: 'category_txt', width: 100 },
|
||||
{ title: '错误摘要', dataIndex: 'error_message', ellipsis: true },
|
||||
{ title: '时间', dataIndex: 'updated_at', width: 170 },
|
||||
];
|
||||
|
||||
/**
|
||||
* 拉取统计;静默刷新时不打断 UI
|
||||
*/
|
||||
async function loadStats(silent = false) {
|
||||
if (!silent) loading.value = true;
|
||||
try {
|
||||
const res = await getQueueJobStats(hours.value);
|
||||
data.value = (res?.data || res) as QueueJobMonitorResult;
|
||||
} catch {
|
||||
if (!silent) message.error('加载监控数据失败');
|
||||
} finally {
|
||||
if (!silent) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBusinessFailed() {
|
||||
router.push({ path: '/log/queue-job/business', query: { status: '3' } });
|
||||
}
|
||||
|
||||
function goInfraFailed() {
|
||||
router.push({ path: '/log/queue-job/infra', query: { status: '3' } });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats();
|
||||
// 大屏自动刷新,减轻手工点刷新
|
||||
timer = setInterval(() => loadStats(true), 30_000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="队列任务监控">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="text-sm text-gray-500">
|
||||
实时快照 · 每 30 秒自动刷新
|
||||
<span v-if="data?.now" class="ml-2">
|
||||
({{ new Date(Number(data.now) * 1000).toLocaleString() }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Radio.Group
|
||||
v-model:value="hours"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
:options="[
|
||||
{ label: '近24小时', value: 24 },
|
||||
{ label: '近7天', value: 168 },
|
||||
]"
|
||||
@change="loadStats()"
|
||||
/>
|
||||
<Button
|
||||
v-if="isSuperAdmin"
|
||||
type="default"
|
||||
danger
|
||||
@click="openProbeModal"
|
||||
>
|
||||
测试任务
|
||||
</Button>
|
||||
<Button type="primary" :loading="loading" @click="loadStats()">
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProbeModalComp @success="loadStats()" />
|
||||
|
||||
<Spin :spinning="loading">
|
||||
<div class="monitor-stat-grid mb-4">
|
||||
<Card
|
||||
v-for="card in summaryCards"
|
||||
:key="card.key"
|
||||
class="monitor-stat-card"
|
||||
:bordered="true"
|
||||
>
|
||||
<div class="text-sm text-gray-500">{{ card.title }}</div>
|
||||
<div class="monitor-stat-value" :style="{ color: card.color }">
|
||||
{{ Number(card.value || 0).toLocaleString() }}
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400">{{ card.tip }}</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||
<AnalysisChartCard class="xl:col-span-2" title="任务趋势">
|
||||
<StatusTrendChart :data="data?.trend || []" />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard title="分类占比">
|
||||
<CategoryPieChart :data="data?.by_category || []" />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
<Card title="失败 TOP(窗口内)" :bordered="true">
|
||||
<div
|
||||
v-if="!(data?.top_failed_jobs || []).length"
|
||||
class="py-8 text-center text-gray-400"
|
||||
>
|
||||
暂无失败
|
||||
</div>
|
||||
<div
|
||||
v-for="item in data?.top_failed_jobs || []"
|
||||
:key="item.job_name"
|
||||
class="mb-3 flex items-center justify-between gap-2"
|
||||
>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<Tag color="error">{{ item.count }}</Tag>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<Button size="small" @click="goBusinessFailed">业务失败列表</Button>
|
||||
<Button size="small" @click="goInfraFailed">基础设施失败列表</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card title="最近失败" :bordered="true">
|
||||
<Table
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:columns="failColumns"
|
||||
:data-source="data?.recent_failures || []"
|
||||
:scroll="{ x: 720 }"
|
||||
row-key="id"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</Spin>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.monitor-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.monitor-stat-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.monitor-stat-grid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-stat-card :deep(.ant-card-body) {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.monitor-stat-value {
|
||||
margin-top: 6px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
</style>
|
||||
48
apps/web-antd/src/views/log/queue-job/api/index.ts
Normal file
48
apps/web-antd/src/views/log/queue-job/api/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 队列任务管理 API(业务/基础设施共用,靠 category 区分)
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'queue-job/';
|
||||
|
||||
export async function getQueueJobList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function getQueueJobDetail(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 某任务的取消/重跑操作记录
|
||||
*/
|
||||
export async function getQueueJobOpLogList(id: number) {
|
||||
return requestClient.get<any>(`${prefix}op-log-list`, { params: { id } });
|
||||
}
|
||||
|
||||
export async function getQueueJobStatusOption() {
|
||||
return requestClient.get<{ value: number; label: string }[]>(
|
||||
`${prefix}status-option`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getQueueJobNameOption(category: number) {
|
||||
return requestClient.get<{ value: string; label: string }[]>(
|
||||
`${prefix}job-name-option`,
|
||||
{ params: { category } },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消排队中任务
|
||||
*/
|
||||
export async function cancelQueueJob(id: number) {
|
||||
return requestClient.post<any>(`${prefix}cancel`, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败/取消后重跑
|
||||
*/
|
||||
export async function retryQueueJob(id: number) {
|
||||
return requestClient.post<any>(`${prefix}retry`, { id });
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 队列任务列表(业务/基础设施共用)
|
||||
* 支持详情、取消、重跑;点标题看操作记录;业务队列可筛普通/延迟
|
||||
*/
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {
|
||||
cancelQueueJob,
|
||||
getQueueJobNameOption,
|
||||
getQueueJobStatusOption,
|
||||
retryQueueJob,
|
||||
} from '../api';
|
||||
import {
|
||||
QUEUE_JOB_CATEGORY,
|
||||
formatDelaySeconds,
|
||||
statusTagColor,
|
||||
} from '../config/constants';
|
||||
import { createFormOptions } from '../config/search';
|
||||
import { createGridOptions } from '../config/table';
|
||||
import DetailModal from './detail-modal.vue';
|
||||
import OpLogModal from './op-log-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
/** 1 业务 2 基础设施 */
|
||||
category: number;
|
||||
/** 页面标题 */
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const isBusiness = props.category === QUEUE_JOB_CATEGORY.BUSINESS;
|
||||
const formOptions = createFormOptions({ showDelayFilter: isBusiness });
|
||||
const gridOptions = createGridOptions(props.category);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||
connectedComponent: DetailModal,
|
||||
});
|
||||
|
||||
const [OpLogModalComp, opLogModalApi] = useVbenModal({
|
||||
connectedComponent: OpLogModal,
|
||||
});
|
||||
|
||||
function openDetail(row: Record<string, any>) {
|
||||
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||
detailModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 点标题查看取消/重跑操作记录
|
||||
*/
|
||||
function openOpLog(row: Record<string, any>) {
|
||||
opLogModalApi.setData({
|
||||
id: Number(row?.id || 0),
|
||||
title: String(row?.title || ''),
|
||||
});
|
||||
opLogModalApi.open();
|
||||
}
|
||||
|
||||
function previewError(msg: unknown) {
|
||||
try {
|
||||
const s = typeof msg === 'string' ? msg : JSON.stringify(msg);
|
||||
if (!s) return '-';
|
||||
return s.length > 80 ? `${s.slice(0, 80)}…` : s;
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消:仅排队中(status=0)
|
||||
*/
|
||||
function onCancel(row: Record<string, any>) {
|
||||
Modal.confirm({
|
||||
title: '确认取消该任务?',
|
||||
content: `任务 #${row.id}「${row.title || ''}」将被标记为已取消,worker 取到后不再执行。`,
|
||||
onOk: async () => {
|
||||
await cancelQueueJob(Number(row.id));
|
||||
message.success('已取消');
|
||||
gridApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重跑:失败(3)/取消(4),有副作用风险需二次确认
|
||||
*/
|
||||
function onRetry(row: Record<string, any>) {
|
||||
Modal.confirm({
|
||||
title: '确认重新执行?',
|
||||
content:
|
||||
`将基于 #${row.id} 新建一条任务并重新投递。` +
|
||||
'订单/退款等业务重跑可能产生副作用,请确认业务幂等后再操作。',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
const res = await retryQueueJob(Number(row.id));
|
||||
const newId = res?.id ?? res?.data?.id;
|
||||
message.success(newId ? `已重新投递,新任务 #${newId}` : '已重新投递');
|
||||
gridApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildActions(row: Record<string, any>) {
|
||||
const status = Number(row.status);
|
||||
const actions: any[] = [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
onClick: () => openDetail(row),
|
||||
},
|
||||
];
|
||||
if (status === 0) {
|
||||
actions.push({
|
||||
label: '取消',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
onClick: () => onCancel(row),
|
||||
});
|
||||
}
|
||||
if (status === 3 || status === 4) {
|
||||
actions.push({
|
||||
label: '重跑',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
onClick: () => onRetry(row),
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [statusOpts, jobOpts] = await Promise.all([
|
||||
getQueueJobStatusOption(),
|
||||
getQueueJobNameOption(props.category),
|
||||
]);
|
||||
const statusOptions = Array.isArray(statusOpts)
|
||||
? statusOpts
|
||||
: statusOpts?.data || [];
|
||||
const jobOptions = Array.isArray(jobOpts) ? jobOpts : jobOpts?.data || [];
|
||||
gridApi.formApi?.updateSchema?.([
|
||||
{
|
||||
fieldName: 'status',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: statusOptions,
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'job_name',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: jobOptions,
|
||||
placeholder: '全部',
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
// 监控页跳转可带 ?status=3 预填筛选
|
||||
const qStatus = route.query.status;
|
||||
if (qStatus !== undefined && qStatus !== null && qStatus !== '') {
|
||||
await gridApi.formApi?.setValues?.({ status: Number(qStatus) });
|
||||
gridApi.reload?.();
|
||||
}
|
||||
} catch {
|
||||
// 接口失败时沿用 constants 兜底
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="title">
|
||||
<DetailModalComp />
|
||||
<OpLogModalComp />
|
||||
<Grid>
|
||||
<template #toolbar-buttons />
|
||||
<template #title="{ row }">
|
||||
<a
|
||||
class="cursor-pointer text-primary"
|
||||
title="查看操作记录"
|
||||
@click.prevent="openOpLog(row)"
|
||||
>
|
||||
{{ row.title || '-' }}
|
||||
</a>
|
||||
</template>
|
||||
<template #delay_type="{ row }">
|
||||
<Tag :color="Number(row.is_delayed) === 1 ? 'purple' : 'default'">
|
||||
{{ row.delay_type_txt || (Number(row.is_delayed) === 1 ? '延迟' : '普通') }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="statusTagColor(Number(row.status))">
|
||||
{{ row.status_txt || row.status }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #error="{ row }">
|
||||
<span class="text-xs text-gray-600">{{
|
||||
previewError(row.error_message)
|
||||
}}</span>
|
||||
</template>
|
||||
<template #available_at="{ row }">
|
||||
<div>
|
||||
<div>{{ row.available_at || row.created_at || '-' }}</div>
|
||||
<Tag
|
||||
v-if="Number(row.is_delayed) === 1"
|
||||
color="purple"
|
||||
class="mt-1"
|
||||
>
|
||||
延迟
|
||||
{{
|
||||
Number(row.delay_seconds) > 0
|
||||
? formatDelaySeconds(Number(row.delay_seconds))
|
||||
: ''
|
||||
}}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction :actions="buildActions(row)" />
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 队列任务详情:展示 payload、错误信息、重跑链路
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, Tag } from 'ant-design-vue';
|
||||
|
||||
import { getQueueJobDetail } from '../api';
|
||||
import { statusTagColor } from '../config/constants';
|
||||
|
||||
const data = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
function formatJson(val: unknown): string {
|
||||
if (val == null || val === '') {
|
||||
return '(无)';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(val, null, 2);
|
||||
} catch {
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: true,
|
||||
draggable: true,
|
||||
showConfirmButton: false,
|
||||
cancelText: '关闭',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
data.value = {};
|
||||
return;
|
||||
}
|
||||
const payload = modalApi.getData<{
|
||||
id?: number;
|
||||
values?: Record<string, any>;
|
||||
}>();
|
||||
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||
if (id <= 0) {
|
||||
data.value = payload?.values || {};
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
modalApi.setState({ loading: true });
|
||||
try {
|
||||
const res = await getQueueJobDetail(id);
|
||||
data.value = res?.data || res || {};
|
||||
} catch {
|
||||
data.value = payload?.values || {};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
modalApi.setState({ loading: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[90%] md:w-[780px]" title="队列任务详情">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag :color="statusTagColor(Number(data.status))">
|
||||
{{ data.status_txt || data.status }}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标题" :span="2">
|
||||
{{ data.title || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="任务类型">
|
||||
{{ data.job_name_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
{{ data.category_txt || data.category }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人">
|
||||
{{ data.user_name || '-' }}(ID: {{ data.user_id || 0 }})
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="耗时">
|
||||
{{ data.time_difference || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ data.created_at || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="执行时间">
|
||||
{{ data.available_at || data.created_at || '-' }}
|
||||
<Tag v-if="Number(data.is_delayed) === 1" color="purple" class="ml-1">
|
||||
延迟
|
||||
</Tag>
|
||||
<span
|
||||
v-if="Number(data.is_delayed) === 1 && Number(data.delay_seconds) > 0"
|
||||
class="ml-1 text-xs text-gray-500"
|
||||
>
|
||||
(相对创建 {{ data.delay_type_txt || '延迟' }})
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">
|
||||
{{ data.updated_at || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="简介" :span="2">
|
||||
{{ data.detail || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="任务类名" :span="2">
|
||||
<code class="break-all text-xs">{{ data.job_name || '-' }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="错误信息" :span="2">
|
||||
<pre class="snap-pre">{{ data.error_message || '(无)' }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="重跑参数 payload" :span="2">
|
||||
<pre class="snap-pre">{{ formatJson(data.payload) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="重跑链路" :span="2">
|
||||
<div v-if="Array.isArray(data.retry_chain) && data.retry_chain.length">
|
||||
<div
|
||||
v-for="item in data.retry_chain"
|
||||
:key="item.id"
|
||||
class="mb-1 text-xs"
|
||||
>
|
||||
#{{ item.id }}
|
||||
<Tag :color="statusTagColor(Number(item.status))" class="mx-1">
|
||||
{{ item.status_txt }}
|
||||
</Tag>
|
||||
{{ item.title }}
|
||||
<span class="text-gray-400">{{ item.created_at }}</span>
|
||||
<span v-if="item.retry_from_id" class="text-gray-400">
|
||||
(来自 #{{ item.retry_from_id }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.snap-pre {
|
||||
margin: 0;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 队列任务操作记录弹窗:展示取消 / 重跑审计
|
||||
* 由列表点击标题打开
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Empty, Tag, Timeline } from 'ant-design-vue';
|
||||
|
||||
import { getQueueJobOpLogList } from '../api';
|
||||
import { opTypeTagColor } from '../config/constants';
|
||||
|
||||
const title = ref('操作记录');
|
||||
const items = ref<Record<string, any>[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
showConfirmButton: false,
|
||||
cancelText: '关闭',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
items.value = [];
|
||||
title.value = '操作记录';
|
||||
return;
|
||||
}
|
||||
const payload = modalApi.getData<{
|
||||
id?: number;
|
||||
title?: string;
|
||||
}>();
|
||||
const id = Number(payload?.id || 0);
|
||||
const jobTitle = String(payload?.title || '');
|
||||
title.value = jobTitle
|
||||
? `操作记录 · #${id} ${jobTitle}`
|
||||
: `操作记录 · #${id}`;
|
||||
if (id <= 0) {
|
||||
items.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
modalApi.setState({ loading: true });
|
||||
try {
|
||||
const res = await getQueueJobOpLogList(id);
|
||||
const data = res?.data || res || {};
|
||||
items.value = Array.isArray(data.items) ? data.items : [];
|
||||
if (data.title) {
|
||||
title.value = `操作记录 · #${id} ${data.title}`;
|
||||
}
|
||||
} catch {
|
||||
items.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
modalApi.setState({ loading: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[90%] md:w-[560px]" :title="title">
|
||||
<Empty v-if="!loading && items.length === 0" description="暂无取消/重跑记录" />
|
||||
<Timeline v-else>
|
||||
<Timeline.Item v-for="row in items" :key="row.id">
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<Tag :color="opTypeTagColor(Number(row.op_type))">
|
||||
{{ row.op_type_txt || row.op_type }}
|
||||
</Tag>
|
||||
<span class="text-xs text-gray-400">{{ row.created_at || '-' }}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
操作人:{{ row.operator_name || '-' }}
|
||||
<span class="text-gray-400">(ID: {{ row.operator_id || 0 }})</span>
|
||||
</div>
|
||||
<div v-if="Number(row.related_job_id) > 0" class="text-sm text-gray-600">
|
||||
关联任务:#{{ row.related_job_id }}
|
||||
</div>
|
||||
<div v-if="row.remark" class="text-xs text-gray-500">
|
||||
{{ row.remark }}
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</Modal>
|
||||
</template>
|
||||
54
apps/web-antd/src/views/log/queue-job/config/constants.ts
Normal file
54
apps/web-antd/src/views/log/queue-job/config/constants.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 队列任务状态/分类常量(与后端 Enum 对齐,接口失败时作兜底)
|
||||
*/
|
||||
export const QUEUE_JOB_STATUS_OPTIONS = [
|
||||
{ label: '排队中', value: 0 },
|
||||
{ label: '正在执行', value: 1 },
|
||||
{ label: '已完成', value: 2 },
|
||||
{ label: '队列异常', value: 3 },
|
||||
{ label: '已取消', value: 4 },
|
||||
];
|
||||
|
||||
/** 普通 / 延迟 筛选(与后端 delay_type 对齐) */
|
||||
export const QUEUE_JOB_DELAY_TYPE_OPTIONS = [
|
||||
{ label: '普通队列', value: 0 },
|
||||
{ label: '延迟队列', value: 1 },
|
||||
];
|
||||
|
||||
/** 1 业务 2 基础设施 */
|
||||
export const QUEUE_JOB_CATEGORY = {
|
||||
BUSINESS: 1,
|
||||
INFRA: 2,
|
||||
} as const;
|
||||
|
||||
export function statusTagColor(status: number): string {
|
||||
if (status === 0) return 'default';
|
||||
if (status === 1) return 'processing';
|
||||
if (status === 2) return 'success';
|
||||
if (status === 3) return 'error';
|
||||
if (status === 4) return 'warning';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟秒数可读文案(列表「延迟」标签旁展示)
|
||||
*/
|
||||
export function formatDelaySeconds(seconds: number): string {
|
||||
const s = Math.max(0, Math.floor(Number(seconds) || 0));
|
||||
if (s < 60) return `${s}秒`;
|
||||
if (s < 3600) return `${Math.round(s / 60)}分钟`;
|
||||
if (s < 86400) {
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.round((s % 3600) / 60);
|
||||
return m > 0 ? `${h}小时${m}分` : `${h}小时`;
|
||||
}
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.round((s % 86400) / 3600);
|
||||
return h > 0 ? `${d}天${h}小时` : `${d}天`;
|
||||
}
|
||||
|
||||
export function opTypeTagColor(opType: number): string {
|
||||
if (opType === 1) return 'warning';
|
||||
if (opType === 2) return 'processing';
|
||||
return 'default';
|
||||
}
|
||||
106
apps/web-antd/src/views/log/queue-job/config/search.ts
Normal file
106
apps/web-antd/src/views/log/queue-job/config/search.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
QUEUE_JOB_DELAY_TYPE_OPTIONS,
|
||||
QUEUE_JOB_STATUS_OPTIONS,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* 队列任务搜索表单
|
||||
* 业务队列额外提供「普通/延迟」筛选
|
||||
*/
|
||||
export function createFormOptions(options?: {
|
||||
showDelayFilter?: boolean;
|
||||
}): VbenFormProps {
|
||||
const schema: VbenFormProps['schema'] = [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '标题 / 简介 / 错误 / 任务类',
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [...QUEUE_JOB_STATUS_OPTIONS],
|
||||
placeholder: '全部',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [],
|
||||
placeholder: '全部',
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
},
|
||||
fieldName: 'job_name',
|
||||
label: '任务类型',
|
||||
},
|
||||
];
|
||||
// 业务队列:普通 / 延迟 筛选
|
||||
if (options?.showDelayFilter) {
|
||||
schema.push({
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [...QUEUE_JOB_DELAY_TYPE_OPTIONS],
|
||||
placeholder: '全部',
|
||||
},
|
||||
fieldName: 'delay_type',
|
||||
label: '队列类型',
|
||||
});
|
||||
}
|
||||
schema.push({
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
},
|
||||
// 默认近 7 天
|
||||
defaultValue: [
|
||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
});
|
||||
return {
|
||||
collapsed: false,
|
||||
schema,
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: false,
|
||||
submitOnEnter: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* search_time → start_time/end_time 时间戳
|
||||
*/
|
||||
export function normalizeQueueJobFilters(formValues: Record<string, any> = {}) {
|
||||
const params: Record<string, any> = { ...formValues };
|
||||
const range = params.search_time;
|
||||
delete params.search_time;
|
||||
if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
|
||||
params.start_time = dayjs(range[0]).startOf('day').unix();
|
||||
params.end_time = dayjs(range[1]).endOf('day').unix();
|
||||
}
|
||||
Object.keys(params).forEach((k) => {
|
||||
if (params[k] === '' || params[k] === undefined || params[k] === null) {
|
||||
delete params[k];
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
84
apps/web-antd/src/views/log/queue-job/config/table.ts
Normal file
84
apps/web-antd/src/views/log/queue-job/config/table.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getQueueJobList } from '../api';
|
||||
import { normalizeQueueJobFilters } from './search';
|
||||
|
||||
/**
|
||||
* 按分类生成表格配置(业务/基础设施共用列)
|
||||
* 标题可点开操作记录;执行时间列展示延迟标记
|
||||
*/
|
||||
export function createGridOptions(category: number): VxeGridProps<any> {
|
||||
return {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{
|
||||
field: 'title',
|
||||
title: '标题',
|
||||
minWidth: 160,
|
||||
slots: { default: 'title' },
|
||||
},
|
||||
{ field: 'job_name_txt', title: '任务类型', minWidth: 140 },
|
||||
{
|
||||
field: 'delay_type_txt',
|
||||
title: '队列类型',
|
||||
width: 100,
|
||||
slots: { default: 'delay_type' },
|
||||
},
|
||||
{
|
||||
field: 'status_txt',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{ field: 'time_difference', title: '耗时', width: 90 },
|
||||
{ field: 'user_name', title: '操作人', width: 100 },
|
||||
{
|
||||
field: 'error_message',
|
||||
title: '错误摘要',
|
||||
minWidth: 160,
|
||||
slots: { default: 'error' },
|
||||
},
|
||||
{ field: 'retry_from_id', title: '重跑自', width: 80 },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{
|
||||
field: 'available_at',
|
||||
title: '执行时间',
|
||||
width: 200,
|
||||
slots: { default: 'available_at' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getQueueJobList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
category,
|
||||
...normalizeQueueJobFilters(formValues || {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user