top推荐

This commit is contained in:
2025-11-25 16:00:09 +08:00
parent 24c5188eb0
commit 48ee6442b3
6 changed files with 414 additions and 138 deletions

View File

@@ -0,0 +1,191 @@
<template>
<div class="params-container" v-if="paramsSchema && paramsSchema.length">
<div
v-for="param in paramsSchema"
:key="param.name"
class="param-item"
>
<span class="param-label">
{{ param.description }}
<span class="param-type">({{ param.type }})</span>
</span>
<!-- 文本输入框 -->
<el-input
v-if="param.type === 'Input'"
v-model="innerValue[param.name]"
:placeholder="`请输入${param.description}`"
clearable
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if="param.type === 'InputNumber'"
v-model="innerNumberMap[param.name]"
:placeholder="`请输入${param.description}`"
controls-position="right"
style="width: 100%"
/>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="param.type === 'DatePicker'"
v-model="innerValue[param.name]"
type="date"
:placeholder="`请选择${param.description}`"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
<!-- 日期范围选择器 -->
<el-date-picker
v-else-if="param.type === 'DateRangePicker'"
v-model="innerValue[param.name]"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
<!-- 布尔开关 -->
<el-switch
v-else-if="param.type === 'Switch'"
v-model="innerBoolMap[param.name]"
active-text=""
inactive-text=""
/>
<!-- 下拉选择器 -->
<el-select
v-else-if="param.type === 'Select'"
v-model="innerValue[param.name]"
:placeholder="`请选择${param.description}`"
clearable
style="width: 100%"
>
<el-option
v-for="option in param.options || []"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, watch, reactive } from 'vue';
interface ParamOption {
label: string;
value: string | number | boolean;
}
interface ParamSchemaItem {
name: string;
description: string;
type:
| 'Input'
| 'InputNumber'
| 'DatePicker'
| 'DateRangePicker'
| 'Switch'
| 'Select';
valueType?: string;
value?: any;
required?: boolean;
options?: ParamOption[];
}
interface Props {
paramsSchema: ParamSchemaItem[];
modelValue: Record<string, any>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: Record<string, any>): void;
}>();
// 内部值使用 reactive保证双向绑定
const innerValue = reactive<Record<string, any>>({});
// 为了兼容不同类型控件,做一些视图层的映射
const innerNumberMap = computed({
get() {
return innerValue as Record<string, number | undefined>;
},
set(val: Record<string, number | undefined>) {
Object.assign(innerValue, val);
}
});
const innerBoolMap = computed({
get() {
return innerValue as Record<string, boolean | undefined>;
},
set(val: Record<string, boolean | undefined>) {
Object.assign(innerValue, val);
}
});
// 初始化 / 外部变更时同步
watch(
() => props.modelValue,
(val) => {
Object.keys(innerValue).forEach((k) => delete innerValue[k]);
if (val) {
Object.assign(innerValue, val);
}
},
{ immediate: true, deep: true }
);
// 内部变更时向外同步
watch(
innerValue,
(val) => {
emit('update:modelValue', { ...val });
},
{ deep: true }
);
</script>
<style scoped lang="scss">
.params-container {
padding: 12px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.param-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.param-label {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: #606266;
font-weight: 500;
.param-type {
color: #909399;
font-weight: normal;
font-size: 12px;
margin-left: 4px;
}
}
}
</style>

View File

@@ -12,3 +12,4 @@ export { default as GenericSelector } from './GenericSelector.vue';
export { default as TreeNode } from './TreeNode.vue';
export { default as Notice } from './Notice.vue';
export { default as ChangeHome } from './ChangeHome.vue';
export { default as DynamicParamForm} from './DynamicParamForm.vue'

View File

