Pre Merge pull request !358 from fan/master-job
commit
bc0faae81e
|
|
@ -3,6 +3,23 @@ import type { PageParam, PageResult } from '@vben/request';
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace InfraJobApi {
|
export namespace InfraJobApi {
|
||||||
|
/**
|
||||||
|
* 任务处理器参数形态
|
||||||
|
* object / array / string_array
|
||||||
|
*/
|
||||||
|
export type JobParamShape = 'array' | 'object' | 'string_array';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务处理器参数字段
|
||||||
|
* 单条配置内的表单项定义
|
||||||
|
*/
|
||||||
|
export interface JobParamField {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
defaultValue?: boolean | number | string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 任务信息 */
|
/** 任务信息 */
|
||||||
export interface Job {
|
export interface Job {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -16,7 +33,52 @@ export namespace InfraJobApi {
|
||||||
monitorTimeout: number;
|
monitorTimeout: number;
|
||||||
createTime?: Date;
|
createTime?: Date;
|
||||||
nextTimes?: Date[];
|
nextTimes?: Date[];
|
||||||
|
/**
|
||||||
|
* 处理器参数字段定义
|
||||||
|
* 仅编辑时 get 详情返回
|
||||||
|
*/
|
||||||
|
paramFields?: JobParamField[];
|
||||||
|
/**
|
||||||
|
* 处理器参数形态
|
||||||
|
* 仅编辑时 get 详情返回
|
||||||
|
*/
|
||||||
|
paramShape?: JobParamShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建/修改任务提交体
|
||||||
|
* 不含 paramFields、paramShape 等详情元数据
|
||||||
|
*/
|
||||||
|
export type JobSavePayload = Pick<
|
||||||
|
Job,
|
||||||
|
| 'cronExpression'
|
||||||
|
| 'handlerName'
|
||||||
|
| 'handlerParam'
|
||||||
|
| 'id'
|
||||||
|
| 'monitorTimeout'
|
||||||
|
| 'name'
|
||||||
|
| 'retryCount'
|
||||||
|
| 'retryInterval'
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从表单值提取任务保存 payload
|
||||||
|
* 过滤 paramFields、paramShape 等只读字段
|
||||||
|
*/
|
||||||
|
export function pickJobSavePayload(
|
||||||
|
job: InfraJobApi.Job,
|
||||||
|
): InfraJobApi.JobSavePayload {
|
||||||
|
return {
|
||||||
|
id: job.id,
|
||||||
|
name: job.name,
|
||||||
|
handlerName: job.handlerName,
|
||||||
|
handlerParam: job.handlerParam,
|
||||||
|
cronExpression: job.cronExpression,
|
||||||
|
retryCount: job.retryCount,
|
||||||
|
retryInterval: job.retryInterval,
|
||||||
|
monitorTimeout: job.monitorTimeout,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询任务列表 */
|
/** 查询任务列表 */
|
||||||
|
|
@ -32,12 +94,12 @@ export function getJob(id: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增任务 */
|
/** 新增任务 */
|
||||||
export function createJob(data: InfraJobApi.Job) {
|
export function createJob(data: InfraJobApi.JobSavePayload) {
|
||||||
return requestClient.post('/infra/job/create', data);
|
return requestClient.post('/infra/job/create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改定时任务调度 */
|
/** 修改定时任务调度 */
|
||||||
export function updateJob(data: InfraJobApi.Job) {
|
export function updateJob(data: InfraJobApi.JobSavePayload) {
|
||||||
return requestClient.put('/infra/job/update', data);
|
return requestClient.put('/infra/job/update', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
<!--
|
||||||
|
任务处理器参数编辑器(Vben Form 自定义组件)
|
||||||
|
|
||||||
|
- 新建:单行 Input,手填 JSON 或纯文本
|
||||||
|
- 编辑:按后端下发的 paramShape + paramFields 渲染结构化表单
|
||||||
|
- object:单组字段
|
||||||
|
- array:多组配置块(如 FTP 列表)
|
||||||
|
- string_array:多行字符串(如域名列表)
|
||||||
|
- 对外仅暴露 handlerParam 字符串(v-model:value)
|
||||||
|
-->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type {
|
||||||
|
JobParamFieldModel,
|
||||||
|
JobParamFieldValue,
|
||||||
|
} from '../utils/job-handler-param';
|
||||||
|
|
||||||
|
import type { InfraJobApi } from '#/api/infra/job';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { ContentWrap, VbenButton } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Input } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildDefaultParamModel,
|
||||||
|
parseArrayParamValue,
|
||||||
|
parseObjectParamValue,
|
||||||
|
parseStringArrayParamValue,
|
||||||
|
serializeStringArrayParam,
|
||||||
|
} from '../utils/job-handler-param';
|
||||||
|
import JobHandlerParamFields from './JobHandlerParamFields.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'JobHandlerParamEditor' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
isEdit?: boolean;
|
||||||
|
paramFields?: InfraJobApi.JobParamField[];
|
||||||
|
paramShape?: InfraJobApi.JobParamShape;
|
||||||
|
value?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isEdit: false,
|
||||||
|
paramFields: () => [],
|
||||||
|
paramShape: 'object',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:value': [value: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 编辑态字段定义,来自 GET /infra/job/get
|
||||||
|
const fields = ref<InfraJobApi.JobParamField[]>([]);
|
||||||
|
// object 形态:单对象模型
|
||||||
|
const formModel = ref(buildDefaultParamModel([]));
|
||||||
|
// array 形态:对象数组,每项对应一个配置块
|
||||||
|
const arrayItems = ref([buildDefaultParamModel([])]);
|
||||||
|
// string_array 形态:字符串数组,每项一行输入
|
||||||
|
const stringItems = ref<string[]>(['']);
|
||||||
|
|
||||||
|
const normalizedShape = computed(() => props.paramShape ?? 'object');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑态且后端有字段定义(或 string_array)时启用结构化 UI
|
||||||
|
*/
|
||||||
|
const useStructuredForm = computed(() => {
|
||||||
|
if (!props.isEdit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (normalizedShape.value === 'string_array') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return fields.value.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const plainValue = computed({
|
||||||
|
get: () => props.value ?? '',
|
||||||
|
set: (val: string) => emit('update:value', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 paramShape 将 props.value 同步到本地模型并回写
|
||||||
|
*/
|
||||||
|
function syncFormModelFromValue() {
|
||||||
|
if (normalizedShape.value === 'string_array') {
|
||||||
|
stringItems.value = parseStringArrayParamValue(props.value);
|
||||||
|
emitStringArrayValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (normalizedShape.value === 'array') {
|
||||||
|
arrayItems.value = parseArrayParamValue(props.value, fields.value);
|
||||||
|
emitArrayValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formModel.value = parseObjectParamValue(props.value, fields.value);
|
||||||
|
emitObjectValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitObjectValue() {
|
||||||
|
if (!useStructuredForm.value || normalizedShape.value !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:value', JSON.stringify(formModel.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitArrayValue() {
|
||||||
|
if (!useStructuredForm.value || normalizedShape.value !== 'array') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:value', JSON.stringify(arrayItems.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitStringArrayValue() {
|
||||||
|
if (!useStructuredForm.value || normalizedShape.value !== 'string_array') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:value', serializeStringArrayParam(stringItems.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addArrayItem() {
|
||||||
|
arrayItems.value.push(buildDefaultParamModel(fields.value));
|
||||||
|
emitArrayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 至少保留一组空配置,避免 UI 消失
|
||||||
|
*/
|
||||||
|
function removeArrayItem(index: number) {
|
||||||
|
arrayItems.value.splice(index, 1);
|
||||||
|
if (arrayItems.value.length === 0) {
|
||||||
|
arrayItems.value.push(buildDefaultParamModel(fields.value));
|
||||||
|
}
|
||||||
|
emitArrayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStringItem() {
|
||||||
|
stringItems.value.push('');
|
||||||
|
emitStringArrayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 至少保留一行空输入
|
||||||
|
*/
|
||||||
|
function removeStringItem(index: number) {
|
||||||
|
stringItems.value.splice(index, 1);
|
||||||
|
if (stringItems.value.length === 0) {
|
||||||
|
stringItems.value.push('');
|
||||||
|
}
|
||||||
|
emitStringArrayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStringItemChange(index: number, val: string) {
|
||||||
|
stringItems.value[index] = val;
|
||||||
|
emitStringArrayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateObjectField(key: string, value: JobParamFieldValue | undefined) {
|
||||||
|
formModel.value[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateArrayItemField(
|
||||||
|
item: JobParamFieldModel,
|
||||||
|
key: string,
|
||||||
|
value: JobParamFieldValue | undefined,
|
||||||
|
) {
|
||||||
|
item[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.isEdit, props.paramFields, props.paramShape] as const,
|
||||||
|
([isEdit, paramFields]) => {
|
||||||
|
if (!isEdit) {
|
||||||
|
fields.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fields.value = paramFields ?? [];
|
||||||
|
syncFormModelFromValue();
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<!--
|
||||||
|
新建:单行文本
|
||||||
|
-->
|
||||||
|
<Input
|
||||||
|
v-if="!useStructuredForm"
|
||||||
|
v-model:value="plainValue"
|
||||||
|
placeholder="请输入处理器的参数"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
object:如 infra 清理 Job {"retainDay":14}
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-else-if="normalizedShape === 'object'"
|
||||||
|
class="flex w-full flex-col gap-2"
|
||||||
|
>
|
||||||
|
<JobHandlerParamFields
|
||||||
|
:fields="fields"
|
||||||
|
:model="formModel"
|
||||||
|
@update:field="updateObjectField"
|
||||||
|
@change="emitObjectValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
array:如云备份 FTP 配置列表
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-else-if="normalizedShape === 'array'"
|
||||||
|
class="flex w-full flex-col gap-3"
|
||||||
|
>
|
||||||
|
<ContentWrap
|
||||||
|
v-for="(item, index) in arrayItems"
|
||||||
|
:key="index"
|
||||||
|
content-class="px-4 pb-4 pt-3"
|
||||||
|
header-class="border-border px-4 py-3"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
|
<span class="text-sm font-medium text-foreground">
|
||||||
|
配置 {{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
<VbenButton
|
||||||
|
class="text-destructive hover:text-destructive"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="removeArrayItem(index)"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:trash-2" class="mr-1" />
|
||||||
|
删除
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<JobHandlerParamFields
|
||||||
|
:fields="fields"
|
||||||
|
:model="item"
|
||||||
|
:number-min="0"
|
||||||
|
@update:field="(key, value) => updateArrayItemField(item, key, value)"
|
||||||
|
@change="emitArrayValue"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
<VbenButton
|
||||||
|
class="w-full border-dashed border-border"
|
||||||
|
variant="outline"
|
||||||
|
@click="addArrayItem"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:plus" class="mr-1" />
|
||||||
|
添加配置
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
string_array:如活动日志域名列表 ["a.com"]
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-else-if="normalizedShape === 'string_array'"
|
||||||
|
class="flex w-full flex-col gap-3"
|
||||||
|
>
|
||||||
|
<ContentWrap
|
||||||
|
v-for="(item, index) in stringItems"
|
||||||
|
:key="index"
|
||||||
|
content-class="px-4 pb-4 pt-3"
|
||||||
|
header-class="border-border px-4 py-3"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
|
<span class="text-sm font-medium text-foreground">
|
||||||
|
配置 {{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
<VbenButton
|
||||||
|
class="text-destructive hover:text-destructive"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="removeStringItem(index)"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:trash-2" class="mr-1" />
|
||||||
|
删除
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex w-full items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-[120px] shrink-0 text-right text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
域名
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
:value="item"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
placeholder="请输入域名"
|
||||||
|
@update:value="(v) => handleStringItemChange(index, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
<VbenButton
|
||||||
|
class="w-full border-dashed border-border"
|
||||||
|
variant="outline"
|
||||||
|
@click="addStringItem"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:plus" class="mr-1" />
|
||||||
|
添加配置
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<!--
|
||||||
|
处理器参数字段行:label 左(120px)+ 输入右
|
||||||
|
字段类型由 defaultValue 推断:number → InputNumber,其余 → Input
|
||||||
|
-->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type {
|
||||||
|
JobParamFieldModel,
|
||||||
|
JobParamFieldValue,
|
||||||
|
} from '../utils/job-handler-param';
|
||||||
|
|
||||||
|
import type { InfraJobApi } from '#/api/infra/job';
|
||||||
|
|
||||||
|
import { Input, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { isNumberParamField } from '../utils/job-handler-param';
|
||||||
|
|
||||||
|
defineOptions({ name: 'JobHandlerParamFields' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/**
|
||||||
|
* 后端 JobHandler#getParamFields() 下发的字段定义
|
||||||
|
*/
|
||||||
|
fields: InfraJobApi.JobParamField[];
|
||||||
|
/**
|
||||||
|
* 当前字段值模型,只读;变更通过 update:field 回传
|
||||||
|
*/
|
||||||
|
model: JobParamFieldModel;
|
||||||
|
/**
|
||||||
|
* 数字字段最小值
|
||||||
|
* array 模式 FTP 端口等可为 0
|
||||||
|
*/
|
||||||
|
numberMin?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [];
|
||||||
|
'update:field': [key: string, value: JobParamFieldValue | undefined];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function toNumberValue(
|
||||||
|
val: JobParamFieldValue | undefined,
|
||||||
|
): number | undefined {
|
||||||
|
if (val === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const num = typeof val === 'number' ? val : Number(String(val));
|
||||||
|
return Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumberFieldValue(key: string) {
|
||||||
|
return toNumberValue(props.model[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFieldValue(key: string) {
|
||||||
|
const val = props.model[key];
|
||||||
|
if (val === undefined || val === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNumberChange(
|
||||||
|
key: string,
|
||||||
|
val: null | number | string | undefined,
|
||||||
|
) {
|
||||||
|
let value: JobParamFieldValue | undefined;
|
||||||
|
if (val === null || val === undefined || val === '') {
|
||||||
|
value = undefined;
|
||||||
|
} else {
|
||||||
|
const num = typeof val === 'number' ? val : Number(val);
|
||||||
|
value = Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
||||||
|
emit('update:field', key, value);
|
||||||
|
emit('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStringChange(key: string, val: string) {
|
||||||
|
emit('update:field', key, val);
|
||||||
|
emit('change');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="field in fields"
|
||||||
|
:key="field.key"
|
||||||
|
class="flex w-full items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="w-[120px] shrink-0 text-right text-sm text-muted-foreground">
|
||||||
|
{{ field.label }}
|
||||||
|
<span v-if="field.required" class="text-destructive">*</span>
|
||||||
|
</span>
|
||||||
|
<InputNumber
|
||||||
|
v-if="isNumberParamField(field)"
|
||||||
|
:value="getNumberFieldValue(field.key)"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
:min="numberMin ?? 1"
|
||||||
|
@update:value="(v) => handleNumberChange(field.key, v)"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-else
|
||||||
|
:value="getStringFieldValue(field.key)"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
allow-clear
|
||||||
|
@update:value="(v) => handleStringChange(field.key, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { InfraJobApi } from '#/api/infra/job';
|
||||||
import type { DescriptionItemSchema } from '#/components/description';
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
import { h, markRaw } from 'vue';
|
import { h, markRaw } from 'vue';
|
||||||
|
|
@ -13,7 +14,12 @@ import { Timeline } from 'ant-design-vue';
|
||||||
import { CronTab } from '#/components/cron-tab';
|
import { CronTab } from '#/components/cron-tab';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
import JobHandlerParamEditor from './components/JobHandlerParamEditor.vue';
|
||||||
|
import { formatHandlerParamDisplay } from './utils/job-handler-param';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增/修改的表单
|
||||||
|
*/
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -49,9 +55,37 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
{
|
{
|
||||||
fieldName: 'handlerParam',
|
fieldName: 'handlerParam',
|
||||||
label: '处理器的参数',
|
label: '处理器的参数',
|
||||||
|
// 自定义组件:新建单行输入,编辑结构化(见 JobHandlerParamEditor)
|
||||||
|
component: markRaw(JobHandlerParamEditor),
|
||||||
|
modelPropName: 'value',
|
||||||
|
formItemClass: 'col-span-2 items-start',
|
||||||
|
help: '新建时手填 JSON 或文本;编辑时按处理器定义展示结构化表单',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['id', 'handlerName', 'paramFields', 'paramShape'],
|
||||||
|
componentProps: (values) => ({
|
||||||
|
isEdit: !!values.id,
|
||||||
|
paramFields: (values.paramFields ??
|
||||||
|
[]) as InfraJobApi.JobParamField[],
|
||||||
|
paramShape: (values.paramShape ??
|
||||||
|
'object') as InfraJobApi.JobParamShape,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 详情元数据:仅编辑时 get 返回,供 handlerParam 组件读取,提交时需过滤
|
||||||
|
{
|
||||||
|
fieldName: 'paramFields',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
dependencies: {
|
||||||
placeholder: '请输入处理器的参数',
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'paramShape',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -207,6 +241,16 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
{
|
{
|
||||||
field: 'handlerParam',
|
field: 'handlerParam',
|
||||||
label: '处理器的参数',
|
label: '处理器的参数',
|
||||||
|
render: (val) => {
|
||||||
|
return h(
|
||||||
|
'pre',
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
'mb-0 max-h-[240px] overflow-auto whitespace-pre-wrap break-all rounded-md border border-border bg-muted p-2 text-xs text-foreground',
|
||||||
|
},
|
||||||
|
formatHandlerParamDisplay(val as string),
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'cronExpression',
|
field: 'cronExpression',
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,18 @@ import { useVbenModal } from '@vben/common-ui';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { createJob, getJob, updateJob } from '#/api/infra/job';
|
import {
|
||||||
|
createJob,
|
||||||
|
getJob,
|
||||||
|
pickJobSavePayload,
|
||||||
|
updateJob,
|
||||||
|
} from '#/api/infra/job';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
defineOptions({ name: 'InfraJobForm' });
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<InfraJobApi.Job>();
|
const formData = ref<InfraJobApi.Job>();
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
|
|
@ -41,8 +48,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
// 提交表单
|
// 过滤 paramFields / paramShape 等只读字段,仅提交 JobSavePayload
|
||||||
const data = (await formApi.getValues()) as InfraJobApi.Job;
|
const data = pickJobSavePayload(
|
||||||
|
(await formApi.getValues()) as InfraJobApi.Job,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id ? updateJob(data) : createJob(data));
|
await (formData.value?.id ? updateJob(data) : createJob(data));
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
|
|
@ -60,13 +69,22 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const data = modalApi.getData<InfraJobApi.Job>();
|
const data = modalApi.getData<InfraJobApi.Job>();
|
||||||
if (!data || !data.id) {
|
// 新建:无 paramFields,handlerParam 为单行文本
|
||||||
|
if (!data?.id) {
|
||||||
|
formData.value = undefined;
|
||||||
|
await formApi.resetForm();
|
||||||
|
await formApi.setValues({
|
||||||
|
handlerParam: '',
|
||||||
|
retryCount: 0,
|
||||||
|
retryInterval: 0,
|
||||||
|
monitorTimeout: 0,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 编辑:get 详情含 paramFields + paramShape,驱动结构化参数表单
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getJob(data.id);
|
formData.value = await getJob(data.id);
|
||||||
// 设置到 values
|
|
||||||
await formApi.setValues(formData.value);
|
await formApi.setValues(formData.value);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
|
|
@ -76,7 +94,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :title="getTitle" class="w-2/5">
|
<Modal :title="getTitle" class="w-3/5">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* 任务处理器参数(handlerParam)解析与序列化工具
|
||||||
|
*
|
||||||
|
* handlerParam 在库中存为 JSON 字符串,形态由后端 JobHandler#getParamShape() 决定:
|
||||||
|
* - object:单个 JSON 对象
|
||||||
|
* - array:JSON 对象数组
|
||||||
|
* - string_array:JSON 字符串数组
|
||||||
|
*/
|
||||||
|
import type { InfraJobApi } from '#/api/infra/job';
|
||||||
|
|
||||||
|
export type JobParamFieldValue = number | string | undefined;
|
||||||
|
|
||||||
|
export type JobParamFieldModel = Record<string, JobParamFieldValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断字段是否渲染为数字输入框
|
||||||
|
* defaultValue 为 number 时使用 InputNumber
|
||||||
|
*/
|
||||||
|
export function isNumberParamField(field: InfraJobApi.JobParamField) {
|
||||||
|
return typeof field.defaultValue === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字段定义生成空模型
|
||||||
|
* 有 defaultValue 时预填
|
||||||
|
*/
|
||||||
|
export function buildDefaultParamModel(
|
||||||
|
paramFields: InfraJobApi.JobParamField[],
|
||||||
|
): JobParamFieldModel {
|
||||||
|
const model: JobParamFieldModel = {};
|
||||||
|
for (const field of paramFields) {
|
||||||
|
const defaultValue = field.defaultValue;
|
||||||
|
model[field.key] =
|
||||||
|
typeof defaultValue === 'number' || typeof defaultValue === 'string'
|
||||||
|
? defaultValue
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 JSON 对象键值写入模型
|
||||||
|
* 按字段类型做 number / string 转换
|
||||||
|
*/
|
||||||
|
export function mapObjectToParamModel(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
paramFields: InfraJobApi.JobParamField[],
|
||||||
|
): JobParamFieldModel {
|
||||||
|
const model = buildDefaultParamModel(paramFields);
|
||||||
|
for (const field of paramFields) {
|
||||||
|
const raw = obj[field.key];
|
||||||
|
if (raw === undefined || raw === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isNumberParamField(field)) {
|
||||||
|
const num = Number(raw);
|
||||||
|
if (!Number.isNaN(num)) {
|
||||||
|
model[field.key] = num;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model[field.key] = String(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* object 形态
|
||||||
|
* 解析 handlerParam → 单对象模型
|
||||||
|
*/
|
||||||
|
export function parseObjectParamValue(
|
||||||
|
value: string | undefined,
|
||||||
|
paramFields: InfraJobApi.JobParamField[],
|
||||||
|
): JobParamFieldModel {
|
||||||
|
const model = buildDefaultParamModel(paramFields);
|
||||||
|
if (!value?.trim()) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as Record<string, unknown>;
|
||||||
|
return mapObjectToParamModel(parsed, paramFields);
|
||||||
|
} catch {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* array 形态
|
||||||
|
* 解析 handlerParam → 对象数组
|
||||||
|
* 兼容历史单对象写法(自动包成数组)
|
||||||
|
*/
|
||||||
|
export function parseArrayParamValue(
|
||||||
|
value: string | undefined,
|
||||||
|
paramFields: InfraJobApi.JobParamField[],
|
||||||
|
): JobParamFieldModel[] {
|
||||||
|
const fallback = [buildDefaultParamModel(paramFields)];
|
||||||
|
if (!value?.trim()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(value);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.length > 0
|
||||||
|
? parsed.map((item) =>
|
||||||
|
mapObjectToParamModel(item as Record<string, unknown>, paramFields),
|
||||||
|
)
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
// 兼容旧数据:单对象自动包成数组
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
return [
|
||||||
|
mapObjectToParamModel(parsed as Record<string, unknown>, paramFields),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 非法 JSON,回退默认一组空配置
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* string_array 形态
|
||||||
|
* 解析 handlerParam → 字符串行列表
|
||||||
|
* 兼容历史 extraDomains 对象写法
|
||||||
|
*/
|
||||||
|
export function parseStringArrayParamValue(
|
||||||
|
value: string | undefined,
|
||||||
|
): string[] {
|
||||||
|
const fallback = [''];
|
||||||
|
if (!value?.trim()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(value);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.length > 0 ? parsed.map(String) : fallback;
|
||||||
|
}
|
||||||
|
// 兼容旧 object 形态:extraDomains 逗号分隔
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
const extraDomains = (parsed as Record<string, unknown>).extraDomains;
|
||||||
|
if (typeof extraDomains === 'string' && extraDomains.trim()) {
|
||||||
|
const domains = extraDomains
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return domains.length > 0 ? domains : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 非法 JSON,回退一行空输入
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* string_array 序列化
|
||||||
|
* 去空白后输出 JSON 数组,空项不写入
|
||||||
|
*/
|
||||||
|
export function serializeStringArrayParam(items: string[]) {
|
||||||
|
const values = items
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return JSON.stringify(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情 / 列表展示 handlerParam
|
||||||
|
* 合法 JSON 时格式化缩进,否则原样返回
|
||||||
|
*/
|
||||||
|
export function formatHandlerParamDisplay(val?: string) {
|
||||||
|
if (!val?.trim()) {
|
||||||
|
return '无';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(val), null, 2);
|
||||||
|
} catch {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue