admin-vben/apps/web-antd/src/views/infra/job/components/JobHandlerParamEditor.vue

315 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
任务处理器参数编辑器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>