@@ -185,16 +185,15 @@
<div class="form-item">
<span class="form-label required">爬虫模板</span>
<el-select
v-model="selectedTemplate"
v-model="selectedCategory"
placeholder="请选择爬虫模板"
style="width: 100%"
>
<el-option
v-for="template in crawlerTemplates"
:key="template.name"
:label="template.name"
:value="template"
:value="template.name"
/>
</el-select>
<span class="form-tip">
@@ -226,75 +225,10 @@
<!-- 动态参数表单 -->
<div class="form-item" v-if="selectedMethod && selectedMethod.params && selectedMethod.params.length > 0">
<span class="form-label">方法参数</span>
<div class="params-container">
<div v-for="param in selectedMethod.params" :key="param.name" class="param-item">
<span class="param-label">
{{ param.description }}
<span class="param-type">({{ param.type }})</span>
</span>
<!-- 文本输入框 -->
<el-input
v-if="param.type === 'Input'"
v-model="dynamicParams[param.name]"
:placeholder="`请输入${param.description}`"
clearable
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if="param.type === 'InputNumber'"
v-model="dynamicParams[param.name]"
:placeholder="`请输入${param.description}`"
controls-position="right"
style="width: 100%"
/>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="param.type === 'DatePicker'"
v-model="dynamicParams[param.name]"
type="date"
:placeholder="`请选择${param.description}`"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
<!-- 日期范围选择器 -->
<el-date-picker
v-else-if="param.type === 'DateRangePicker'"
v-model="dynamicParams[param.name]"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
<!-- 布尔开关 -->
<el-switch
v-else-if="param.type === 'Switch'"
v-model="dynamicParams[param.name]"
active-text=""
inactive-text=""
/>
<!-- 下拉选择器 -->
<el-select
v-else-if="param.type === 'Select'"
v-model="dynamicParams[param.name]"
:placeholder="`请选择${param.description}`"
clearable
style="width: 100%"
>
<el-option
v-for="option in param.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
</div>
<DynamicParamForm
v-model="dynamicParams"
:params-schema="selectedMethod.params"
/>
</div>
<div class="form-item">
@@ -405,7 +339,7 @@ import { crontabApi } from '@/apis/crontab';
import { userApi } from '@/apis/system/user';
import type { CrontabTask, TaskMeta, CrontabItem, CrontabMethod, CrontabParam, PageParam, CreateTaskRequest, RecipientUserInfo, UserVO, ResultDomain, EmailDefault } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components';
import { GenericSelector, DynamicParamForm } from '@/components';
defineOptions({
name: 'NewsCrawlerView'
});
@@ -417,11 +351,17 @@ const crawlerList = ref<CrontabTask[]>([]);
// 爬虫元数据
const taskMetaList = ref<TaskMeta[]>([]);
const crawlerTemplates = ref<CrontabItem[]>([]); // 转换后的模板结构
const selectedTemplate = ref<CrontabItem | null>(null);
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
const selectedMetaId = ref<string>(''); // 选中的元数据ID
const selectedCategory = ref<string>(''); // 选中的模板分类名称
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
const selectedMetaId = ref<string>(''); // 选中的元数据ID
const dynamicParams = ref<Record<string, any>>({});
// 当前选中的模板对象(通过 category 查找)
const selectedTemplate = computed<CrontabItem | null>(() => {
if (!selectedCategory.value) return null;
return crawlerTemplates.value.find(t => t.name === selectedCategory.value) || null;
});
// 邮件接收人相关
const useDefaultRecipients = ref<boolean>(false);
const defaultRecipients = ref<RecipientUserInfo[]>([]);
@@ -512,10 +452,9 @@ const selectedRecipients = computed(() => {
});
// 监听模板选择变化
watch(selectedTemplate, (newTemplate, oldTemplate) => {
// 只在用户手动切换模板时重置oldTemplate存在且不为null时才重置)
// 编辑回填时oldTemplate为null不会触发重置
if (newTemplate && oldTemplate) {
watch(selectedCategory, (newCategory, oldCategory) => {
// 只在用户手动切换模板时重置oldCategory存在且不为时才重置)
if (newCategory && oldCategory) {
selectedMethodId.value = '';
dynamicParams.value = {};
}
@@ -732,7 +671,7 @@ async function loadCrawlerTemplates() {
params: meta.paramSchema ? JSON.parse(meta.paramSchema) : []
}))
}));
console.log(" crawlerTemplates.value",crawlerTemplates.value);
} else {
ElMessage.error(result.message || '加载爬虫模板失败');
@@ -801,7 +740,7 @@ function handleSizeChange(size: number) {
function handleAdd() {
isEdit.value = false;
resetFormData();
selectedTemplate.value = null;
selectedCategory.value = '';
selectedMethodId.value = '';
dynamicParams.value = {};
dialogVisible.value = true;
@@ -813,7 +752,7 @@ async function handleEdit(row: CrontabTask) {
Object.assign(formData, row);
// 重置选择
selectedTemplate.value = null;
selectedCategory.value = '';
selectedMethodId.value = '';
dynamicParams.value = {};
@@ -844,7 +783,7 @@ async function handleEdit(row: CrontabTask) {
const method = template.methods.find(m => m.metaId === row.metaId);
if (method) {
// 找到匹配的方法设置template和method
selectedTemplate.value = template;
selectedCategory.value = template.name;
selectedMethodId.value = method.metaId || '';
selectedMetaId.value = method.metaId || '';

View File

@@ -244,11 +244,10 @@
</el-form-item>
<el-form-item label="任务参数" prop="methodParams">
<el-input
v-model="taskForm.methodParams"
type="textarea"
:rows="4"
placeholder="请输入JSON格式的参数例如{}"
<DynamicParamForm
class="dynamic-params"
v-model="dynamicParams"
:params-schema="currentParamSchema"
/>
</el-form-item>
@@ -285,6 +284,7 @@ import {
} from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import AdminLayout from '@/views/admin/AdminLayout.vue';
import { DynamicParamForm } from '@/components';
import type { CrontabTask, TaskMeta, PageParam, CreateTaskRequest } from '@/types';
// 搜索表单
@@ -306,6 +306,10 @@ const taskList = ref<CrontabTask[]>([]);
const taskMetaList = ref<TaskMeta[]>([]);
const loading = ref(false);
// 动态任务参数
const dynamicParams = ref<Record<string, any>>({});
const currentParamSchema = ref<any[]>([]);
// 对话框
const dialogVisible = ref(false);
const dialogTitle = ref('新增任务');
@@ -395,6 +399,9 @@ function handleAddTask() {
methodParams: '{}',
status: 1
});
dynamicParams.value = {};
currentParamSchema.value = [];
dialogVisible.value = true;
}
@@ -412,6 +419,25 @@ function handleEdit(task: CrontabTask) {
methodParams: task.methodParams || '{}',
status: task.status
});
// 根据 metaId 加载参数 schema
const meta = taskMetaList.value.find(m => m.metaId === task.metaId);
if (meta) {
try {
currentParamSchema.value = meta.paramSchema ? JSON.parse(meta.paramSchema) : [];
} catch {
currentParamSchema.value = [];
}
} else {
currentParamSchema.value = [];
}
// 回填动态参数
try {
dynamicParams.value = task.methodParams ? JSON.parse(task.methodParams) : {};
} catch {
dynamicParams.value = {};
}
dialogVisible.value = true;
}
@@ -421,6 +447,34 @@ function handleMetaChange(metaId: string) {
if (meta) {
taskForm.taskName = meta.name;
taskForm.description = meta.description;
// 解析参数 schema
try {
currentParamSchema.value = meta.paramSchema ? JSON.parse(meta.paramSchema) : [];
} catch {
currentParamSchema.value = [];
}
// 根据 schema 默认值初始化 dynamicParams
const baseParams: Record<string, any> = {};
if (Array.isArray(currentParamSchema.value)) {
currentParamSchema.value.forEach((p: any) => {
if (p && p.name !== undefined && p.value !== undefined) {
baseParams[p.name] = p.value;
}
});
}
// 如果表单中已有 methodParams合并覆盖默认值
try {
const exist = taskForm.methodParams ? JSON.parse(taskForm.methodParams) : {};
dynamicParams.value = { ...baseParams, ...exist };
} catch {
dynamicParams.value = baseParams;
}
} else {
currentParamSchema.value = [];
dynamicParams.value = {};
}
}
@@ -432,11 +486,11 @@ async function handleSubmit() {
if (!valid) return;
try {
// 验证JSON格式
// 根据动态参数生成 methodParams
try {
JSON.parse(taskForm.methodParams || '{}');
taskForm.methodParams = JSON.stringify(dynamicParams.value || {});
} catch {
ElMessage.error('任务参数必须是有效的JSON格式');
ElMessage.error('任务参数序列化失败');
return;
}
@@ -614,7 +668,9 @@ onMounted(() => {
margin-left: auto;
}
}
.dynamic-params{
width: 100%;
}
.crawler-list {
min-height: 400px;