搜索关键字爬虫
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type { CrontabTask, CrontabLog, ResultDomain, PageParam } from '@/types';
|
||||
import type { CrontabTask, CrontabLog, DataCollectionItem, CrontabItem, ResultDomain, PageParam } from '@/types';
|
||||
|
||||
/**
|
||||
* 定时任务API服务
|
||||
@@ -14,14 +14,23 @@ export const crontabApi = {
|
||||
baseUrl: '/crontab',
|
||||
|
||||
// ==================== 定时任务管理 ====================
|
||||
|
||||
|
||||
/**
|
||||
* 获取可创建的定时任务模板列表
|
||||
* @returns Promise<ResultDomain<CrontabItem>>
|
||||
*/
|
||||
async getEnabledCrontabList(): Promise<ResultDomain<CrontabItem>> {
|
||||
const response = await api.get<CrontabItem>(`${this.baseUrl}/getEnabledCrontabList`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建定时任务
|
||||
* @param task 任务对象
|
||||
* @returns Promise<ResultDomain<CrontabTask>>
|
||||
*/
|
||||
async createTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||
const response = await api.post<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||
const response = await api.post<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -31,7 +40,7 @@ export const crontabApi = {
|
||||
* @returns Promise<ResultDomain<CrontabTask>>
|
||||
*/
|
||||
async updateTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||
const response = await api.put<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||
const response = await api.put<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -41,7 +50,7 @@ export const crontabApi = {
|
||||
* @returns Promise<ResultDomain<CrontabTask>>
|
||||
*/
|
||||
async deleteTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||
const response = await api.delete<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||
const response = await api.delete<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -72,11 +81,11 @@ export const crontabApi = {
|
||||
* @returns Promise<ResultDomain<CrontabTask>>
|
||||
*/
|
||||
async getTaskPage(filter?: Partial<CrontabTask>, pageParam?: PageParam): Promise<ResultDomain<CrontabTask>> {
|
||||
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/page`, {
|
||||
const response = await api.post<CrontabTask>(`${this.baseUrl}/crontabTaskPage`, {
|
||||
filter,
|
||||
pageParam: {
|
||||
pageNumber: pageParam?.page || 1,
|
||||
pageSize: pageParam?.size || 10
|
||||
pageNumber: pageParam?.pageNumber || 1,
|
||||
pageSize: pageParam?.pageSize || 10
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
@@ -153,11 +162,11 @@ export const crontabApi = {
|
||||
* @returns Promise<ResultDomain<CrontabLog>>
|
||||
*/
|
||||
async getLogPage(filter?: Partial<CrontabLog>, pageParam?: PageParam): Promise<ResultDomain<CrontabLog>> {
|
||||
const response = await api.post<CrontabLog>(`${this.baseUrl}/log/page`, {
|
||||
const response = await api.post<CrontabLog>(`${this.baseUrl}/crontabTaskLogPage`, {
|
||||
filter,
|
||||
pageParam: {
|
||||
pageNumber: pageParam?.page || 1,
|
||||
pageSize: pageParam?.size || 10
|
||||
pageNumber: pageParam?.pageNumber || 1,
|
||||
pageSize: pageParam?.pageSize || 10
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
@@ -191,6 +200,49 @@ export const crontabApi = {
|
||||
async deleteLog(log: CrontabLog): Promise<ResultDomain<CrontabLog>> {
|
||||
const response = await api.delete<CrontabLog>(`${this.baseUrl}/log`, log);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== 数据采集项管理 ====================
|
||||
|
||||
/**
|
||||
* 根据任务日志ID查询数据采集项列表
|
||||
* @param taskLogId 任务日志ID
|
||||
* @returns Promise<ResultDomain<DataCollectionItem>>
|
||||
*/
|
||||
async getCollectionItemsByLogId(taskLogId: string): Promise<ResultDomain<DataCollectionItem>> {
|
||||
const response = await api.get<DataCollectionItem>(`${this.baseUrl}/collection/item/task/${taskLogId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询数据采集项列表
|
||||
* @param filter 过滤条件
|
||||
* @param pageParam 分页参数
|
||||
* @returns Promise<ResultDomain<DataCollectionItem>>
|
||||
*/
|
||||
async getCollectionItemPage(filter?: Partial<DataCollectionItem>, pageParam?: PageParam): Promise<ResultDomain<DataCollectionItem>> {
|
||||
const response = await api.post<DataCollectionItem>(`${this.baseUrl}/collection/item/page`, {
|
||||
filter,
|
||||
pageParam: {
|
||||
pageNumber: pageParam?.pageNumber || 1,
|
||||
pageSize: pageParam?.pageSize || 10
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 转换采集项为资源文章
|
||||
* @param itemId 采集项ID
|
||||
* @param tagId 标签ID
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async convertItemToResource(itemId: string, tagId: string): Promise<ResultDomain<string>> {
|
||||
const response = await api.post<string>(`${this.baseUrl}/collection/item/resource`, {
|
||||
itemId,
|
||||
tagId
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ watch(
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
height: calc(100vh - 76px);
|
||||
min-height: calc(100vh - 76px);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ function handleMenuClick(menu: SysMenu) {
|
||||
|
||||
.main-content-full {
|
||||
background: #F9FAFB;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface CrontabTask extends BaseDTO {
|
||||
* 定时任务执行日志
|
||||
*/
|
||||
export interface CrontabLog extends BaseDTO {
|
||||
/** 日志ID */
|
||||
logId?: string;
|
||||
/** 任务ID */
|
||||
taskId?: string;
|
||||
/** 任务名称 */
|
||||
@@ -90,3 +92,93 @@ export interface NewsCrawlerConfig {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据采集项
|
||||
*/
|
||||
export interface DataCollectionItem extends BaseDTO {
|
||||
/** 采集项ID */
|
||||
itemId?: string;
|
||||
/** 日志ID */
|
||||
logId?: string;
|
||||
/** 任务ID */
|
||||
taskId?: string;
|
||||
/** 任务名称 */
|
||||
taskName?: string;
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 内容(HTML格式) */
|
||||
content?: string;
|
||||
/** 来源URL */
|
||||
sourceUrl?: string;
|
||||
/** 发布时间 */
|
||||
publishTime?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 摘要 */
|
||||
summary?: string;
|
||||
/** 封面图片 */
|
||||
coverImage?: string;
|
||||
/** 分类 */
|
||||
category?: string;
|
||||
/** 来源(人民日报、新华社等) */
|
||||
source?: string;
|
||||
/** 标签(多个用逗号分隔) */
|
||||
tags?: string;
|
||||
/** 图片列表(JSON格式) */
|
||||
images?: string;
|
||||
/** 状态(0:未处理 1:已转换 2:已忽略) */
|
||||
status?: number;
|
||||
/** 转换时间 */
|
||||
convertTime?: string;
|
||||
/** 转换后的资源ID */
|
||||
resourceId?: string;
|
||||
/** 错误信息 */
|
||||
errorMessage?: string;
|
||||
/** 爬取时间 */
|
||||
crawlTime?: string;
|
||||
/** 处理时间 */
|
||||
processTime?: string;
|
||||
/** 处理人 */
|
||||
processor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 爬虫任务参数
|
||||
*/
|
||||
export interface CrontabParam {
|
||||
/** 参数名称 */
|
||||
name: string;
|
||||
/** 参数描述 */
|
||||
description: string;
|
||||
/** 参数类型 */
|
||||
type: string;
|
||||
/** 默认值 */
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 爬虫任务模板方法
|
||||
*/
|
||||
export interface CrontabMethod {
|
||||
/** 方法名称 */
|
||||
name: string;
|
||||
/** Bean类名 */
|
||||
clazz?: string;
|
||||
/** 执行方法名 */
|
||||
excuete_method?: string;
|
||||
/** Python脚本路径 */
|
||||
path: string;
|
||||
/** 参数定义列表 */
|
||||
params?: CrontabParam[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 爬虫任务模板项
|
||||
*/
|
||||
export interface CrontabItem {
|
||||
/** 模板名称 */
|
||||
name: string;
|
||||
/** 可用方法列表 */
|
||||
methods: CrontabMethod[];
|
||||
}
|
||||
|
||||
|
||||
@@ -115,60 +115,152 @@
|
||||
</div>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="执行日志详情"
|
||||
width="700px"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="detail-content" v-if="currentLog">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务名称:</span>
|
||||
<span class="detail-value">{{ currentLog.taskName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务分组:</span>
|
||||
<span class="detail-value">{{ currentLog.taskGroup }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Bean名称:</span>
|
||||
<span class="detail-value">{{ currentLog.beanName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">方法名称:</span>
|
||||
<span class="detail-value">{{ currentLog.methodName }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentLog.methodParams">
|
||||
<span class="detail-label">方法参数:</span>
|
||||
<span class="detail-value">{{ currentLog.methodParams }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行状态:</span>
|
||||
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行时长:</span>
|
||||
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">开始时间:</span>
|
||||
<span class="detail-value">{{ currentLog.startTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">结束时间:</span>
|
||||
<span class="detail-value">{{ currentLog.endTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentLog.executeMessage">
|
||||
<span class="detail-label">执行结果:</span>
|
||||
<div class="detail-message">{{ currentLog.executeMessage }}</div>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentLog.exceptionInfo">
|
||||
<span class="detail-label">异常信息:</span>
|
||||
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
|
||||
</div>
|
||||
<!-- 日志基本信息 -->
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header-title">
|
||||
<span>执行信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务名称:</span>
|
||||
<span class="detail-value">{{ currentLog.taskName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务分组:</span>
|
||||
<span class="detail-value">{{ currentLog.taskGroup }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Bean名称:</span>
|
||||
<span class="detail-value">{{ currentLog.beanName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">方法名称:</span>
|
||||
<span class="detail-value">{{ currentLog.methodName }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentLog.methodParams">
|
||||
<span class="detail-label">方法参数:</span>
|
||||
<span class="detail-value">{{ currentLog.methodParams }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行状态:</span>
|
||||
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行时长:</span>
|
||||
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">开始时间:</span>
|
||||
<span class="detail-value">{{ currentLog.startTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">结束时间:</span>
|
||||
<span class="detail-value">{{ currentLog.endTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item full-width" v-if="currentLog.executeMessage">
|
||||
<span class="detail-label">执行结果:</span>
|
||||
<div class="detail-message">{{ currentLog.executeMessage }}</div>
|
||||
</div>
|
||||
<div class="detail-item full-width" v-if="currentLog.exceptionInfo">
|
||||
<span class="detail-label">异常信息:</span>
|
||||
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 采集的新闻数据 -->
|
||||
<el-card class="detail-card" shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div class="card-header-title">
|
||||
<span>采集数据</span>
|
||||
<el-tag size="small" type="info">共 {{ collectionItems.length }} 条</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="loadingItems">
|
||||
<!-- 无数据提示 -->
|
||||
<el-empty
|
||||
v-if="!loadingItems && collectionItems.length === 0"
|
||||
description="暂无采集数据"
|
||||
:image-size="80"
|
||||
/>
|
||||
|
||||
<!-- 新闻列表 -->
|
||||
<div v-else class="news-list">
|
||||
<div
|
||||
v-for="(item, index) in collectionItems"
|
||||
:key="item.id"
|
||||
class="news-item"
|
||||
>
|
||||
<div class="news-header">
|
||||
<span class="news-index">#{{ index + 1 }}</span>
|
||||
<el-tag
|
||||
v-if="item.status === 0"
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
未处理
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="item.status === 1"
|
||||
type="success"
|
||||
size="small"
|
||||
>
|
||||
已转换
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
type="warning"
|
||||
size="small"
|
||||
>
|
||||
已忽略
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<h4 class="news-title">{{ item.title }}</h4>
|
||||
|
||||
<div class="news-meta">
|
||||
<span v-if="item.source">来源: {{ item.source }}</span>
|
||||
<span v-if="item.author">作者: {{ item.author }}</span>
|
||||
<span v-if="item.publishTime">发布: {{ item.publishTime }}</span>
|
||||
<span v-if="item.category">分类: {{ item.category }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.summary" class="news-summary">
|
||||
{{ item.summary }}
|
||||
</div>
|
||||
|
||||
<div class="news-footer">
|
||||
<el-link
|
||||
v-if="item.sourceUrl"
|
||||
:href="item.sourceUrl"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
>
|
||||
查看原文
|
||||
</el-link>
|
||||
<span v-if="item.crawlTime" class="crawl-time">
|
||||
采集时间: {{ item.crawlTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
@@ -222,7 +314,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Delete, Search, Refresh } from '@element-plus/icons-vue';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import type { CrontabLog, PageParam } from '@/types';
|
||||
import type { CrontabLog, PageParam, DataCollectionItem } from '@/types';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
defineOptions({
|
||||
name: 'LogManagementView'
|
||||
@@ -233,6 +325,8 @@ const submitting = ref(false);
|
||||
const logList = ref<CrontabLog[]>([]);
|
||||
const total = ref(0);
|
||||
const currentLog = ref<CrontabLog | null>(null);
|
||||
const collectionItems = ref<DataCollectionItem[]>([]);
|
||||
const loadingItems = ref(false);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@@ -262,9 +356,18 @@ async function loadLogList() {
|
||||
if (searchForm.executeStatus !== undefined) filter.executeStatus = searchForm.executeStatus;
|
||||
|
||||
const result = await crontabApi.getLogPage(filter, pageParam);
|
||||
if (result.success && result.dataList) {
|
||||
logList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
if (result.success) {
|
||||
// 根据后端返回结构处理数据
|
||||
if (result.pageDomain) {
|
||||
logList.value = result.pageDomain.dataList || [];
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
} else if (result.dataList) {
|
||||
logList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
} else {
|
||||
logList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载日志列表失败');
|
||||
logList.value = [];
|
||||
@@ -310,16 +413,36 @@ function handleSizeChange(size: number) {
|
||||
// 查看详情
|
||||
async function handleViewDetail(row: CrontabLog) {
|
||||
try {
|
||||
const result = await crontabApi.getLogById(row.id!);
|
||||
if (result.success && result.data) {
|
||||
currentLog.value = result.data;
|
||||
detailDialogVisible.value = true;
|
||||
// 同时加载日志详情和采集项数据
|
||||
loadingItems.value = true;
|
||||
collectionItems.value = [];
|
||||
|
||||
const [logResult, itemsResult] = await Promise.all([
|
||||
crontabApi.getLogById(row.id!),
|
||||
crontabApi.getCollectionItemsByLogId(row.id!)
|
||||
]);
|
||||
|
||||
if (logResult.success && logResult.data) {
|
||||
currentLog.value = logResult.data;
|
||||
} else {
|
||||
ElMessage.error(result.message || '获取详情失败');
|
||||
ElMessage.error(logResult.message || '获取日志详情失败');
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemsResult.success) {
|
||||
collectionItems.value = itemsResult.dataList || [];
|
||||
} else {
|
||||
console.warn('获取采集项失败:', itemsResult.message);
|
||||
// 即使采集项加载失败,也显示日志详情
|
||||
collectionItems.value = [];
|
||||
}
|
||||
|
||||
detailDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取日志详情失败:', error);
|
||||
ElMessage.error('获取日志详情失败');
|
||||
} finally {
|
||||
loadingItems.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,42 +555,165 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
.detail-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.detail-label {
|
||||
min-width: 100px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
.card-header-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-message,
|
||||
.detail-exception {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 14px;
|
||||
|
||||
&.full-width {
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: column;
|
||||
|
||||
.detail-label {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: 100px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-message,
|
||||
.detail-exception {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-exception {
|
||||
background-color: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-exception {
|
||||
background-color: #fef0f0;
|
||||
color: #f56c6c;
|
||||
.news-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
.news-item {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #409eff;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #ecf5ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.news-index {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.news-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '|';
|
||||
margin-left: 16px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-summary {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
max-height: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.news-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
|
||||
.crawl-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +55,14 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="!loading && crawlerList.length === 0"
|
||||
description="暂无爬虫配置"
|
||||
style="margin-top: 40px"
|
||||
/>
|
||||
<!-- 爬虫配置列表 -->
|
||||
<div class="crawler-list">
|
||||
<div v-else class="crawler-list">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8" v-for="crawler in crawlerList" :key="crawler.taskId">
|
||||
<el-card class="crawler-card" shadow="hover">
|
||||
@@ -146,13 +151,6 @@
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="!loading && crawlerList.length === 0"
|
||||
description="暂无爬虫配置"
|
||||
style="margin-top: 40px"
|
||||
/>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
@@ -167,8 +165,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑爬虫' : '新增爬虫'"
|
||||
width="700px"
|
||||
@close="resetForm"
|
||||
@@ -176,45 +174,91 @@
|
||||
<div class="form-content">
|
||||
<div class="form-item">
|
||||
<span class="form-label required">爬虫名称</span>
|
||||
<el-input
|
||||
v-model="formData.taskName"
|
||||
<el-input
|
||||
v-model="formData.taskName"
|
||||
placeholder="请输入爬虫名称"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 爬虫模板选择 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label required">Bean名称</span>
|
||||
<el-input
|
||||
v-model="formData.beanName"
|
||||
placeholder="请输入Spring Bean名称(如:newsCrawlerTask)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label required">方法名称</span>
|
||||
<el-input
|
||||
v-model="formData.methodName"
|
||||
placeholder="请输入要执行的方法名(如:crawlNews)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">方法参数</span>
|
||||
<el-input
|
||||
v-model="formData.methodParams"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入方法参数(JSON格式,可选)"
|
||||
clearable
|
||||
/>
|
||||
<span class="form-label required">爬虫模板</span>
|
||||
<el-select
|
||||
v-model="selectedTemplate"
|
||||
placeholder="请选择爬虫模板"
|
||||
style="width: 100%"
|
||||
|
||||
>
|
||||
<el-option
|
||||
v-for="template in crawlerTemplates"
|
||||
:key="template.name"
|
||||
:label="template.name"
|
||||
:value="template"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="form-tip">
|
||||
示例:{"source":"xinhua","category":"education"}
|
||||
选择要使用的新闻爬虫类型
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 爬取方法选择 -->
|
||||
<div class="form-item" v-if="selectedTemplate">
|
||||
<span class="form-label required">爬取方法</span>
|
||||
<el-select
|
||||
v-model="selectedMethod"
|
||||
placeholder="请选择爬取方法"
|
||||
style="width: 100%"
|
||||
|
||||
>
|
||||
<el-option
|
||||
v-for="method in selectedTemplate.methods"
|
||||
:key="method.name"
|
||||
:label="method.name"
|
||||
:value="method"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="form-tip">
|
||||
选择具体的爬取方式
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 动态参数表单 -->
|
||||
<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 === 'String'"
|
||||
v-model="dynamicParams[param.name]"
|
||||
:placeholder="`请输入${param.description}`"
|
||||
clearable
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="param.type === 'Integer'"
|
||||
v-model="dynamicParams[param.name]"
|
||||
:placeholder="`请输入${param.description}`"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="param.type === 'Boolean'"
|
||||
v-model="dynamicParams[param.name]"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="form-label required">Cron表达式</span>
|
||||
<el-input
|
||||
v-model="formData.cronExpression"
|
||||
<el-input
|
||||
v-model="formData.cronExpression"
|
||||
placeholder="请输入Cron表达式"
|
||||
clearable
|
||||
>
|
||||
@@ -231,8 +275,8 @@
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">爬虫描述</span>
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入爬虫描述"
|
||||
@@ -249,11 +293,11 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitting"
|
||||
>
|
||||
@@ -266,11 +310,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Search, Refresh, DocumentCopy, VideoPlay, VideoPause, Promotion, Edit, Delete } from '@element-plus/icons-vue';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import type { CrontabTask, PageParam } from '@/types';
|
||||
import type { CrontabTask, CrontabItem, CrontabMethod, PageParam } from '@/types';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
defineOptions({
|
||||
name: 'NewsCrawlerView'
|
||||
@@ -281,6 +325,12 @@ const submitting = ref(false);
|
||||
const crawlerList = ref<CrontabTask[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
// 爬虫模板数据
|
||||
const crawlerTemplates = ref<CrontabItem[]>([]);
|
||||
const selectedTemplate = ref<CrontabItem | null>(null);
|
||||
const selectedMethod = ref<CrontabMethod | null>(null);
|
||||
const dynamicParams = ref<Record<string, any>>({});
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
taskName: '',
|
||||
@@ -300,7 +350,7 @@ const isEdit = ref(false);
|
||||
// 表单数据
|
||||
const formData = reactive<Partial<CrontabTask>>({
|
||||
taskName: '',
|
||||
taskGroup: 'NEWS_CRAWLER',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
@@ -311,21 +361,65 @@ const formData = reactive<Partial<CrontabTask>>({
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 监听模板选择变化
|
||||
watch(selectedTemplate, (newTemplate) => {
|
||||
if (newTemplate) {
|
||||
selectedMethod.value = null;
|
||||
dynamicParams.value = {};
|
||||
}
|
||||
});
|
||||
|
||||
// 监听方法选择变化
|
||||
watch(selectedMethod, (newMethod) => {
|
||||
if (newMethod) {
|
||||
dynamicParams.value = {};
|
||||
// 遍历params数组提取默认值
|
||||
if (newMethod.params && Array.isArray(newMethod.params)) {
|
||||
newMethod.params.forEach(param => {
|
||||
dynamicParams.value[param.name] = param.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 加载爬虫模板
|
||||
async function loadCrawlerTemplates() {
|
||||
try {
|
||||
const result = await crontabApi.getEnabledCrontabList();
|
||||
if (result.success && result.dataList) {
|
||||
crawlerTemplates.value = result.dataList;
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载爬虫模板失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载爬虫模板失败:', error);
|
||||
ElMessage.error('加载爬虫模板失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载爬虫列表
|
||||
async function loadCrawlerList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const filter: Partial<CrontabTask> = {
|
||||
taskGroup: 'NEWS_CRAWLER'
|
||||
taskGroup: ''
|
||||
};
|
||||
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||
|
||||
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||
if (result.success && result.dataList) {
|
||||
const pageDomain = result.pageDomain!;
|
||||
crawlerList.value = pageDomain.dataList!;
|
||||
total.value = pageDomain.pageParam.totalElements!;
|
||||
if (result.success) {
|
||||
// 根据后端返回结构处理数据
|
||||
if (result.pageDomain) {
|
||||
crawlerList.value = result.pageDomain.dataList || [];
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
} else if (result.dataList) {
|
||||
crawlerList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
} else {
|
||||
crawlerList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载爬虫列表失败');
|
||||
crawlerList.value = [];
|
||||
@@ -371,6 +465,9 @@ function handleSizeChange(size: number) {
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
resetFormData();
|
||||
selectedTemplate.value = null;
|
||||
selectedMethod.value = null;
|
||||
dynamicParams.value = {};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -378,6 +475,32 @@ function handleAdd() {
|
||||
function handleEdit(row: CrontabTask) {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, row);
|
||||
|
||||
// 尝试解析methodParams来回填表单
|
||||
if (row.methodParams) {
|
||||
try {
|
||||
const params = JSON.parse(row.methodParams);
|
||||
// 如果有scriptPath,尝试匹配模板和方法
|
||||
if (params.scriptPath) {
|
||||
const template = crawlerTemplates.value.find(t =>
|
||||
t.methods.some(m => m.path === params.scriptPath)
|
||||
);
|
||||
if (template) {
|
||||
selectedTemplate.value = template;
|
||||
const method = template.methods.find(m => m.path === params.scriptPath);
|
||||
if (method) {
|
||||
selectedMethod.value = method;
|
||||
// 回填动态参数
|
||||
const { scriptPath, ...restParams } = params;
|
||||
dynamicParams.value = restParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('解析methodParams失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -495,12 +618,8 @@ async function handleSubmit() {
|
||||
ElMessage.warning('请输入爬虫名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.beanName) {
|
||||
ElMessage.warning('请输入Bean名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.methodName) {
|
||||
ElMessage.warning('请输入方法名称');
|
||||
if (!selectedTemplate.value || !selectedMethod.value) {
|
||||
ElMessage.warning('请选择爬虫模板和爬取方法');
|
||||
return;
|
||||
}
|
||||
if (!formData.cronExpression) {
|
||||
@@ -508,14 +627,39 @@ async function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填参数
|
||||
if (selectedMethod.value.params && Array.isArray(selectedMethod.value.params)) {
|
||||
for (const param of selectedMethod.value.params) {
|
||||
const value = dynamicParams.value[param.name];
|
||||
|
||||
if (param.type === 'String' && (!value || value.trim() === '')) {
|
||||
ElMessage.warning(`请输入${param.description}`);
|
||||
return;
|
||||
}
|
||||
if (param.type === 'Integer' && (value === undefined || value === null || value === '')) {
|
||||
ElMessage.warning(`请输入${param.description}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const data = {
|
||||
const data = {
|
||||
...formData,
|
||||
taskGroup: 'NEWS_CRAWLER'
|
||||
taskGroup: selectedTemplate.value.name, // 第一层name作为taskGroup
|
||||
methodName: selectedMethod.value.name, // 第二层name作为methodName
|
||||
methodParams: JSON.stringify({
|
||||
scriptPath: selectedMethod.value.path,
|
||||
...dynamicParams.value
|
||||
})
|
||||
};
|
||||
let result;
|
||||
|
||||
console.log('📤 准备提交的数据:', data);
|
||||
console.log('📤 taskGroup (模板名称):', data.taskGroup);
|
||||
console.log('📤 methodName (方法名称):', data.methodName);
|
||||
|
||||
let result;
|
||||
if (isEdit.value) {
|
||||
result = await crontabApi.updateTask(data as CrontabTask);
|
||||
} else {
|
||||
@@ -546,7 +690,7 @@ function resetForm() {
|
||||
function resetFormData() {
|
||||
Object.assign(formData, {
|
||||
taskName: '',
|
||||
taskGroup: 'NEWS_CRAWLER',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
@@ -561,6 +705,7 @@ function resetFormData() {
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadCrawlerList();
|
||||
loadCrawlerTemplates();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -569,7 +714,8 @@ onMounted(() => {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
max-height: 50%;
|
||||
overflow: auto;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -696,6 +842,35 @@ onMounted(() => {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ const isEdit = ref(false);
|
||||
// 表单数据
|
||||
const formData = reactive<Partial<CrontabTask>>({
|
||||
taskName: '',
|
||||
taskGroup: 'DEFAULT',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
@@ -301,9 +301,18 @@ const loadTaskList = async () => {
|
||||
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||
|
||||
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||
if (result.success && result.dataList) {
|
||||
taskList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
if (result.success) {
|
||||
// 根据后端返回结构处理数据
|
||||
if (result.pageDomain) {
|
||||
taskList.value = result.pageDomain.dataList || [];
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
} else if (result.dataList) {
|
||||
taskList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
} else {
|
||||
taskList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载任务列表失败');
|
||||
taskList.value = [];
|
||||
@@ -526,7 +535,7 @@ function resetForm() {
|
||||
function resetFormData() {
|
||||
Object.assign(formData, {
|
||||
taskName: '',
|
||||
taskGroup: 'DEFAULT',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
|
||||
@@ -1,20 +1,654 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
title="资源管理"
|
||||
<AdminLayout
|
||||
title="资源管理"
|
||||
subtitle="管理文章、资源、数据等内容"
|
||||
>
|
||||
<div class="resource-management">
|
||||
<el-empty description="请使用顶部标签页切换到对应的资源管理功能" />
|
||||
<div class="header">
|
||||
<h2>数据采集管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="search-bar">
|
||||
<!-- 任务名称搜索 -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">任务名称</span>
|
||||
<el-input
|
||||
v-model="searchForm.taskName"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 日志批次ID搜索 -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">批次ID</span>
|
||||
<el-input
|
||||
v-model="searchForm.logId"
|
||||
placeholder="请输入批次ID"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 标题搜索 -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">标题</span>
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入标题"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 来源URL搜索 -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">来源URL</span>
|
||||
<el-input
|
||||
v-model="searchForm.sourceUrl"
|
||||
placeholder="请输入URL"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">转换状态</span>
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="未处理" :value="0" />
|
||||
<el-option label="已转换" :value="1" />
|
||||
<el-option label="已忽略" :value="2" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 搜索/重置按钮 -->
|
||||
<div class="search-actions">
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- 任务名称 -->
|
||||
<el-table-column
|
||||
prop="taskName"
|
||||
label="任务名称"
|
||||
width="150"
|
||||
fixed="left"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<!-- 日志批次ID -->
|
||||
<el-table-column
|
||||
prop="logId"
|
||||
label="批次ID"
|
||||
width="100"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<!-- 来源URL -->
|
||||
<el-table-column label="来源URL" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.sourceUrl"
|
||||
:href="row.sourceUrl"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
>
|
||||
{{ truncateUrl(row.sourceUrl) }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 爬虫解析结果 -->
|
||||
<el-table-column label="解析结果" width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="parse-result">
|
||||
<div v-if="row.category" class="result-item">
|
||||
<el-tag size="small" type="info">{{ row.category }}</el-tag>
|
||||
</div>
|
||||
<div v-if="row.source" class="result-item">
|
||||
来源: {{ row.source }}
|
||||
</div>
|
||||
<div v-if="row.tags" class="result-item">
|
||||
标签: {{ row.tags }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 标题 -->
|
||||
<el-table-column
|
||||
prop="title"
|
||||
label="标题"
|
||||
min-width="250"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<!-- 作者 -->
|
||||
<el-table-column
|
||||
prop="author"
|
||||
label="作者"
|
||||
width="100"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<!-- 发布时间 -->
|
||||
<el-table-column label="发布时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.publishTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 转换状态 -->
|
||||
<el-table-column label="转换状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getStatusTagType(row.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleConvert(row)"
|
||||
>
|
||||
转换为资源
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="数据采集详情"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="detail-content" v-if="currentItem">
|
||||
<!-- 基本信息区域 -->
|
||||
<el-descriptions title="基本信息" :column="2" border>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
{{ currentItem.title }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ currentItem.author || '未知' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发布时间">
|
||||
{{ formatDateTime(currentItem.publishTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">
|
||||
{{ currentItem.source || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">
|
||||
{{ currentItem.category || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTagType(currentItem.status)">
|
||||
{{ getStatusText(currentItem.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务名称">
|
||||
{{ currentItem.taskName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源URL" :span="2">
|
||||
<el-link
|
||||
v-if="currentItem.sourceUrl"
|
||||
:href="currentItem.sourceUrl"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
{{ currentItem.sourceUrl }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标签" :span="2">
|
||||
{{ currentItem.tags || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 封面图片 -->
|
||||
<div v-if="currentItem.coverImage" class="cover-section">
|
||||
<h4>封面图片</h4>
|
||||
<el-image
|
||||
:src="currentItem.coverImage"
|
||||
fit="cover"
|
||||
style="width: 200px; height: 150px; border-radius: 4px"
|
||||
:preview-src-list="[currentItem.coverImage]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<div v-if="currentItem.summary" class="summary-section">
|
||||
<h4>摘要</h4>
|
||||
<p>{{ currentItem.summary }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 正文内容 - 使用富文本显示 -->
|
||||
<div v-if="currentItem.content" class="content-section">
|
||||
<h4>正文内容</h4>
|
||||
<div class="content-display" v-html="currentItem.content"></div>
|
||||
</div>
|
||||
|
||||
<!-- 转换信息 -->
|
||||
<div v-if="currentItem.status === 1" class="convert-info">
|
||||
<h4>转换信息</h4>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="资源ID">
|
||||
{{ currentItem.resourceId || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转换时间">
|
||||
{{ formatDateTime(currentItem.processTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理人" :span="2">
|
||||
{{ currentItem.processor || '系统' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="currentItem.status === 2 && currentItem.errorMessage" class="error-info">
|
||||
<h4>错误信息</h4>
|
||||
<el-alert type="error" :closable="false">
|
||||
{{ currentItem.errorMessage }}
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
v-if="currentItem && currentItem.status === 0"
|
||||
type="success"
|
||||
@click="handleConvertFromDetail"
|
||||
>
|
||||
转换为资源
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 转换对话框 - 使用 ArticleAdd 组件 -->
|
||||
<el-dialog
|
||||
v-model="convertDialogVisible"
|
||||
title="转换为资源"
|
||||
width="90%"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
top="5vh"
|
||||
>
|
||||
<ArticleAdd
|
||||
v-if="convertDialogVisible"
|
||||
:initial-data="convertFormData"
|
||||
:show-back-button="false"
|
||||
@publish-success="handleConvertSuccess"
|
||||
@back="convertDialogVisible = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, Refresh } from '@element-plus/icons-vue';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import { ArticleAdd } from '@/views/public/article/components';
|
||||
import type { DataCollectionItem, PageParam, ResourceVO } from '@/types';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
|
||||
defineOptions({
|
||||
name: 'ResourceManagementView'
|
||||
});
|
||||
|
||||
// ==================== 数据状态 ====================
|
||||
const loading = ref(false);
|
||||
const dataList = ref<DataCollectionItem[]>([]);
|
||||
const total = ref(0);
|
||||
const currentItem = ref<DataCollectionItem | null>(null);
|
||||
const convertItem = ref<DataCollectionItem | null>(null);
|
||||
|
||||
// 转换表单数据
|
||||
const convertFormData = ref<ResourceVO>({
|
||||
resource: {},
|
||||
tags: []
|
||||
});
|
||||
|
||||
// ==================== 搜索表单 ====================
|
||||
const searchForm = reactive({
|
||||
taskName: '',
|
||||
logId: '',
|
||||
title: '',
|
||||
sourceUrl: '',
|
||||
status: undefined as number | undefined
|
||||
});
|
||||
|
||||
// ==================== 分页参数 ====================
|
||||
const pageParam = reactive<PageParam>({
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
// ==================== 对话框状态 ====================
|
||||
const detailDialogVisible = ref(false);
|
||||
const convertDialogVisible = ref(false);
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
|
||||
/**
|
||||
* 加载数据采集列表
|
||||
*/
|
||||
async function loadDataList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const filter: Partial<DataCollectionItem> = {};
|
||||
|
||||
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||
if (searchForm.logId) filter.logId = searchForm.logId;
|
||||
if (searchForm.title) filter.title = searchForm.title;
|
||||
if (searchForm.sourceUrl) filter.sourceUrl = searchForm.sourceUrl;
|
||||
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||
|
||||
const result = await crontabApi.getCollectionItemPage(filter, pageParam);
|
||||
|
||||
if (result.success) {
|
||||
if (result.pageDomain) {
|
||||
dataList.value = result.pageDomain.dataList || [];
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
} else if (result.dataList) {
|
||||
dataList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
} else {
|
||||
dataList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载数据失败');
|
||||
dataList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据采集列表失败:', error);
|
||||
ElMessage.error('加载数据失败');
|
||||
dataList.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 搜索操作 ====================
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function handleSearch() {
|
||||
pageParam.pageNumber = 1;
|
||||
loadDataList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置搜索
|
||||
*/
|
||||
function handleReset() {
|
||||
searchForm.taskName = '';
|
||||
searchForm.logId = '';
|
||||
searchForm.title = '';
|
||||
searchForm.sourceUrl = '';
|
||||
searchForm.status = undefined;
|
||||
pageParam.pageNumber = 1;
|
||||
loadDataList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新列表
|
||||
*/
|
||||
function handleRefresh() {
|
||||
loadDataList();
|
||||
}
|
||||
|
||||
// ==================== 分页操作 ====================
|
||||
|
||||
/**
|
||||
* 页码变化
|
||||
*/
|
||||
function handlePageChange(page: number) {
|
||||
pageParam.pageNumber = page;
|
||||
loadDataList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页数量变化
|
||||
*/
|
||||
function handleSizeChange(size: number) {
|
||||
pageParam.pageSize = size;
|
||||
pageParam.pageNumber = 1;
|
||||
loadDataList();
|
||||
}
|
||||
|
||||
// ==================== 详情查看 ====================
|
||||
|
||||
/**
|
||||
* 查看详情
|
||||
*/
|
||||
function handleViewDetail(row: DataCollectionItem) {
|
||||
currentItem.value = row;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// ==================== 转换操作 ====================
|
||||
|
||||
/**
|
||||
* 处理富文本内容,清理不必要的样式
|
||||
*/
|
||||
function cleanHtmlContent(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
// 创建临时DOM元素来处理HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// 移除所有内联样式中的字体大小、字体族等可能导致显示问题的样式
|
||||
const elementsWithStyle = tempDiv.querySelectorAll('[style]');
|
||||
elementsWithStyle.forEach((el) => {
|
||||
const element = el as HTMLElement;
|
||||
const style = element.style;
|
||||
|
||||
// 保留一些重要的样式,移除可能冲突的样式
|
||||
const preservedStyles: string[] = [];
|
||||
|
||||
// 保留文本颜色
|
||||
if (style.color) preservedStyles.push(`color: ${style.color}`);
|
||||
// 保留背景色
|
||||
if (style.backgroundColor) preservedStyles.push(`background-color: ${style.backgroundColor}`);
|
||||
// 保留文本对齐
|
||||
if (style.textAlign) preservedStyles.push(`text-align: ${style.textAlign}`);
|
||||
// 保留边距
|
||||
if (style.marginTop) preservedStyles.push(`margin-top: ${style.marginTop}`);
|
||||
if (style.marginBottom) preservedStyles.push(`margin-bottom: ${style.marginBottom}`);
|
||||
|
||||
element.setAttribute('style', preservedStyles.join('; '));
|
||||
});
|
||||
|
||||
// 移除可能的外部类名,避免样式冲突
|
||||
const elementsWithClass = tempDiv.querySelectorAll('[class]');
|
||||
elementsWithClass.forEach((el) => {
|
||||
el.removeAttribute('class');
|
||||
});
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开转换对话框,预填充数据
|
||||
*/
|
||||
function handleConvert(row: DataCollectionItem) {
|
||||
convertItem.value = row;
|
||||
|
||||
// 处理富文本内容,清理样式
|
||||
const cleanedContent = cleanHtmlContent(row.content || '');
|
||||
|
||||
// 预填充文章数据
|
||||
convertFormData.value = {
|
||||
resource: {
|
||||
title: row.title || '',
|
||||
content: cleanedContent,
|
||||
summary: row.summary || '',
|
||||
coverImage: row.coverImage || '',
|
||||
author: row.author || '',
|
||||
source: row.source || '',
|
||||
sourceUrl: row.sourceUrl || '',
|
||||
publishTime: row.publishTime || new Date().toISOString(),
|
||||
status: 1, // 已发布
|
||||
allowComment: true,
|
||||
isTop: false,
|
||||
isRecommend: false
|
||||
},
|
||||
tags: []
|
||||
};
|
||||
|
||||
convertDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从详情页转换
|
||||
*/
|
||||
function handleConvertFromDetail() {
|
||||
detailDialogVisible.value = false;
|
||||
handleConvert(currentItem.value!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成功后的回调
|
||||
*/
|
||||
function handleConvertSuccess(resourceId: string) {
|
||||
ElMessage.success('转换成功');
|
||||
convertDialogVisible.value = false;
|
||||
|
||||
// 更新采集项状态为已转换
|
||||
if (convertItem.value?.id) {
|
||||
// 这里可以调用API更新状态,或者直接刷新列表
|
||||
loadDataList();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
function formatDateTime(dateTime: string | Date | undefined): string {
|
||||
if (!dateTime) return '-';
|
||||
const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断URL显示
|
||||
*/
|
||||
function truncateUrl(url: string | undefined): string {
|
||||
if (!url) return '-';
|
||||
return url.length > 30 ? url.substring(0, 30) + '...' : url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
function getStatusText(status: number | undefined): string {
|
||||
switch (status) {
|
||||
case 0: return '未处理';
|
||||
case 1: return '已转换';
|
||||
case 2: return '已忽略';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态标签类型
|
||||
*/
|
||||
function getStatusTagType(status: number | undefined): string {
|
||||
switch (status) {
|
||||
case 0: return 'warning';
|
||||
case 1: return 'success';
|
||||
case 2: return 'info';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
loadDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -23,8 +657,184 @@ defineOptions({
|
||||
padding: 24px;
|
||||
border-radius: 14px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #141F38;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.search-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
min-width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格内的解析结果
|
||||
.parse-result {
|
||||
.result-item {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页容器
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 详情对话框样式
|
||||
.detail-content {
|
||||
max-height: 70vh;
|
||||
// overflow-y: auto;
|
||||
|
||||
h4 {
|
||||
margin: 20px 0 10px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
border-left: 4px solid #409eff;
|
||||
padding-left: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
margin-top: 20px;
|
||||
|
||||
p {
|
||||
padding: 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-top: 20px;
|
||||
|
||||
.content-display {
|
||||
padding: 16px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
// 富文本内容样式
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(h1), :deep(h2), :deep(h3),
|
||||
:deep(h4), :deep(h5), :deep(h6) {
|
||||
margin: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
border-left: 4px solid #dcdfe6;
|
||||
padding-left: 12px;
|
||||
color: #909399;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.convert-info,
|
||||
.error-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,6 +125,7 @@ interface Props {
|
||||
articleId?: string;
|
||||
showBackButton?: boolean;
|
||||
backButtonText?: string;
|
||||
initialData?: ResourceVO;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -195,7 +196,7 @@ async function loadCategoryList() {
|
||||
async function loadTagList() {
|
||||
try {
|
||||
tagLoading.value = true;
|
||||
const result = await resourceTagApi.getTagList();
|
||||
const result = await resourceTagApi.getTagList({});
|
||||
if (result.success) {
|
||||
tagList.value = result.dataList || [];
|
||||
} else {
|
||||
@@ -220,13 +221,22 @@ async function handlePublish() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
publishing.value = true;
|
||||
|
||||
const result = await resourceApi.createResource(articleForm.value);
|
||||
if (result.success) {
|
||||
ElMessage.success('发布成功');
|
||||
emit('publish-success', result.data?.resource?.resourceID || '');
|
||||
if (isEdit.value) {
|
||||
const result = await resourceApi.updateResource(articleForm.value);
|
||||
if (result.success) {
|
||||
ElMessage.success('保存成功');
|
||||
emit('publish-success', result.data?.resource?.resourceID || '');
|
||||
} else {
|
||||
ElMessage.error(result.message || '保存失败');
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '发布失败');
|
||||
const result = await resourceApi.createResource(articleForm.value);
|
||||
if (result.success) {
|
||||
ElMessage.success('发布成功');
|
||||
emit('publish-success', result.data?.resource?.resourceID || '');
|
||||
} else {
|
||||
ElMessage.error(result.message || '发布失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
@@ -282,8 +292,17 @@ onMounted(async () => {
|
||||
loadCategoryList(),
|
||||
loadTagList()
|
||||
]);
|
||||
|
||||
// 如果是编辑模式,加载文章数据
|
||||
|
||||
// 如果有初始数据,使用初始数据填充表单
|
||||
if (props.initialData) {
|
||||
articleForm.value = {
|
||||
resource: { ...props.initialData.resource },
|
||||
tags: [...(props.initialData.tags || [])]
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是编辑模式,加载文章数据
|
||||
if (props.articleId) {
|
||||
try {
|
||||
isEdit.value = true;
|
||||
|
||||
Reference in New Issue
Block a user