top推荐
This commit is contained in:
@@ -289,7 +289,24 @@ INSERT INTO `tb_crontab_task_meta` (
|
|||||||
'topRecommendTask',
|
'topRecommendTask',
|
||||||
'execute',
|
'execute',
|
||||||
'',
|
'',
|
||||||
'[]',
|
'[
|
||||||
|
{
|
||||||
|
"name": "viewNum",
|
||||||
|
"description": "按浏览量取多少条",
|
||||||
|
"type": "InputNumber",
|
||||||
|
"valueType": "Integer",
|
||||||
|
"value": 10,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timeNum",
|
||||||
|
"description": "按时间取多少条",
|
||||||
|
"type": "InputNumber",
|
||||||
|
"valueType": "Integer",
|
||||||
|
"value": 10,
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]',
|
||||||
0,
|
0,
|
||||||
9,
|
9,
|
||||||
'system',
|
'system',
|
||||||
@@ -310,7 +327,7 @@ INSERT INTO `tb_crontab_task` (
|
|||||||
'topRecommendTask',
|
'topRecommendTask',
|
||||||
'0 0 1 * * ?',
|
'0 0 1 * * ?',
|
||||||
'execute',
|
'execute',
|
||||||
'{}',
|
'{"viewNum":10,"timeNum":10}',
|
||||||
1,
|
1,
|
||||||
'system',
|
'system',
|
||||||
NOW()
|
NOW()
|
||||||
|
|||||||
@@ -43,49 +43,108 @@ public class TopRecommendTask extends BaseTask {
|
|||||||
logger.info("开始执行热门资源推荐任务");
|
logger.info("开始执行热门资源推荐任务");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 从参数中获取配置:浏览量和时间排序各取多少条,默认都为10
|
||||||
|
Integer viewNum = taskParams != null ? taskParams.getParamAsInt("viewNum") : null;
|
||||||
|
Integer timeNum = taskParams != null ? taskParams.getParamAsInt("timeNum") : null;
|
||||||
|
|
||||||
|
if (viewNum == null || viewNum <= 0) {
|
||||||
|
viewNum = 10;
|
||||||
|
}
|
||||||
|
if (timeNum == null || timeNum <= 0) {
|
||||||
|
timeNum = 10;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 清除旧的热门推荐(recommend_type = 1)
|
// 1. 清除旧的热门推荐(recommend_type = 1)
|
||||||
// clearOldRecommends();
|
// clearOldRecommends();
|
||||||
|
|
||||||
// 2. 获取浏览量前10的资源
|
// 2. 合并并去重(使用 Set 确保资源ID唯一),并处理补位逻辑
|
||||||
List<ResourceVO> topViewCountResources = getTopResourcesByViewCount(10);
|
|
||||||
logger.info("获取到浏览量前10的资源数量: {}", topViewCountResources.size());
|
|
||||||
|
|
||||||
// 3. 获取发布时间前10的资源
|
|
||||||
List<ResourceVO> topPublishTimeResources = getTopResourcesByPublishTime(10);
|
|
||||||
logger.info("获取到发布时间前10的资源数量: {}", topPublishTimeResources.size());
|
|
||||||
|
|
||||||
// 4. 合并并去重(使用 Set 确保资源ID唯一)
|
|
||||||
Set<String> resourceIds = new HashSet<>();
|
Set<String> resourceIds = new HashSet<>();
|
||||||
List<TbResourceRecommend> recommendList = new ArrayList<>();
|
List<TbResourceRecommend> recommendList = new ArrayList<>();
|
||||||
int orderNum = 1;
|
int orderNum = 1;
|
||||||
|
|
||||||
// 添加浏览量前10
|
int viewAdded = 0;
|
||||||
for (ResourceVO vo : topViewCountResources) {
|
int timeAdded = 0;
|
||||||
if (vo.getResource() != null && resourceIds.add(vo.getResource().getResourceID())) {
|
|
||||||
recommendList.add(createRecommend(vo.getResource().getResourceID(), orderNum++));
|
// 4.1 按浏览量补足 viewNum 条,采用分页方式逐页向后拿
|
||||||
}
|
int viewPageNumber = 1;
|
||||||
|
int viewPageSize = viewNum; // 每页拉 viewNum 条
|
||||||
|
|
||||||
|
while (viewAdded < viewNum) {
|
||||||
|
List<ResourceVO> viewPageList = getViewCountPage(viewPageNumber, viewPageSize);
|
||||||
|
if (viewPageList == null || viewPageList.isEmpty()) {
|
||||||
|
logger.info("按浏览量补位时已经没有更多候选数据,page={}", viewPageNumber);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加发布时间前10
|
for (ResourceVO vo : viewPageList) {
|
||||||
for (ResourceVO vo : topPublishTimeResources) {
|
if (viewAdded >= viewNum) {
|
||||||
if (vo.getResource() != null && resourceIds.add(vo.getResource().getResourceID())) {
|
break;
|
||||||
recommendList.add(createRecommend(vo.getResource().getResourceID(), orderNum++));
|
|
||||||
}
|
}
|
||||||
|
if (vo.getResource() == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 批量插入新推荐
|
String resourceId = vo.getResource().getResourceID();
|
||||||
if (!recommendList.isEmpty()) {
|
|
||||||
for (TbResourceRecommend recommend : recommendList) {
|
// 已经是热门推荐了,跳过,向后拿下一条
|
||||||
ResultDomain<TbResourceRecommend> addResult = resourceRecommendService.addRecommend(recommend);
|
if (isAlreadyHotRecommend(resourceId)) {
|
||||||
if (!addResult.isSuccess()) {
|
logger.info("资源 {} 已经是热门推荐(按浏览量列表),跳过并向后补位", resourceId);
|
||||||
logger.warn("插入推荐失败: 资源ID={}, 原因={}", recommend.getResourceID(), addResult.getMessage());
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 本次任务中已选过(可能来自另外一个列表),跳过并向后拿
|
||||||
|
if (!resourceIds.add(resourceId)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
logger.info("成功插入{}条热门推荐记录", recommendList.size());
|
|
||||||
} else {
|
recommendList.add(createRecommend(resourceId, orderNum++));
|
||||||
logger.warn("没有找到符合条件的资源,未插入推荐记录");
|
viewAdded++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewPageNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.2 按发布时间补足 timeNum 条,采用分页方式逐页向后拿
|
||||||
|
int timePageNumber = 1;
|
||||||
|
int timePageSize = timeNum; // 每页拉 timeNum 条,基本一两页就足够
|
||||||
|
|
||||||
|
while (timeAdded < timeNum) {
|
||||||
|
List<ResourceVO> pageList = getPublishTimePage(timePageNumber, timePageSize);
|
||||||
|
if (pageList == null || pageList.isEmpty()) {
|
||||||
|
logger.info("按时间补位时已经没有更多候选数据,page={}", timePageNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ResourceVO vo : pageList) {
|
||||||
|
if (timeAdded >= timeNum) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (vo.getResource() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resourceId = vo.getResource().getResourceID();
|
||||||
|
|
||||||
|
// 已经是热门推荐了,跳过,向后拿下一条
|
||||||
|
if (isAlreadyHotRecommend(resourceId)) {
|
||||||
|
logger.info("资源 {} 已经是热门推荐(按时间列表),跳过并向后补位", resourceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本次任务中已选过(例如在浏览量列表中已经加入),跳过并向后拿
|
||||||
|
if (!resourceIds.add(resourceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendList.add(createRecommend(resourceId, orderNum++));
|
||||||
|
timeAdded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
timePageNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("热门资源推荐任务执行成功,共推荐{}个资源", resourceIds.size());
|
logger.info("热门资源推荐任务执行成功,共推荐{}个资源", resourceIds.size());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -104,21 +163,20 @@ public class TopRecommendTask extends BaseTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取浏览量前N的资源
|
* 获取按浏览量排序的一页资源,用于分页向后补位
|
||||||
*/
|
*/
|
||||||
private List<ResourceVO> getTopResourcesByViewCount(int limit) {
|
private List<ResourceVO> getViewCountPage(int pageNumber, int pageSize) {
|
||||||
TbResource filter = new TbResource();
|
TbResource filter = new TbResource();
|
||||||
filter.setStatus(1); // 只查询已发布的资源
|
filter.setStatus(1); // 只查询已发布的资源
|
||||||
|
|
||||||
// 设置排序:按浏览量降序
|
// 设置排序:先按浏览量降序,再按发布时间降序
|
||||||
List<BaseDTO.OrderType> orderTypes = new ArrayList<>();
|
List<BaseDTO.OrderType> orderTypes = new ArrayList<>();
|
||||||
orderTypes.add(new BaseDTO.OrderType("view_count", "DESC"));
|
orderTypes.add(new BaseDTO.OrderType("view_count", "DESC"));
|
||||||
orderTypes.add(new BaseDTO.OrderType("publish_time", "DESC"));
|
orderTypes.add(new BaseDTO.OrderType("publish_time", "DESC"));
|
||||||
filter.setOrderTypes(orderTypes);
|
filter.setOrderTypes(orderTypes);
|
||||||
|
|
||||||
PageParam pageParam = new PageParam();
|
PageParam pageParam = new PageParam();
|
||||||
pageParam.setPageSize(limit);
|
pageParam.setPageSize(pageSize);
|
||||||
pageParam.setOffset(0L);
|
|
||||||
|
|
||||||
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
||||||
if (result.isSuccess() && result.getDataList() != null) {
|
if (result.isSuccess() && result.getDataList() != null) {
|
||||||
@@ -128,9 +186,9 @@ public class TopRecommendTask extends BaseTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取发布时间前N的资源(最新发布)
|
* 获取按发布时间排序的一页资源(最新发布),用于分页向后补位
|
||||||
*/
|
*/
|
||||||
private List<ResourceVO> getTopResourcesByPublishTime(int limit) {
|
private List<ResourceVO> getPublishTimePage(int pageNumber, int pageSize) {
|
||||||
TbResource filter = new TbResource();
|
TbResource filter = new TbResource();
|
||||||
filter.setStatus(1); // 只查询已发布的资源
|
filter.setStatus(1); // 只查询已发布的资源
|
||||||
|
|
||||||
@@ -141,8 +199,7 @@ public class TopRecommendTask extends BaseTask {
|
|||||||
filter.setOrderTypes(orderTypes);
|
filter.setOrderTypes(orderTypes);
|
||||||
|
|
||||||
PageParam pageParam = new PageParam();
|
PageParam pageParam = new PageParam();
|
||||||
pageParam.setPageSize(limit);
|
pageParam.setPageSize(pageSize);
|
||||||
pageParam.setOffset(0L);
|
|
||||||
|
|
||||||
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
||||||
if (result.isSuccess() && result.getDataList() != null) {
|
if (result.isSuccess() && result.getDataList() != null) {
|
||||||
@@ -165,4 +222,19 @@ public class TopRecommendTask extends BaseTask {
|
|||||||
recommend.setDeleted(false);
|
recommend.setDeleted(false);
|
||||||
return recommend;
|
return recommend;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断资源是否已经在热门推荐中(recommend_type = 1)
|
||||||
|
*/
|
||||||
|
private boolean isAlreadyHotRecommend(String resourceId) {
|
||||||
|
try {
|
||||||
|
ResultDomain<Boolean> result = resourceRecommendService.isResourceRecommendedByType(resourceId, 1);
|
||||||
|
if (result != null && result.isSuccess() && result.getData() != null) {
|
||||||
|
return Boolean.TRUE.equals(result.getData());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("检查资源是否已为热门推荐失败, resourceId={}", resourceId, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
schoolNewsWeb/src/components/base/DynamicParamForm.vue
Normal file
191
schoolNewsWeb/src/components/base/DynamicParamForm.vue
Normal 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>
|
||||||
@@ -12,3 +12,4 @@ export { default as GenericSelector } from './GenericSelector.vue';
|
|||||||
export { default as TreeNode } from './TreeNode.vue';
|
export { default as TreeNode } from './TreeNode.vue';
|
||||||
export { default as Notice } from './Notice.vue';
|
export { default as Notice } from './Notice.vue';
|
||||||
export { default as ChangeHome } from './ChangeHome.vue';
|
export { default as ChangeHome } from './ChangeHome.vue';
|
||||||
|
export { default as DynamicParamForm} from './DynamicParamForm.vue'
|
||||||
@@ -185,16 +185,15 @@
|
|||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<span class="form-label required">爬虫模板</span>
|
<span class="form-label required">爬虫模板</span>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedTemplate"
|
v-model="selectedCategory"
|
||||||
placeholder="请选择爬虫模板"
|
placeholder="请选择爬虫模板"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="template in crawlerTemplates"
|
v-for="template in crawlerTemplates"
|
||||||
:key="template.name"
|
:key="template.name"
|
||||||
:label="template.name"
|
:label="template.name"
|
||||||
:value="template"
|
:value="template.name"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<span class="form-tip">
|
<span class="form-tip">
|
||||||
@@ -226,75 +225,10 @@
|
|||||||
<!-- 动态参数表单 -->
|
<!-- 动态参数表单 -->
|
||||||
<div class="form-item" v-if="selectedMethod && selectedMethod.params && selectedMethod.params.length > 0">
|
<div class="form-item" v-if="selectedMethod && selectedMethod.params && selectedMethod.params.length > 0">
|
||||||
<span class="form-label">方法参数</span>
|
<span class="form-label">方法参数</span>
|
||||||
<div class="params-container">
|
<DynamicParamForm
|
||||||
<div v-for="param in selectedMethod.params" :key="param.name" class="param-item">
|
v-model="dynamicParams"
|
||||||
<span class="param-label">
|
:params-schema="selectedMethod.params"
|
||||||
{{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
@@ -405,7 +339,7 @@ import { crontabApi } from '@/apis/crontab';
|
|||||||
import { userApi } from '@/apis/system/user';
|
import { userApi } from '@/apis/system/user';
|
||||||
import type { CrontabTask, TaskMeta, CrontabItem, CrontabMethod, CrontabParam, PageParam, CreateTaskRequest, RecipientUserInfo, UserVO, ResultDomain, EmailDefault } from '@/types';
|
import type { CrontabTask, TaskMeta, CrontabItem, CrontabMethod, CrontabParam, PageParam, CreateTaskRequest, RecipientUserInfo, UserVO, ResultDomain, EmailDefault } from '@/types';
|
||||||
import { AdminLayout } from '@/views/admin';
|
import { AdminLayout } from '@/views/admin';
|
||||||
import { GenericSelector } from '@/components';
|
import { GenericSelector, DynamicParamForm } from '@/components';
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'NewsCrawlerView'
|
name: 'NewsCrawlerView'
|
||||||
});
|
});
|
||||||
@@ -417,11 +351,17 @@ const crawlerList = ref<CrontabTask[]>([]);
|
|||||||
// 爬虫元数据
|
// 爬虫元数据
|
||||||
const taskMetaList = ref<TaskMeta[]>([]);
|
const taskMetaList = ref<TaskMeta[]>([]);
|
||||||
const crawlerTemplates = ref<CrontabItem[]>([]); // 转换后的模板结构
|
const crawlerTemplates = ref<CrontabItem[]>([]); // 转换后的模板结构
|
||||||
const selectedTemplate = ref<CrontabItem | null>(null);
|
const selectedCategory = ref<string>(''); // 选中的模板分类名称
|
||||||
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
|
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
|
||||||
const selectedMetaId = ref<string>(''); // 选中的元数据ID
|
const selectedMetaId = ref<string>(''); // 选中的元数据ID
|
||||||
const dynamicParams = ref<Record<string, any>>({});
|
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 useDefaultRecipients = ref<boolean>(false);
|
||||||
const defaultRecipients = ref<RecipientUserInfo[]>([]);
|
const defaultRecipients = ref<RecipientUserInfo[]>([]);
|
||||||
@@ -512,10 +452,9 @@ const selectedRecipients = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听模板选择变化
|
// 监听模板选择变化
|
||||||
watch(selectedTemplate, (newTemplate, oldTemplate) => {
|
watch(selectedCategory, (newCategory, oldCategory) => {
|
||||||
// 只在用户手动切换模板时重置(oldTemplate存在且不为null时才重置)
|
// 只在用户手动切换模板时重置(oldCategory存在且不为空时才重置)
|
||||||
// 编辑回填时oldTemplate为null,不会触发重置
|
if (newCategory && oldCategory) {
|
||||||
if (newTemplate && oldTemplate) {
|
|
||||||
selectedMethodId.value = '';
|
selectedMethodId.value = '';
|
||||||
dynamicParams.value = {};
|
dynamicParams.value = {};
|
||||||
}
|
}
|
||||||
@@ -732,7 +671,7 @@ async function loadCrawlerTemplates() {
|
|||||||
params: meta.paramSchema ? JSON.parse(meta.paramSchema) : []
|
params: meta.paramSchema ? JSON.parse(meta.paramSchema) : []
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
console.log(" crawlerTemplates.value",crawlerTemplates.value);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result.message || '加载爬虫模板失败');
|
ElMessage.error(result.message || '加载爬虫模板失败');
|
||||||
@@ -801,7 +740,7 @@ function handleSizeChange(size: number) {
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
resetFormData();
|
resetFormData();
|
||||||
selectedTemplate.value = null;
|
selectedCategory.value = '';
|
||||||
selectedMethodId.value = '';
|
selectedMethodId.value = '';
|
||||||
dynamicParams.value = {};
|
dynamicParams.value = {};
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
@@ -813,7 +752,7 @@ async function handleEdit(row: CrontabTask) {
|
|||||||
Object.assign(formData, row);
|
Object.assign(formData, row);
|
||||||
|
|
||||||
// 重置选择
|
// 重置选择
|
||||||
selectedTemplate.value = null;
|
selectedCategory.value = '';
|
||||||
selectedMethodId.value = '';
|
selectedMethodId.value = '';
|
||||||
dynamicParams.value = {};
|
dynamicParams.value = {};
|
||||||
|
|
||||||
@@ -844,7 +783,7 @@ async function handleEdit(row: CrontabTask) {
|
|||||||
const method = template.methods.find(m => m.metaId === row.metaId);
|
const method = template.methods.find(m => m.metaId === row.metaId);
|
||||||
if (method) {
|
if (method) {
|
||||||
// 找到匹配的方法,设置template和method
|
// 找到匹配的方法,设置template和method
|
||||||
selectedTemplate.value = template;
|
selectedCategory.value = template.name;
|
||||||
selectedMethodId.value = method.metaId || '';
|
selectedMethodId.value = method.metaId || '';
|
||||||
selectedMetaId.value = method.metaId || '';
|
selectedMetaId.value = method.metaId || '';
|
||||||
|
|
||||||
|
|||||||
@@ -244,11 +244,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="任务参数" prop="methodParams">
|
<el-form-item label="任务参数" prop="methodParams">
|
||||||
<el-input
|
<DynamicParamForm
|
||||||
v-model="taskForm.methodParams"
|
class="dynamic-params"
|
||||||
type="textarea"
|
v-model="dynamicParams"
|
||||||
:rows="4"
|
:params-schema="currentParamSchema"
|
||||||
placeholder="请输入JSON格式的参数,例如:{}"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -285,6 +284,7 @@ import {
|
|||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import { crontabApi } from '@/apis/crontab';
|
import { crontabApi } from '@/apis/crontab';
|
||||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||||
|
import { DynamicParamForm } from '@/components';
|
||||||
import type { CrontabTask, TaskMeta, PageParam, CreateTaskRequest } from '@/types';
|
import type { CrontabTask, TaskMeta, PageParam, CreateTaskRequest } from '@/types';
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
@@ -306,6 +306,10 @@ const taskList = ref<CrontabTask[]>([]);
|
|||||||
const taskMetaList = ref<TaskMeta[]>([]);
|
const taskMetaList = ref<TaskMeta[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 动态任务参数
|
||||||
|
const dynamicParams = ref<Record<string, any>>({});
|
||||||
|
const currentParamSchema = ref<any[]>([]);
|
||||||
|
|
||||||
// 对话框
|
// 对话框
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const dialogTitle = ref('新增任务');
|
const dialogTitle = ref('新增任务');
|
||||||
@@ -395,6 +399,9 @@ function handleAddTask() {
|
|||||||
methodParams: '{}',
|
methodParams: '{}',
|
||||||
status: 1
|
status: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dynamicParams.value = {};
|
||||||
|
currentParamSchema.value = [];
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +419,25 @@ function handleEdit(task: CrontabTask) {
|
|||||||
methodParams: task.methodParams || '{}',
|
methodParams: task.methodParams || '{}',
|
||||||
status: task.status
|
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;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +447,34 @@ function handleMetaChange(metaId: string) {
|
|||||||
if (meta) {
|
if (meta) {
|
||||||
taskForm.taskName = meta.name;
|
taskForm.taskName = meta.name;
|
||||||
taskForm.description = meta.description;
|
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;
|
if (!valid) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证JSON格式
|
// 根据动态参数生成 methodParams
|
||||||
try {
|
try {
|
||||||
JSON.parse(taskForm.methodParams || '{}');
|
taskForm.methodParams = JSON.stringify(dynamicParams.value || {});
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('任务参数必须是有效的JSON格式');
|
ElMessage.error('任务参数序列化失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +668,9 @@ onMounted(() => {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dynamic-params{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.crawler-list {
|
.crawler-list {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user