Commit 4c265c8e by liucan

feat:完成卫星编号弹窗相关功能,修复部分爆红组件

parent 0b581533
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"@types/node": "^22.15.17", "@types/node": "^22.15.17",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"animate.css": "^4.1.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"cron-parser": "^5.4.0", "cron-parser": "^5.4.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
...@@ -2293,6 +2294,12 @@ ...@@ -2293,6 +2294,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"license": "MIT"
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"@types/node": "^22.15.17", "@types/node": "^22.15.17",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"animate.css": "^4.1.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"cron-parser": "^5.4.0", "cron-parser": "^5.4.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
</template> </template>
<style> <style>
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
......
// API路径配置文件 // API路径配置文件
// 这个文件确保所有API路径在一处统一管理,方便修改 // 这个文件确保所有API路径在一处统一管理,方便修改
import { pauseJob } from "./schedule" import { pauseJob } from "./schedule";
export const authApi = { export const authApi = {
login: '/auth/login', login: "/auth/login",
logout: '/auth/logout' logout: "/auth/logout",
} as const } as const;
export const scrapydApi = { export const scrapydApi = {
// scrapyd相关接口 // scrapyd相关接口
listProjects: '/scrapyd/listProjects', listProjects: "/scrapyd/listProjects",
listVersions: '/scrapyd/listVersions', listVersions: "/scrapyd/listVersions",
listSpiders: '/scrapyd/listSpiders', listSpiders: "/scrapyd/listSpiders",
listJobsMerge: '/scrapyd/listJobsMerge', listJobsMerge: "/scrapyd/listJobsMerge",
cancel: '/scrapyd/cancel', cancel: "/scrapyd/cancel",
cancelAllJob: '/scrapyd/cancelAllJob', cancelAllJob: "/scrapyd/cancelAllJob",
deleteVersion: '/scrapyd/deleteVersion', deleteVersion: "/scrapyd/deleteVersion",
deleteProject: '/scrapyd/deleteProject', deleteProject: "/scrapyd/deleteProject",
schedule: '/scrapyd/schedule', schedule: "/scrapyd/schedule",
daemonStatus: '/scrapyd/daemonStatus', daemonStatus: "/scrapyd/daemonStatus",
addVersion: '/scrapyd/addVersion', addVersion: "/scrapyd/addVersion",
// 日志相关 // 日志相关
logs: '/scrapyd/logs', logs: "/scrapyd/logs",
projectLogs: '/scrapyd/projectLogs', projectLogs: "/scrapyd/projectLogs",
spiderLogs: '/scrapyd/spiderLogs', spiderLogs: "/scrapyd/spiderLogs",
jobLog: '/scrapyd/jobLog' jobLog: "/scrapyd/jobLog",
} as const } as const;
export const scheduleApi = { export const scheduleApi = {
// 调度任务相关 // 调度任务相关
addJob: '/schedule/addJob', addJob: "/schedule/addJob",
getJobs: '/schedule/getJobs', getJobs: "/schedule/getJobs",
pauseJob: '/schedule/pauseJob', pauseJob: "/schedule/pauseJob",
resumeJob: '/schedule/resumeJob', resumeJob: "/schedule/resumeJob",
removeJob: '/schedule/removeJob', removeJob: "/schedule/removeJob",
jobDetail: '/schedule/jobDetail', jobDetail: "/schedule/jobDetail",
state: '/schedule/state', state: "/schedule/state",
start: '/schedule/start', start: "/schedule/start",
shutdown: '/schedule/shutdown', shutdown: "/schedule/shutdown",
pause: '/schedule/pause', pause: "/schedule/pause",
resume: '/schedule/resume', resume: "/schedule/resume",
removeAllJobs: '/schedule/removeAllJobs', removeAllJobs: "/schedule/removeAllJobs",
scheduleLogs: '/schedule/scheduleLogs', scheduleLogs: "/schedule/scheduleLogs",
removeScheduleLogs: '/schedule/removeScheduleLogs', removeScheduleLogs: "/schedule/removeScheduleLogs",
} as const } as const;
export const scrapydServerApi = { export const scrapydServerApi = {
getScrapydServerPage: '/scrapydServer/getScrapydServerPage', getScrapydServerPage: "/scrapydServer/getScrapydServerPage",
addScrapydServer: '/scrapydServer/addScrapydServer', addScrapydServer: "/scrapydServer/addScrapydServer",
updateScrapydServer: '/scrapydServer/updateScrapydServer', updateScrapydServer: "/scrapydServer/updateScrapydServer",
updateScrapydServerStatus: '/scrapydServer/updateScrapydServerStatus', updateScrapydServerStatus: "/scrapydServer/updateScrapydServerStatus",
deleteScrapydServer: '/scrapydServer/deleteScrapydServer' deleteScrapydServer: "/scrapydServer/deleteScrapydServer",
} as const } as const;
// 系统相关 // 系统相关
export const systemApi = { export const systemApi = {
systemInfo: '/system/systemInfo', systemInfo: "/system/systemInfo",
systemData: '/system/systemData', systemData: "/system/systemData",
systemConfig: '/system/systemConfig', systemConfig: "/system/systemConfig",
loginHistoryList: '/actionHistory/loginHistoryList', loginHistoryList: "/actionHistory/loginHistoryList",
scrapydServerList: '/system/scrapydServerList' // 没用 scrapydServerList: "/system/scrapydServerList", // 没用
} as const } as const;
// 统计相关 // 统计相关
export const statsApi = { export const statsApi = {
statsList: '/statsCollection/listItem', statsList: "/statsCollection/listItem",
statsDetail: '/statsCollection/detail', statsDetail: "/statsCollection/detail",
removeStats: '/statsCollection/delete', removeStats: "/statsCollection/delete",
clearAllStats: '/statsCollection/clearAll' // TODO 未实现 clearAllStats: "/statsCollection/clearAll", // TODO 未实现
} as const } as const;
// 系统用户相关接口 // 系统用户相关接口
export const userApi = { export const userApi = {
userList: '/user/list', userList: "/user/list",
addUser: '/user/insert', addUser: "/user/insert",
updateUser: '/user/update', updateUser: "/user/update",
deleteUser: '/user/delete', deleteUser: "/user/delete",
batchDelete: '/user/batchDelete', batchDelete: "/user/batchDelete",
} as const } as const;
// 系统用户相关接口 // 系统用户相关接口
export const spiderApi = { export const spiderApi = {
spiderList: '/scrapyd/listSpiders', spiderList: "/scrapyd/listSpiders",
} as const } as const;
// 爬虫任务相关接口 // 爬虫任务相关接口
export const spiderTaskApi = { export const spiderTaskApi = {
taskList: '/schedule/getJobs', taskList: "/schedule/getJobs",
taskRecord: '/schedule/scheduleLogs', taskRecord: "/schedule/scheduleLogs",
addTask: '/schedule/addJob', addTask: "/schedule/addJob",
deleteTask: '/schedule/removeJob', deleteTask: "/schedule/removeJob",
pauseJob: '/schedule/pauseJob', pauseJob: "/schedule/pauseJob",
resumeJob: '/schedule/resumeJob', resumeJob: "/schedule/resumeJob",
jobDetail: '/schedule/jobDetail', jobDetail: "/schedule/jobDetail",
taskCount:'/schedule/getAllJobCount' taskCount: "/schedule/getAllJobCount",
} as const addSateNo: "/schedule/resetNoradID",
getSateIdList: "/schedule/getNoradIDList",
// 爬虫数据相关接口 } as const;
// 爬虫数据相关接口
export const spiderDataApi = { export const spiderDataApi = {
dsnList: '/dsn/list', dsnList: "/dsn/list",
dsnDetail: '/dsn/detail', dsnDetail: "/dsn/detail",
dsnDataDelete: '/dsn/delete', dsnDataDelete: "/dsn/delete",
ituList: '/itu/list', ituList: "/itu/list",
ituDetail: '/itu/detail', ituDetail: "/itu/detail",
ituDataDelete: '/itu/delete', ituDataDelete: "/itu/delete",
stList: '/spaceTrack/list', stList: "/spaceTrack/list",
stDetail: '/spaceTrack/detail', stDetail: "/spaceTrack/detail",
stDataDelete: '/sspaceTrackt/delete', stDataDelete: "/sspaceTrackt/delete",
exportSpiderData: '/export/downloadFile', exportSpiderData: "/export/downloadFile",
esaList: '/esa/list', esaList: "/esa/list",
esaMissionDetail: '/esa/missionDetail', esaMissionDetail: "/esa/missionDetail",
esaStationDetail: '/esa/stationDetail', esaStationDetail: "/esa/stationDetail",
} as const;
} as const
// 爬虫数据相关接口
// 爬虫数据相关接口
export const dataApi = { export const dataApi = {
dataStatistics: '/statistic/dataStatistics', dataStatistics: "/statistic/dataStatistics",
taskStatistics: '/statistic/taskStatistics', taskStatistics: "/statistic/taskStatistics",
performanceStatistics: '/statistic/performanceStatistics', performanceStatistics: "/statistic/performanceStatistics",
allSpiderTaskStatistics: '/statistic/getAllTaskStatistics', allSpiderTaskStatistics: "/statistic/getAllTaskStatistics",
} as const } as const;
import { request, POST } from '@/utils/request' import { request, POST } from "@/utils/request";
import type { ApiResponse, QueryParams ,UserQueryParams } from '@/utils/request' import type { ApiResponse, QueryParams, UserQueryParams } from "@/utils/request";
import { systemApi, scrapydServerApi, spiderApi } from './apiPaths' import { systemApi, scrapydServerApi, spiderApi, spiderTaskApi } from "./apiPaths";
// 获取系统信息 // 获取系统信息
export function getSystemInfo(): Promise<ApiResponse> { export function getSystemInfo(): Promise<ApiResponse> {
return request({ return request({
url: systemApi.systemInfo, url: systemApi.systemInfo,
method: POST method: POST,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取系统数据 // 获取系统数据
...@@ -15,16 +15,16 @@ export function getSystemData(data: QueryParams): Promise<ApiResponse> { ...@@ -15,16 +15,16 @@ export function getSystemData(data: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: systemApi.systemData, url: systemApi.systemData,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取系统配置 // 获取系统配置
export function getSystemConfig(): Promise<ApiResponse> { export function getSystemConfig(): Promise<ApiResponse> {
return request({ return request({
url: systemApi.systemConfig, url: systemApi.systemConfig,
method: POST method: POST,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 更新系统配置 // 更新系统配置
...@@ -32,8 +32,8 @@ export function updateSystemConfig(data: Record<string, any>): Promise<ApiRespon ...@@ -32,8 +32,8 @@ export function updateSystemConfig(data: Record<string, any>): Promise<ApiRespon
return request({ return request({
url: systemApi.systemConfig, url: systemApi.systemConfig,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取登录历史 // 获取登录历史
...@@ -41,22 +41,39 @@ export function getLoginHistory(params: QueryParams): Promise<ApiResponse> { ...@@ -41,22 +41,39 @@ export function getLoginHistory(params: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: systemApi.loginHistoryList, url: systemApi.loginHistoryList,
method: POST, method: POST,
params params,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
export const getScrapydServerList = (): Promise<ApiResponse> => { export const getScrapydServerList = (): Promise<ApiResponse> => {
return request({ return request({
url: scrapydServerApi.getScrapydServerPage, url: scrapydServerApi.getScrapydServerPage,
method: POST, method: POST,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} };
// 爬虫列表 // 爬虫列表
export function getSpiderList(data: UserQueryParams) { export function getSpiderList(data: UserQueryParams) {
return request({ return request({
url: spiderApi.spiderList, url: spiderApi.spiderList,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
\ No newline at end of file
//添加卫星编号
export function addSateNo(data: { id: string[] }) {
return request({
url: spiderTaskApi.addSateNo,
method: POST,
data,
}) as unknown as Promise<ApiResponse>;
}
//获取编号列表
export function getSateIdList() {
return request({
url: spiderTaskApi.getSateIdList,
method: POST,
}) as unknown as Promise<ApiResponse>;
}
<template> <template>
<div> <div>
<el-dialog v-model="exportDialogVisible" v-loading="fullscreenLoading" title="导出" width="500" center align-center <el-dialog v-model="exportDialogVisible" title="导出" width="500" center align-center @close="close" draggable>
@close="close" draggable> <div class="dialog-content" v-loading="fullscreenLoading">
<el-form class="formStyle" :rules="rules" ref="formRef" :model="exportObject"> <el-form class="formStyle" :rules="rules" ref="formRef" :model="exportObject">
<el-form-item prop="timeValue"> <el-form-item prop="timeValue">
<el-date-picker v-model="exportObject.timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss" <el-date-picker
start-placeholder="开始时间" end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" v-model="exportObject.timeValue"
value-format="YYYY-MM-DD HH:mm:ss" id="export-time-picker"/> type="datetimerange"
</el-form-item> format="YYYY-MM-DD HH:mm:ss"
<el-form-item prop="spiderType"> start-placeholder="开始时间"
<el-checkbox-group v-model="exportObject.spiderType" class="checkboxStyle"> end-placeholder="结束时间"
<el-checkbox label="DSN数据" value="dsn" id="dsn-checkbox"/> date-format="YYYY/MM/DD ddd"
<el-checkbox label="ITU数据" value="itu" id="itu-checkbox"/> time-format="A hh:mm:ss"
<el-checkbox label="ST数据" value="spaceTrack" id="st-checkbox"/> value-format="YYYY-MM-DD HH:mm:ss"
<el-checkbox label="ESA数据" value="esa" id="st-checkbox"/> />
</el-form-item>
</el-checkbox-group> <el-form-item prop="spiderType">
</el-form-item> <el-checkbox-group v-model="exportObject.spiderType" class="checkboxStyle">
</el-form> <el-checkbox label="DSN数据" value="dsn" id="dsn-checkbox" />
<template #footer> <el-checkbox label="ITU数据" value="itu" id="itu-checkbox" />
<div class="dialog-footer"> <el-checkbox label="ST数据" value="spaceTrack" id="st-checkbox" />
<el-button @click="close">取消</el-button> <el-checkbox label="ESA数据" value="esa" id="esa-checkbox" />
<el-button type="primary" @click="handleExport"> </el-checkbox-group>
确定 </el-form-item>
</el-button> </el-form>
</div> </div>
</template> <template #footer>
</el-dialog> <div class="dialog-footer">
</div> <el-button @click="close">取消</el-button>
<el-button type="primary" @click="handleExport"> 确定 </el-button>
</div>
</template>
</el-dialog>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue' import { ref, watch } from "vue";
import { defineProps } from 'vue'; import { defineProps } from "vue";
import axios from 'axios' import axios from "axios";
import { ElLoading, ElMessage } from 'element-plus' import { ElLoading, ElMessage } from "element-plus";
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from "element-plus";
interface RuleForm { interface RuleForm {
//表单字段的类型 //表单字段的类型
timeValue: string timeValue: string;
spiderType: Array<string> spiderType: Array<string>;
} }
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>();
const rules = ref<FormRules<RuleForm>>({ const rules = ref<FormRules<RuleForm>>({
timeValue: [ timeValue: [
{ {
// type: 'date', // type: 'date',
required: true, required: true,
message: '请选择时间段', message: "请选择时间段",
trigger: 'change', trigger: "change",
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
// 检查是否为数组且长度为2 // 检查是否为数组且长度为2
if (!Array.isArray(value) || value.length !== 2) { if (!Array.isArray(value) || value.length !== 2) {
callback(new Error('请选择完整的时间段')); callback(new Error("请选择完整的时间段"));
} else { } else {
// 检查日期格式是否有效(可选) // 检查日期格式是否有效(可选)
const isValid = value.every(date => /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)); const isValid = value.every((date) => /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date));
if (!isValid) { if (!isValid) {
callback(new Error('时间段格式不正确')); callback(new Error("时间段格式不正确"));
} else { } else {
callback(); callback();
} }
} }
} },
}, },
], ],
spiderType: [ spiderType: [
{ {
type: 'array', type: "array",
required: true, required: true,
message: '请至少选择一种数据类型', message: "请至少选择一种数据类型",
trigger: 'change', trigger: "change",
}, },
] ],
}) });
//表单绑定的字段 //表单绑定的字段
const exportObject = ref({ const exportObject = ref({
timeValue: [], timeValue: [],
spiderType: [] spiderType: [],
}) });
const fullscreenLoading = ref(false) const fullscreenLoading = ref(false);
const props = defineProps({ const props = defineProps({
dialogVisible: { dialogVisible: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}) });
const emit = defineEmits(['update:dialogVisible']) const emit = defineEmits(["update:dialogVisible"]);
const exportDialogVisible = ref(props.dialogVisible) const exportDialogVisible = ref(props.dialogVisible);
// 导出方法 // 导出方法
// const handleExport = async () => { // const handleExport = async () => {
...@@ -145,7 +150,6 @@ const exportDialogVisible = ref(props.dialogVisible) ...@@ -145,7 +150,6 @@ const exportDialogVisible = ref(props.dialogVisible)
// } // }
// }) // })
// } // }
// const handleExport = async () => { // const handleExport = async () => {
...@@ -203,135 +207,136 @@ const exportDialogVisible = ref(props.dialogVisible) ...@@ -203,135 +207,136 @@ const exportDialogVisible = ref(props.dialogVisible)
// } // }
const handleExport = async () => { const handleExport = async () => {
if (!formRef.value) return; if (!formRef.value) return;
try { try {
// 1. 先在用户点击时获取保存文件句柄(关键:在用户手势中执行) // 1. 先在用户点击时获取保存文件句柄(关键:在用户手势中执行)
const fileHandle = await getSaveFileHandle(); const fileHandle = await getSaveFileHandle();
if (!fileHandle) { if (!fileHandle) {
ElMessage.info('已取消保存'); ElMessage.info("已取消保存");
return; return;
} }
// 2. 执行表单验证 // 2. 执行表单验证
await formRef.value.validate(async (valid, fields) => { await formRef.value.validate(async (valid, fields) => {
if (valid) { if (valid) {
const loading = ElLoading.service({ const loading = ElLoading.service({
lock: true, lock: true,
text: '正在导出文件...', text: "正在导出文件...",
background: 'rgba(0, 0, 0, 0.7)', background: "rgba(0, 0, 0, 0.7)",
}); });
try { try {
// 3. 请求文件数据 // 3. 请求文件数据
const res = await axios.post( const res = await axios.post(
'/api/export/downloadFile', "/api/export/downloadFile",
{ times: exportObject.value.timeValue, filters: exportObject.value.spiderType }, { times: exportObject.value.timeValue, filters: exportObject.value.spiderType },
{ {
responseType: 'blob', responseType: "blob",
headers: { headers: {
'Token': localStorage.getItem('Admin-Token') || '', Token: localStorage.getItem("Admin-Token") || "",
}, },
} }
); );
// 4. 使用之前获取的句柄保存文件 // 4. 使用之前获取的句柄保存文件
await saveToSelectedLocation(res.data, fileHandle); await saveToSelectedLocation(res.data, fileHandle);
loading.close(); loading.close();
ElMessage.success('数据导出成功'); ElMessage.success("数据导出成功");
close() close();
} catch (e) { } catch (e) {
console.error('导出失败', e); console.error("导出失败", e);
loading.close(); loading.close();
ElMessage.error('ZIP文件导出失败,请重试'); ElMessage.error("ZIP文件导出失败,请重试");
} }
} }
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
// ElMessage.error('无法打开保存对话框,请使用浏览器默认下载'); // ElMessage.error('无法打开保存对话框,请使用浏览器默认下载');
} }
}; };
// 获取用户选择的保存位置 // 获取用户选择的保存位置
const getSaveFileHandle = async () => { const getSaveFileHandle = async () => {
// 检查浏览器是否支持 // 检查浏览器是否支持
if (!(window as any).showSaveFilePicker) { if (!(window as any).showSaveFilePicker) {
ElMessage.warning('您的浏览器不支持选择保存位置,请使用最新版Chrome或Edge浏览器'); ElMessage.warning("您的浏览器不支持选择保存位置,请使用最新版Chrome或Edge浏览器");
return null; return null;
} }
try { try {
// 弹出保存对话框(必须在用户直接交互中调用) // 弹出保存对话框(必须在用户直接交互中调用)
return await (window as any).showSaveFilePicker({ return await (window as any).showSaveFilePicker({
suggestedName: 'export.zip', suggestedName: "export.zip",
types: [{ types: [
description: 'ZIP压缩文件', {
accept: { 'application/zip': ['.zip'] }, description: "ZIP压缩文件",
}], accept: { "application/zip": [".zip"] },
excludeAcceptAllOption: true },
}); ],
} catch (error: any) { excludeAcceptAllOption: true,
// 用户取消选择时也会触发错误,这里视为正常取消 });
if (error.name !== 'AbortError') { } catch (error: any) {
console.error('获取保存位置失败', error); // 用户取消选择时也会触发错误,这里视为正常取消
} if (error.name !== "AbortError") {
return null; console.error("获取保存位置失败", error);
} }
return null;
}
}; };
// 保存数据到用户选择的位置 // 保存数据到用户选择的位置
const saveToSelectedLocation = async (blobData: any, fileHandle: any) => { const saveToSelectedLocation = async (blobData: any, fileHandle: any) => {
if (!(blobData instanceof Blob) || blobData.size === 0) { if (!(blobData instanceof Blob) || blobData.size === 0) {
throw new Error('无效的ZIP文件数据'); throw new Error("无效的ZIP文件数据");
} }
// 将blob转换为可写数据 // 将blob转换为可写数据
const arrayBuffer = await blobData.arrayBuffer(); const arrayBuffer = await blobData.arrayBuffer();
// 写入到用户选择的文件 // 写入到用户选择的文件
const writable = await fileHandle.createWritable(); const writable = await fileHandle.createWritable();
await writable.write(arrayBuffer); await writable.write(arrayBuffer);
await writable.close(); await writable.close();
}; };
// 关闭弹窗的方法 // 关闭弹窗的方法
const close = () => { const close = () => {
exportObject.value = { exportObject.value = {
timeValue: [], timeValue: [],
spiderType: [] spiderType: [],
} };
formRef.value?.clearValidate() formRef.value?.clearValidate();
exportDialogVisible.value = false exportDialogVisible.value = false;
} };
// 监听父组件传过来的值 // 监听父组件传过来的值
watch(() => props.dialogVisible, watch(
(newVal) => { () => props.dialogVisible,
exportDialogVisible.value = newVal (newVal) => {
} exportDialogVisible.value = newVal;
) }
);
// 监听组件内的值并向父组件更新 // 监听组件内的值并向父组件更新
watch(() => exportDialogVisible.value, watch(
(newVal) => { () => exportDialogVisible.value,
emit('update:dialogVisible', newVal) (newVal) => {
} emit("update:dialogVisible", newVal);
) }
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.formStyle { .formStyle {
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.checkboxStyle { .checkboxStyle {
display: flex; display: flex;
gap: 30px; gap: 30px;
/* 复选框之间的间距 */ /* 复选框之间的间距 */
} }
</style> </style>
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue";
import { useRoute } from 'vue-router' import { useRoute } from "vue-router";
const route = useRoute() const route = useRoute();
const key = computed(() => route.path) const key = computed(() => route.path);
</script> </script>
<template> <template>
...@@ -28,7 +28,7 @@ const key = computed(() => route.path) ...@@ -28,7 +28,7 @@ const key = computed(() => route.path)
overflow-y: auto; /* 修改为auto允许垂直滚动 */ overflow-y: auto; /* 修改为auto允许垂直滚动 */
overflow-x: hidden; /* 防止水平滚动 */ overflow-x: hidden; /* 防止水平滚动 */
} }
.fixed-header+.app-main { .fixed-header + .app-main {
padding-top: 50px; padding-top: 50px;
} }
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import settings from '@/settings' import settings from "@/settings";
const props = defineProps({ const props = defineProps({
collapse: { collapse: {
type: Boolean, type: Boolean,
required: true required: true,
} },
}) });
const title = settings.title || 'fk spider web' const title = settings.title || "fk spider web";
// Logo 图片可在后续使用实际的 logo 文件 // Logo 图片可在后续使用实际的 logo 文件
// const logo = require('@/assets/logo.png') // const logo = require('@/assets/logo.png')
const logo = '' // 暂时为空 const logo = ""; // 暂时为空
</script> </script>
<template> <template>
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
<div class="sidebar-logo-container" :class="{'collapse': collapse}">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" alt="logo"> <img v-if="logo" :src="logo" class="sidebar-logo" alt="logo" />
<h1 v-else class="sidebar-title">{{ title }}</h1> <h1 v-else class="sidebar-title">{{ title }}</h1>
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" alt="logo"> <img v-if="logo" :src="logo" class="sidebar-logo" alt="logo" />
<h1 class="sidebar-title">{{ title }}</h1> <h1 class="sidebar-title">{{ title }}</h1>
</router-link> </router-link>
</transition> </transition>
......
import { createApp } from 'vue' import { createApp } from "vue";
import 'virtual:uno.css' import "virtual:uno.css";
import App from './App.vue' import App from "./App.vue";
import router from './router' import router from "./router";
import pinia from './store' import pinia from "./store";
import ElementPlus from 'element-plus' import ElementPlus from "element-plus";
import 'element-plus/dist/index.css' import "element-plus/dist/index.css";
import { import {
UserFilled, UserFilled,
Lock, Lock,
...@@ -31,46 +31,47 @@ import { ...@@ -31,46 +31,47 @@ import {
Message, Message,
DataAnalysis, DataAnalysis,
Notebook, Notebook,
Setting Setting,
} from '@element-plus/icons-vue' } from "@element-plus/icons-vue";
import 'normalize.css/normalize.css' import "normalize.css/normalize.css";
import './styles/index.scss' import "./styles/index.scss";
import './style.css' import "./style.css";
import './permission' // 权限控制 import "./permission"; // 权限控制
import "animate.css";
const app = createApp(App) const app = createApp(App);
// 注册 Element Plus 图标 // 注册 Element Plus 图标
app.component('Monitor', Monitor) app.component("Monitor", Monitor);
app.component('Document', Document) app.component("Document", Document);
app.component('List', List) app.component("List", List);
app.component('Key', Key) app.component("Key", Key);
app.component('House', House) app.component("House", House);
app.component('Clock', Clock) app.component("Clock", Clock);
app.component('Message', Message) app.component("Message", Message);
app.component('DataAnalysis', DataAnalysis) app.component("DataAnalysis", DataAnalysis);
app.component('Notebook', Notebook) app.component("Notebook", Notebook);
app.component('Setting', Setting) app.component("Setting", Setting);
app.component('UserFilled', UserFilled) app.component("UserFilled", UserFilled);
app.component('Lock', Lock) app.component("Lock", Lock);
app.component('View', View) app.component("View", View);
app.component('Hide', Hide) app.component("Hide", Hide);
app.component('CaretBottom', CaretBottom) app.component("CaretBottom", CaretBottom);
app.component('HomeFilled', HomeFilled) app.component("HomeFilled", HomeFilled);
app.component('Menu', Menu) app.component("Menu", Menu);
app.component('Tickets', Tickets) app.component("Tickets", Tickets);
app.component('Folder', Folder) app.component("Folder", Folder);
app.component('Files', Files) app.component("Files", Files);
app.component('Loading', Loading) app.component("Loading", Loading);
app.component('VideoPlay', VideoPlay) app.component("VideoPlay", VideoPlay);
app.component('Refresh', Refresh) app.component("Refresh", Refresh);
app.component('Back', Back) app.component("Back", Back);
app.component('Upload', Upload) app.component("Upload", Upload);
app.component('Plus', Plus) app.component("Plus", Plus);
app.use(router) app.use(router);
app.use(pinia) app.use(pinia);
app.use(ElementPlus, { size: 'default' }) app.use(ElementPlus, { size: "default" });
app.mount('#app') app.mount("#app");
...@@ -58,3 +58,7 @@ body { ...@@ -58,3 +58,7 @@ body {
display: table; display: table;
clear: both; clear: both;
} }
:root {
--animate-duration: 0.15s !important;
}
\ No newline at end of file
import axios from 'axios' import axios from "axios";
import { ElMessage } from 'element-plus' import { ElMessage } from "element-plus";
import { getToken, removeToken } from '@/utils/auth' import { getToken, removeToken } from "@/utils/auth";
// 创建axios实例 // 创建axios实例
const instance = axios.create({ const instance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API || '', baseURL: import.meta.env.VITE_APP_BASE_API || "",
timeout: 10000, // 毫秒 timeout: 10000, // 毫秒
headers: { headers: {
'Content-Type': 'application/zip; charset=utf-8' "Content-Type": "application/zip; charset=utf-8",
} },
}) });
// 请求拦截器 // 请求拦截器
instance.interceptors.request.use( instance.interceptors.request.use(
(config) => { (config) => {
const token = getToken() const token = getToken();
if (token) { if (token) {
// 保持与原项目相同的Token头设置 // 保持与原项目相同的Token头设置
config.headers['Token'] = token config.headers["Token"] = token;
} }
return config return config;
}, },
(err) => { (err) => {
console.log('请求出错') console.log("请求出错");
return Promise.reject(err) return Promise.reject(err);
} }
) );
// 响应拦截器 // 响应拦截器
instance.interceptors.response.use( instance.interceptors.response.use(
(res) => { (res) => {
let result = res.data let result = res.data;
if (typeof res.data === 'string') { if (typeof res.data === "string") {
try { try {
result = JSON.parse(res.data) result = JSON.parse(res.data);
} catch (e) { } catch (e) {
// 不是JSON格式,保持原样 // 不是JSON格式,保持原样
} }
} else { } else {
// 4000 token无效或者过期 // 4000 token无效或者过期
if (result.code === 4000) { if (result.code === 4000) {
removeToken() removeToken();
window.location.reload() window.location.reload();
} }
if (result.code === 0) { if (result.code === 0) {
result.ok = true result.ok = true;
} else { } else {
result.ok = false result.ok = false;
ElMessage.error(result.msg || '请求失败') ElMessage.error(result.msg || "请求失败");
} }
} }
return result return result;
}, },
(err) => { (err) => {
ElMessage.error('网络请求出错, 请检查网络') ElMessage.error("网络请求出错, 请检查网络");
return Promise.reject(err) return Promise.reject(err);
} }
) );
// HTTP 请求方法常量 // HTTP 请求方法常量
export const GET = 'get' export const GET = "get";
export const POST = 'post' export const POST = "post";
export const PUT = 'put' export const PUT = "put";
export const DELETE = 'delete' export const DELETE = "delete";
// 请求函数类型 // 请求函数类型
export interface RequestConfig { export interface RequestConfig {
url: string; url: string;
method: 'get' | 'post' | 'put' | 'delete'; method: "get" | "post" | "put" | "delete";
params?: any; params?: any;
data?: any; data?: any;
headers?: Record<string, string>; headers?: Record<string, string>;
} }
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code: number code: number;
data: T data: T;
message: string message: string;
} }
export interface QueryParams { export interface QueryParams {
page?: number page?: number;
size?: number size?: number;
project?: string project?: string;
spider?: string spider?: string;
scrapydServerId?: string scrapydServerId?: string;
order_prop?: string order_prop?: string;
order_type?: string order_type?: string;
status?: any status?: any;
job?:any job?: any;
} }
// 请求函数 // 请求函数
export const request = (config: RequestConfig) => { export const request = (config: RequestConfig) => {
return instance(config); return instance(config);
} };
export interface UserQueryParams { export interface UserQueryParams {
page?: number page?: number;
size?: number size?: number;
username?: string username?: string;
nickname?: string nickname?: string;
password?: string password?: string;
role?: number role?: number;
status?: any status?: any;
id?: number id?: number;
ids?: any ids?: any;
scrapydServerId?: string scrapydServerId?: string;
project?: string project?: string;
spiders?: any spiders?: any;
times?: any times?: any;
job_id?: string job_id?: string;
scrapyd_server_id?: string scrapyd_server_id?: string;
schedule_type?: string schedule_type?: string;
spider?: string spider?: string;
cron?: string cron?: string;
options?: string options?: string;
sat_name?: string sat_name?: string;
ntc_id?: string ntc_id?: string;
filters?: any filters?: any;
norad_cat_id?: string norad_cat_id?: string;
object_name?: string object_name?: string;
name?:string name?: string;
spacecraft?:string spacecraft?: string;
station?:string station?: string;
} interval?: number[];
\ No newline at end of file }
...@@ -11,16 +11,15 @@ ...@@ -11,16 +11,15 @@
<!-- <allDataTab v-if="mode === '综合数据'"> <!-- <allDataTab v-if="mode === '综合数据'">
</allDataTab> --> </allDataTab> -->
<!-- DSN数据页面组件 --> <!-- DSN数据页面组件 -->
<dsnDataTab v-if="mode === 'DSN数据'"> </dsnDataTab> <dsnDataTab v-if="mode === 'DSN数据'"></dsnDataTab>
<!-- ITU数据页面组件 --> <!-- ITU数据页面组件 -->
<ituDataTab v-if="mode === 'ITU数据'"> </ituDataTab> <ituDataTab v-if="mode === 'ITU数据'"></ituDataTab>
<!-- ST数据页面组件 --> <!-- ST数据页面组件 -->
<stDataTab v-if="mode === 'ST数据'"> </stDataTab> <stDataTab v-if="mode === 'ST数据'"></stDataTab>
<!-- ESA数据页面组件 --> <!-- ESA数据页面组件 -->
<esDataTab v-if="mode === 'ESA数据'"> </esDataTab> <esDataTab v-if="mode === 'ESA数据'"></esDataTab>
<exportDialog v-model:dialogVisible="showDeleteDialog" @confirm="handleExportConfirm" />
</div> </div>
<exportDialog v-model:dialogVisible="showDeleteDialog" @confirm="handleExportConfirm" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
......
<template>
<div class="No-Item" :class="{ selected: isSelected }" @click="selectNo">
<!-- 新增选中标识,增强视觉反馈 -->
<span class="selected-icon" v-if="isSelected"></span>
{{ props.No }}
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
const props = defineProps<{
No: string | number;
resetSelected?: boolean;
}>();
const isSelected = ref(false);
const emit = defineEmits<{
(e: "addNo", sateNo: string | number): void;
(e: "delNo", sateNo: string | number): void;
}>();
const selectNo = () => {
isSelected.value = !isSelected.value;
isSelected.value ? emit("addNo", props.No) : emit("delNo", props.No);
};
watch(
() => props.resetSelected,
(newVal) => {
if (newVal) isSelected.value = false;
},
{ immediate: true }
);
</script>
<style scoped>
.No-Item {
width: 100% !important;
letter-spacing: 6px;
background-color: #e8f4f8;
font-size: 16px;
font-weight: 600;
color: #192a3e;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
padding: 0 16px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-sizing: border-box;
user-select: none;
border: 1px solid transparent;
}
.No-Item:not(.selected):hover {
background-color: #f0f8fb;
border-color: #4299e1;
}
.selected {
background-color: #4299e1;
color: #ffffff;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
border-color: #2563eb;
}
.selected-icon {
margin-right: 8px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
}
</style>
...@@ -13,13 +13,19 @@ ...@@ -13,13 +13,19 @@
:header-cell-style="{ textAlign: 'center' }" :header-cell-style="{ textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"
:row-style="{ height: '60px' }" :row-style="{ height: '60px' }"
v-loading="tableData.length == 0"
element-loading-background="rgba(48, 65, 86, 0.7)"
> >
<!-- <el-table-column type="selection" width="40" height="40" /> -->
<el-table-column property="number" label="序号" type="index" width="80" /> <el-table-column property="number" label="序号" type="index" width="80" />
<el-table-column property="spider" label="爬虫名称" show-overflow-tooltip /> <el-table-column property="spider" label="爬虫代号" show-overflow-tooltip />
<el-table-column label="查看任务"> <el-table-column property="spider_name" label="爬虫名称" show-overflow-tooltip />
<el-table-column property="spider_info" label="任务描述" show-overflow-tooltip />
<el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-button type="primary" plain @click="handleDetails(scope.row)"> 任务 </el-button> <div class="btn-group">
<el-button type="primary" plain @click="handleDetails(scope.row)">查看任务</el-button>
<el-button type="primary" plain @click="handleEditSateId">编辑</el-button>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
...@@ -32,14 +38,56 @@ ...@@ -32,14 +38,56 @@
@pagination="getData" @pagination="getData"
/> />
</div> </div>
//编辑弹窗
<el-dialog style="z-index: 999999" draggable v-model="editDialogVisible" title="指定下载以下卫星" width="400">
<div class="No-Content" ref="noContentRef">
<TransitionGroup
enter-active-class="animate__animated animate__slideInRight"
leave-active-class="animate__animated animate__slideOutRight"
enter-from-class="animate__opacity-0"
leave-to-class="animate__opacity-0"
>
<NoItem
v-for="(no, index) in sateNoList"
:key="index"
class="no-item-row"
:-no="no"
@add-no="handleSateAdd"
@del-no="handleSateDel"
:reset-selected="resetSelected"
></NoItem>
/>
</TransitionGroup>
</div>
<div class="footer">
<div class="No">
<span style="color: white; font-weight: 500">NORAD编号:</span>
<el-input
@input="handleNumInput"
maxlength="5"
placeholder="请输入编号"
v-model="curSateNo"
style="width: 100px"
></el-input>
</div>
<div class="btn">
<el-button :disabled="curSateNo == ''" plain type="primary" @click="addNoToList">添加</el-button>
<el-button plain type="danger" @click="delNoList">删除</el-button>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted, nextTick } from "vue";
import Pagination from "@/components/pagination/index.vue"; import Pagination from "@/components/pagination/index.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getSpiderList } from "@/api/system.ts"; import { getSpiderList, addSateNo, getSateIdList } from "@/api/system.ts";
import NoItem from "./components/NoItem.vue";
import { ElMessage } from "element-plus";
const router = useRouter(); const router = useRouter();
const tableData = ref([]); const tableData = ref([]);
const pageObj = ref({ const pageObj = ref({
...@@ -48,7 +96,6 @@ const pageObj = ref({ ...@@ -48,7 +96,6 @@ const pageObj = ref({
pageNo: 1, pageNo: 1,
}); });
const handleDetails = (row: any) => { const handleDetails = (row: any) => {
// console.log(row);
router.push({ router.push({
path: "/osTaskInformation/list", path: "/osTaskInformation/list",
query: { query: {
...@@ -64,6 +111,100 @@ const getData = async () => { ...@@ -64,6 +111,100 @@ const getData = async () => {
tableData.value = res.data; tableData.value = res.data;
}; };
const editDialogVisible = ref(false);
const sateNoList = ref<string[]>([]);
const curSateNo = ref("");
const curSelSateNo = ref<(string | number)[]>([]);
const resetSelected = ref(false);
//添加选中项到当前列表
const handleSateAdd = (no: string | number) => {
curSelSateNo.value.push(padZeroTo5Digits(no));
};
//从当前列表删除选中项
const handleSateDel = (no: string | number) => {
curSelSateNo.value = curSelSateNo.value.filter((item) => item != no);
};
//添加编号到列表并整体发到后端
const addNoToList = async () => {
try {
if (sateNoList.value.includes(padZeroTo5Digits(curSateNo.value))) {
ElMessage.error("当前编号已存在,请重新输入");
curSateNo.value = "";
return;
}
sateNoList.value.push(padZeroTo5Digits(curSateNo.value));
const res = await addSateNo({ id: sateNoList.value });
if (res.code === 0) {
ElMessage.success("添加编号成功");
} else {
sateNoList.value.pop();
ElMessage.error(res.message);
}
curSateNo.value = "";
scrollToBottom();
} catch (error: any) {
ElMessage.error(error.message);
}
};
//删除选中编号
const delNoList = async () => {
try {
sateNoList.value = sateNoList.value.filter((item) => !curSelSateNo.value.includes(item));
const res = await addSateNo({ id: sateNoList.value as string[] });
if (res.code === 0) {
}
resetSelected.value = true;
curSelSateNo.value = [];
nextTick(() => {
resetSelected.value = false;
});
} catch (error: any) {}
};
const noContentRef = ref<HTMLDivElement | null>(null);
//新添加滚动到底部
const scrollToBottom = () => {
nextTick(() => {
const container = noContentRef.value;
if (container) {
container.scrollTop = container.scrollHeight - container.clientHeight;
}
});
};
const handleNumInput = (value: string) => {
const filtered = value.replace(/\D/g, "");
curSateNo.value = filtered.slice(0, 5);
};
//编号格式化
const padZeroTo5Digits = (num: number | string): string => {
const numStr = String(num);
const pureNumStr = numStr.replace(/\D/g, "");
return pureNumStr.padStart(5, "0").slice(-5);
};
const handleEditSateId = async () => {
editDialogVisible.value = true;
try {
const res = await getSateIdList();
if (res.code === 0) {
sateNoList.value = res.data || [];
}
console.log(res);
} catch (error: any) {
ElMessage.error(error.message);
}
};
onMounted(() => { onMounted(() => {
getData(); getData();
}); });
...@@ -94,4 +235,68 @@ onMounted(() => { ...@@ -94,4 +235,68 @@ onMounted(() => {
.divider { .divider {
margin-top: 15px; margin-top: 15px;
} }
.No-Content {
width: 100%;
height: 200px;
background-color: #1d5484;
border: 4px solid #083b67;
display: flex;
padding: 10px;
box-sizing: border-box;
flex-direction: column;
gap: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.footer {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
.No {
display: flex;
align-items: center;
}
.No-Content::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.No-Content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.No-Content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
}
.No-Content::-webkit-scrollbar-thumb:hover {
background: #999;
}
.No-Content::-webkit-scrollbar-button {
display: none;
}
.No-Content {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
.btn-group {
display: flex;
gap: 8px;
white-space: nowrap;
justify-content: center;
}
.btn-group .el-button {
flex-shrink: 0;
}
</style> </style>
<template> <template>
<el-dialog v-model="currentVisible" :title="currentMode === AddMode.ADD_TASK ? '新增' : '修改'" width="710" center <el-dialog
align-center @close="handleClose" draggable> v-model="currentVisible"
<el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-width="100px"> :title="currentMode === AddMode.ADD_TASK ? '新增' : '修改'"
<el-form-item label="任务名称:" v-if="currentMode === AddMode.ADD_TASK" prop="taskName"> width="710"
<el-input v-model="ruleForm.taskName" placeholder="请输入任务名称" style="width: 90%" /> center
</el-form-item> align-center
<el-form-item label="所属爬虫:" v-if="currentMode === AddMode.ADD_TASK" prop="spiderTypeValue"> @close="handleClose"
<el-select v-model="ruleForm.spiderTypeValue" placeholder="请选择所属爬虫" style="width: 90%" > draggable
<el-option v-for="item in spiderTypeOptions" :key="item.spider" :label="item.spider" >
:value="item.spider" :id="item.spider" /> <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-width="100px">
</el-select> <el-form-item label="任务名称:" v-if="currentMode === AddMode.ADD_TASK" prop="taskName">
</el-form-item> <el-input v-model="ruleForm.taskName" placeholder="请输入任务名称" style="width: 90%" />
<el-form-item label="执行频率:" prop="cronExpression"> </el-form-item>
<Crontab v-model="ruleForm.cronExpression" style="width: 90%" /> <el-form-item label="所属爬虫:" v-if="currentMode === AddMode.ADD_TASK" prop="spiderTypeValue">
</el-form-item> <el-select v-model="ruleForm.spiderTypeValue" placeholder="请选择所属爬虫" style="width: 90%">
</el-form> <el-option
<template #footer> v-for="item in spiderTypeOptions"
<div class="dialog-footer"> :key="item.spider"
<el-button @click="handleClose">取消</el-button> :label="item.spider"
<el-button type="primary" @click="handleConfirm"> :value="item.spider"
确定 :id="item.spider"
</el-button> />
</div> </el-select>
</template> </el-form-item>
</el-dialog> <el-form-item label="执行频率:">
<!-- <Crontab v-model="ruleForm.cronExpression" style="width: 90%"/> -->
<div class="gap-units">
<div class="gap-unit">
<el-input-number
v-model="exGaps.day"
class="gap-ipt"
:min="0"
:precision="0"
size="small"
></el-input-number>
<span class="unit-num"></span>
</div>
<div class="gap-unit">
<el-input-number
v-model="exGaps.hour"
class="gap-ipt"
:min="0"
:precision="0"
size="small"
></el-input-number>
<span class="unit-num">小时</span>
</div>
<div class="gap-unit">
<el-input-number
v-model="exGaps.minute"
class="gap-ipt"
:min="0"
:precision="0"
size="small"
></el-input-number>
<span class="unit-num">分钟</span>
</div>
<div class="gap-unit">
<el-input-number
v-model="exGaps.second"
class="gap-ipt"
:min="0"
:precision="2"
size="small"
></el-input-number>
<span class="unit-num"></span>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm"> 确定 </el-button>
</div>
</template>
</el-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue' import { onMounted, reactive, ref } from "vue";
import { defineProps } from 'vue'; import { defineProps } from "vue";
import { watch } from 'vue' import { watch } from "vue";
import { addSpiderTask } from '@/api/spiderTask' import { addSpiderTask } from "@/api/spiderTask";
import { AddMode } from './enum' import { AddMode } from "./enum";
import { Crontab } from '@/components/Crontab/index' import type { FormInstance, FormRules } from "element-plus";
import type { FormInstance, FormRules } from 'element-plus' import { ElMessage } from "element-plus";
import { ElMessage } from 'element-plus' import { getSpiderList } from "@/api/system.ts";
import { getSpiderList } from '@/api/system.ts'
interface RuleForm { interface RuleForm {
taskName: string taskName: string;
spiderTypeValue: string spiderTypeValue: string;
cronExpression: string
} }
const ruleFormRef = ref<FormInstance>() const ruleFormRef = ref<FormInstance>();
const ruleForm = ref<RuleForm>({ const ruleForm = ref<RuleForm>({
taskName: '', taskName: "",
spiderTypeValue: '', spiderTypeValue: "",
cronExpression: '', });
})
interface addDialogPropType { interface addDialogPropType {
dialogVisible: boolean, dialogVisible: boolean;
mode: AddMode, mode: AddMode;
jobId: string, jobId: string;
cron: string, exGap: number[];
spiderType: string, spiderType: string;
options: string, options: string;
} }
const props = defineProps<addDialogPropType>() const props = defineProps<addDialogPropType>();
const rules = ref<FormRules<RuleForm>>({ const rules = ref<FormRules<RuleForm>>({
taskName: [ taskName: [
{ required: true, message: '请输入任务名称', trigger: 'blur' }, { required: true, message: "请输入任务名称", trigger: "blur" },
{ min: 2, max: 50, message: '任务名称应在2-20个字符之间', trigger: 'blur' }, { min: 2, max: 50, message: "任务名称应在2-20个字符之间", trigger: "blur" },
], ],
spiderTypeValue: [ spiderTypeValue: [
{ {
required: true, required: true,
message: '请选择所属爬虫', message: "请选择所属爬虫",
trigger: 'change', trigger: "change",
}, },
], ],
cronExpression: [ });
{
required: true,
message: '请输入执行频率',
trigger: 'change',
},
{
validator: (rule, value, callback) => {
// 计算字符串中'*'的数量
const starCount = (value.match(/\//g) || []).length;
if (starCount > 1) {
callback(new Error('cron表达式最多设置一个间隔'));
}else if(starCount == 0){
callback(new Error('请设置cron表达式'));
}else {
callback();
}
},
trigger: 'blur'
}
]
})
// 向父组件传递dialog值 // 向父组件传递dialog值
const emit = defineEmits(['update:dialogVisible', 'confirm', 'update:mode', 'getTaskList']) const emit = defineEmits(["update:dialogVisible", "confirm", "update:mode", "getTaskList"]);
// 组件的状态 // 组件的状态
const currentVisible = ref(props.dialogVisible) const currentVisible = ref(props.dialogVisible);
const currentMode = ref(props.mode) const currentMode = ref(props.mode);
const spiderTypeOptions = ref() const spiderTypeOptions = ref();
const taskParams = ref({ const taskParams = ref({
scrapyd_server_id: '1', scrapyd_server_id: "1",
schedule_type: '0', schedule_type: "0",
project: 'spiders' project: "spiders",
}) });
const cronExpression = ref('')
// 关闭的方法 // 关闭的方法
const handleClose = () => { const handleClose = () => {
ruleForm.value.taskName = '' ruleForm.value.taskName = "";
ruleForm.value.spiderTypeValue ruleForm.value.spiderTypeValue;
ruleForm.value.cronExpression = '' ruleFormRef.value?.resetFields();
ruleFormRef.value?.resetFields() currentVisible.value = false;
currentVisible.value = false };
}
//间隔数据
const exGaps = reactive({
second: 0,
minute: 0,
hour: 0,
day: 0,
});
// 确定的方法 // 确定的方法
const handleConfirm = async () => { const handleConfirm = async () => {
console.log(ruleForm.value.cronExpression); if (!ruleFormRef.value) return;
await ruleFormRef.value.validate(async (valid) => {
if (!ruleFormRef.value) return //验证表单内容是否通过,通过继续执行
await ruleFormRef.value.validate(async (valid, fields) => { if (valid) {
console.log('开始校验'); if (currentMode.value === AddMode.ADD_TASK) {
//验证表单内容是否通过,通过继续执行 await addSpiderTask({
if (valid) { scrapyd_server_id: taskParams.value.scrapyd_server_id,
if (currentMode.value === AddMode.ADD_TASK) { schedule_type: taskParams.value.schedule_type,
await addSpiderTask({ project: taskParams.value.project,
scrapyd_server_id: taskParams.value.scrapyd_server_id, spider: ruleForm.value.spiderTypeValue,
schedule_type: taskParams.value.schedule_type, options: JSON.stringify({ jobName: ruleForm.value.taskName }),
project: taskParams.value.project, interval: [exGaps.second, exGaps.minute, exGaps.hour, exGaps.day],
spider: ruleForm.value.spiderTypeValue, });
cron: ruleForm.value.cronExpression,
options: JSON.stringify({ jobName: ruleForm.value.taskName }) currentVisible.value = false;
}) emit("getTaskList");
currentVisible.value = false ElMessage.success("添加成功");
emit('getTaskList') } else if (currentMode.value === AddMode.UPDATE_TASK) {
ElMessage.success('添加成功') await addSpiderTask({
} else if (currentMode.value === AddMode.UPDATE_TASK) { scrapyd_server_id: taskParams.value.scrapyd_server_id,
await addSpiderTask({ schedule_type: taskParams.value.schedule_type,
scrapyd_server_id: taskParams.value.scrapyd_server_id, project: taskParams.value.project,
schedule_type: taskParams.value.schedule_type, spider: props.spiderType,
project: taskParams.value.project, job_id: props.jobId,
spider: props.spiderType, options: props.options,
job_id: props.jobId, interval: [exGaps.second, exGaps.minute, exGaps.hour, exGaps.day],
cron: ruleForm.value.cronExpression, });
options: props.options currentVisible.value = false;
}) emit("getTaskList");
currentVisible.value = false ElMessage.success("修改成功");
emit('getTaskList') }
ElMessage.success('修改成功') } else {
} console.log("校验不通过");
} else { }
console.log('校验不通过'); exGaps.second = 0;
} exGaps.minute = 0;
}) exGaps.hour = 0;
} exGaps.day = 0;
});
};
const getData = async () => { const getData = async () => {
const res = await getSpiderList({scrapydServerId:'1',project:'spiders'}) const res = await getSpiderList({ scrapydServerId: "1", project: "spiders" });
spiderTypeOptions.value = res.data spiderTypeOptions.value = res.data;
console.log(res); };
}
// 监听props变化,同步给组件内部 // 监听props变化,同步给组件内部
watch(() => props.dialogVisible, watch(
(newVal) => { () => props.dialogVisible,
currentVisible.value = newVal (newVal) => {
} currentVisible.value = newVal;
) }
);
// 监听组件内部状态变化,同步给父组件,这样才能做到控制对话框组件的显隐 // 监听组件内部状态变化,同步给父组件,这样才能做到控制对话框组件的显隐
watch(() => currentVisible.value,
(newVal) => {
emit('update:dialogVisible', newVal)
}
)
watch(() => props.mode,
(newVal) => {
currentMode.value = newVal
}
)
watch(() => currentMode.value,
(newVal) => {
emit('update:mode', newVal)
}
)
watch( watch(
[() => props.dialogVisible, () => props.mode, () => props.cron], () => currentVisible.value,
([newVisible, newMode, newCron]) => { (newVal) => {
if (newVisible && newMode === AddMode.UPDATE_TASK) { emit("update:dialogVisible", newVal);
ruleForm.value.cronExpression = newCron; }
} );
if (!newVisible) {
// 清空表单 watch(
// cronExpression.value = '' () => props.mode,
} (newVal) => {
}, currentMode.value = newVal;
{ immediate: true } }
);
watch(
() => currentMode.value,
(newVal) => {
emit("update:mode", newVal);
}
);
watch(
() => props.exGap,
(newVal) => {
exGaps.second = newVal[0] || 0;
exGaps.minute = newVal[1] || 0;
exGaps.hour = newVal[2] || 0;
exGaps.day = newVal[3] || 0;
},
{ immediate: true }
); );
onMounted(() => { onMounted(() => {
getData() getData();
}) });
</script> </script>
<style> <style>
/* 修改弹出框样式 */ /* 修改弹出框样式 */
.el-dialog { .el-dialog {
background: transparent; background: transparent;
background-image: url("@/assets/picture/dialog1.png"); background-image: url("@/assets/picture/dialog1.png");
background-size: 100% 100%; background-size: 100% 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
color: #ffffff; color: #ffffff;
.el-dialog__title { .el-dialog__title {
color: #ffffff; color: #ffffff;
} }
.el-input__inner { .el-input__inner {
color: #ffffff; color: #ffffff;
} }
} }
/* 修改表单样式 */ /* 修改表单样式 */
.el-form { .el-form {
.el-form-item__label { .el-form-item__label {
color: #ffffff; color: #ffffff;
} }
}
</style>
<style scope>
.gap-unit {
display: flex;
gap: 2px;
align-items: center;
margin-right: 10px;
}
.unit-num {
color: white;
}
.gap-units {
display: flex;
}
.gap-ipt {
width: 100px;
} }
</style> </style>
...@@ -10,26 +10,25 @@ ...@@ -10,26 +10,25 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<div> <div>
<el-select v-model="searchCondition.spiders" placeholder="请选择" style="width: 220px"> <el-select
v-model="searchCondition.spiders"
placeholder="请选择"
style="width: 220px"
:disabled="!taskSelectOptions.length"
>
<el-option <el-option
v-for="item in taskSelectOptions" v-for="item in taskSelectOptions || []"
:key="item.spider" :key="item?.spider"
:label="item.spider" :label="item?.spider || '未知'"
:value="item.spider" :value="item?.spider"
/> />
</el-select> </el-select>
</div> </div>
</el-form-item> </el-form-item>
<!-- <el-form-item>
<el-text class="mx-1" style="color: #fff;">任务名称:</el-text>
</el-form-item>
<el-form-item>
<el-input v-model="taskName" placeholder="请输入" style="width: 220px" />
</el-form-item> -->
<el-form-item> <el-form-item>
<el-space> <el-space>
<el-button type="primary" @Click="search">查询</el-button> <el-button type="primary" @click="search">查询</el-button>
<el-button type="primary" @Click="resetData">重置列表</el-button> <el-button type="primary" @click="resetData">重置列表</el-button>
<el-button type="primary" @click="openTaskDialog">新建任务</el-button> <el-button type="primary" @click="openTaskDialog">新建任务</el-button>
</el-space> </el-space>
</el-form-item> </el-form-item>
...@@ -37,32 +36,42 @@ ...@@ -37,32 +36,42 @@
</div> </div>
</div> </div>
<div class="cardStyle"> <div class="cardStyle">
<el-row v-loading="taskList.length == 0" element-loading-background="rgba(48, 65, 86, 0.7)"> <!-- 加载状态优化:用专门的 loading 变量,避免依赖 taskList 长度 -->
<div class="taskCard p-6" v-for="task in taskList" :key="task.taskId"> <el-row v-loading="isLoading" element-loading-background="rgba(48, 65, 86, 0.7)">
<!-- 任务列表为空时显示提示,避免空循环 -->
<div v-if="taskList.length === 0 && !isLoading" class="empty-tip">暂无任务数据</div>
<!-- 循环前确保 taskList 是数组,key 用安全的 id -->
<div class="taskCard p-6" v-for="task in taskList || []" :key="task?.id || task?.taskId">
<div class="taskContent"> <div class="taskContent">
<div> <div>
<el-form-item class="form-item"> <el-form-item class="form-item">
<!-- 安全访问 JSON 解析,避免 options 为 undefined 或 JSON 解析失败 -->
<span class="titleStyle">{{ <span class="titleStyle">{{
task.kwargs.options === "" ? "无名称" : JSON.parse(task.kwargs.options).jobName (task?.kwargs?.options && JSON.parse(task.kwargs.options).jobName) || "无名称"
}}</span> }}</span>
</el-form-item> </el-form-item>
</div> </div>
<div> <div>
<el-form-item class="form-item"> <el-form-item class="form-item">
<el-button type="primary" @click="editTask(task)" class="editButton">编辑</el-button> <el-button type="primary" @click="editTask(task)" class="editButton" :disabled="!task?.id"
<!-- <el-button type="success" @click="goToTaskRecord" class="recordButton">执行记录 </el-button> --> >编辑</el-button
<el-button type="danger" @click="deleteTask(task)" class="deleteButton">删除</el-button> >
<el-button type="danger" @click="deleteTask(task)" class="deleteButton" :disabled="!task?.id"
>删除</el-button
>
</el-form-item> </el-form-item>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<el-form-item class="form-item"> <el-form-item class="form-item">
<el-space> <el-space>
<span class="wordStyle">启用/停止: </span> <span class="wordStyle">启用/停止: </span>
<!-- 确保 task.status 有默认值,避免 v-model 绑定 undefined -->
<el-switch <el-switch
v-model="task.status" v-model="task.status"
:active-value="'running'" :active-value="'running'"
:inactive-value="'paused'" :inactive-value="'paused'"
@change="(newStatus: any) => changeStatus(task, newStatus)" @change="(newStatus: any) => changeStatus(task, newStatus)"
:disabled="!task?.id"
/> />
</el-space> </el-space>
</el-form-item> </el-form-item>
...@@ -72,7 +81,7 @@ ...@@ -72,7 +81,7 @@
<el-space> <el-space>
<span class="wordStyle" <span class="wordStyle"
>执行频率: >执行频率:
<span style="color: skyblue">{{ parseCronExpression(task.kwargs.cron) }} </span> <span style="color: skyblue">{{ parseCronExpression(task?.kwargs?.cron) || "未设置" }} </span>
</span> </span>
</el-space> </el-space>
</el-form-item> </el-form-item>
...@@ -81,7 +90,7 @@ ...@@ -81,7 +90,7 @@
<el-form-item class="form-item"> <el-form-item class="form-item">
<el-space> <el-space>
<span class="wordStyle" <span class="wordStyle"
>执行次数: <span style="color: skyblue">{{ formatExactLargeNum(task.count) }}</span> >执行次数: <span style="color: skyblue">{{ formatExactLargeNum(task?.count || 0) }}</span>
</span> </span>
</el-space> </el-space>
</el-form-item> </el-form-item>
...@@ -90,19 +99,11 @@ ...@@ -90,19 +99,11 @@
<el-form-item class="form-item"> <el-form-item class="form-item">
<el-space> <el-space>
<span class="wordStyle" <span class="wordStyle"
>所属爬虫: <span style="color: skyblue"> {{ task.kwargs.spider }}</span></span >所属爬虫: <span style="color: skyblue"> {{ task?.kwargs?.spider || "未知" }}</span></span
> >
</el-space> </el-space>
</el-form-item> </el-form-item>
</div> </div>
<!-- <div class="wordStyle">
<el-form-item class="form-item">
<el-space>
<span class="wordStyle">失败次数: </span>
<span class="wordStyle">异常记录: </span>
</el-space>
</el-form-item>
</div> -->
</div> </div>
</div> </div>
</el-row> </el-row>
...@@ -114,12 +115,12 @@ ...@@ -114,12 +115,12 @@
:mode="editMode" :mode="editMode"
@getTaskList="getData" @getTaskList="getData"
:jobId="jobId" :jobId="jobId"
:cron="frequency" :exGap="frequency || []"
:spiderType="belongSpiderType" :spiderType="belongSpiderType || ''"
:options="taskOptions" :options="taskOptions || ''"
/> />
<deleteDialog v-model:dialogVisible="showDeleteDialog" @get-user-list="getData" :jobId="jobId" /> <deleteDialog v-model:dialogVisible="showDeleteDialog" @get-user-list="getData" :jobId="jobId" :disabled="!jobId" />
</div> </div>
</template> </template>
...@@ -150,17 +151,19 @@ const props = defineProps({ ...@@ -150,17 +151,19 @@ const props = defineProps({
}, },
}); });
const taskSelectOptions = ref(); // 所有响应式变量初始化时避免 undefined
const taskSelectOptions = ref<any[]>([]); // 初始化为空数组
const searchCondition = ref({ const searchCondition = ref({
spiders: "", spiders: "",
options: "", options: "",
}); });
const belongSpider = ref(""); const belongSpider = ref("");
const taskName = ref(""); const taskName = ref("");
const taskValue = ref(""); const taskValue = ref("");
const belongSpiderType = ref(""); const belongSpiderType = ref("");
const router = useRouter(); const router = useRouter();
const frequency = ref(); const frequency = ref([]); // 初始化为空字符串,解决 cron 类型错误
const editMode = ref(AddMode.UPDATE_TASK); const editMode = ref(AddMode.UPDATE_TASK);
const deleteMode = ref(DeleteMode.SINGLE_DELETE); const deleteMode = ref(DeleteMode.SINGLE_DELETE);
const showDeleteDialog = ref(false); const showDeleteDialog = ref(false);
...@@ -169,23 +172,32 @@ const taskSwitch = ref(false); ...@@ -169,23 +172,32 @@ const taskSwitch = ref(false);
const taskOptions = ref(""); const taskOptions = ref("");
const jobId = ref(""); const jobId = ref("");
const taskList = ref<any[]>([]); const taskList = ref<any[]>([]);
const isLoading = ref(false); // 新增专门的加载状态变量
// 删除任务的方法 // 删除任务的方法
const deleteTask = (task: any) => { const deleteTask = (task: any) => {
if (!task?.id) return; // 避免 task 为 undefined
jobId.value = task.id; jobId.value = task.id;
deleteMode.value = DeleteMode.SINGLE_DELETE; deleteMode.value = DeleteMode.SINGLE_DELETE;
showDeleteDialog.value = true; showDeleteDialog.value = true;
}; };
// 编辑任务的方法 // 编辑任务的方法
const editTask = (task: any) => { const editTask = (task: any) => {
if (!task?.id) return; // 避免 task 为 undefined
jobId.value = task.id; jobId.value = task.id;
taskOptions.value = JSON.stringify(JSON.parse(task.kwargs.options)); // 安全处理 JSON 字符串,避免解析失败
belongSpiderType.value = task.kwargs.spider; try {
frequency.value = task.kwargs.cron; taskOptions.value = JSON.stringify(task?.kwargs?.options ? JSON.parse(task.kwargs.options) : {});
} catch (err) {
taskOptions.value = "{}";
}
belongSpiderType.value = task?.kwargs?.spider || "";
frequency.value = task?.kwargs?.interval || []; // 兜底空字符串
editMode.value = AddMode.UPDATE_TASK; editMode.value = AddMode.UPDATE_TASK;
showTaskDialog.value = true; showTaskDialog.value = true;
// console.log(JSON.stringify(JSON.parse(task.kwargs.options)));
}; };
const goToTaskRecord = () => { const goToTaskRecord = () => {
router.push({ router.push({
path: "/osTaskRecord/list", path: "/osTaskRecord/list",
...@@ -195,107 +207,166 @@ const goToTaskRecord = () => { ...@@ -195,107 +207,166 @@ const goToTaskRecord = () => {
}, },
}); });
}; };
// 启动与停止任务的方法 // 启动与停止任务的方法
const changeStatus = async (task: any, newStatus: string) => { const changeStatus = async (task: any, newStatus: string) => {
if (newStatus === "running") { if (!task?.id) return; // 避免 task 为 undefined
await resumeSpiderTask({ job_id: task.id }); try {
} else if (newStatus === "paused") { if (newStatus === "running") {
await pauseSpiderTask({ job_id: task.id }); await resumeSpiderTask({ job_id: task.id });
ElMessage.success("任务已启动");
} else if (newStatus === "paused") {
await pauseSpiderTask({ job_id: task.id });
ElMessage.success("任务已暂停");
}
} catch (err) {
ElMessage.error("状态切换失败");
console.error(err);
// 状态回滚,避免界面显示错误
task.status = newStatus === "running" ? "paused" : "running";
} }
// await getData();
}; };
const handleEdit = () => {}; const handleEdit = () => {};
// 打开新建任务弹窗 // 打开新建任务弹窗
const openTaskDialog = () => { const openTaskDialog = () => {
editMode.value = AddMode.ADD_TASK; editMode.value = AddMode.ADD_TASK;
frequency.value = "* * * * * *"; frequency.value = [];
showTaskDialog.value = true; showTaskDialog.value = true;
}; };
// 获取任务列表的方法 // 获取任务列表的方法
const getData = async () => { const getData = async () => {
const res = await getSpiderTaskList({ isLoading.value = true; // 开始加载
spiders: searchCondition.value.spiders ? [searchCondition.value.spiders] : [], try {
}); const res = await getSpiderTaskList({
const resId = await getTaskCount({}); spiders: searchCondition.value.spiders ? [searchCondition.value.spiders] : [],
for (let i = 0; i < res.data.length; i++) { });
res.data[i].count = resId.data[res.data[i].id] === undefined ? 0 : resId.data[res.data[i].id]; const resId = await getTaskCount({});
// 安全处理返回数据,避免 res.data 为 undefined
const taskData = res?.data || [];
const countData = resId?.data || {};
// 遍历赋值时兜底
taskList.value = taskData.map((item: any) => ({
...item,
count: countData[item?.id] ?? 0, // 执行次数兜底 0
status: item?.status || "paused", // 状态兜底
kwargs: item?.kwargs || {}, // kwargs 兜底空对象
}));
} catch (err) {
ElMessage.error("获取任务列表失败");
console.error(err);
taskList.value = []; // 错误时置空数组
} finally {
isLoading.value = false; // 结束加载
} }
taskList.value = res.data;
}; };
// 重置任务列表的方法 // 重置任务列表的方法
const resetData = async () => { const resetData = async () => {
searchCondition.value.spiders = ""; searchCondition.value.spiders = "";
const res = await getSpiderTaskList({ spiders: [], options: JSON.stringify({ jobName: taskName.value }) }); isLoading.value = true;
const resId = await getTaskCount({}); try {
for (let i = 0; i < res.data.length; i++) { const res = await getSpiderTaskList({ spiders: [], options: JSON.stringify({ jobName: taskName.value }) });
res.data[i].count = resId.data[res.data[i].id] === undefined ? 0 : resId.data[res.data[i].id]; const resId = await getTaskCount({});
const taskData = res?.data || [];
const countData = resId?.data || {};
taskList.value = taskData.map((item: any) => ({
...item,
count: countData[item?.id] ?? 0,
status: item?.status || "paused",
kwargs: item?.kwargs || {},
}));
} catch (err) {
ElMessage.error("重置任务列表失败");
console.error(err);
taskList.value = [];
} finally {
isLoading.value = false;
} }
taskList.value = res.data;
}; };
// 搜索任务的方法 // 搜索任务的方法
const search = async () => { const search = async () => {
if (searchCondition.value.spiders === "") { if (searchCondition.value.spiders === "") {
ElMessage.warning("请先选择爬虫"); ElMessage.warning("请先选择爬虫");
return; return;
} }
const res = await getSpiderTaskList({ isLoading.value = true;
spiders: searchCondition.value.spiders ? [searchCondition.value.spiders] : [], try {
options: JSON.stringify({ jobName: taskName.value }), const res = await getSpiderTaskList({
}); spiders: [searchCondition.value.spiders],
const resId = await getTaskCount({}); options: JSON.stringify({ jobName: taskName.value }),
for (let i = 0; i < res.data.length; i++) { });
res.data[i].count = resId.data[res.data[i].id] === undefined ? 0 : resId.data[res.data[i].id]; const resId = await getTaskCount({});
const taskData = res?.data || [];
const countData = resId?.data || {};
taskList.value = taskData.map((item: any) => ({
...item,
count: countData[item?.id] ?? 0,
status: item?.status || "paused",
kwargs: item?.kwargs || {},
}));
} catch (err) {
ElMessage.error("搜索任务失败");
console.error(err);
taskList.value = [];
} finally {
isLoading.value = false;
} }
taskList.value = res.data;
}; };
// 解析cron表达式的方法
const parseCronExpression = (cronExpression: string) => { // 解析cron表达式的方法(修复逻辑错误,避免数组越界)
const res = cronExpression.split("*").length - 1; const parseCronExpression = (cronExpression?: string) => {
if (res >= 5) { // 兜底:cron表达式不存在时返回默认值
const aaa = cronExpression.split(" "); if (!cronExpression) return "未设置";
for (let i = 0; i < aaa.length; i++) { const parts = cronExpression.trim().split(" ");
if (aaa[i] != "*") { // 只处理标准 5/6 位 cron 表达式
if (i == 0) { if (parts.length < 5 || parts.length > 6) return "表达式格式错误";
if (aaa[i].length === 3) {
return `每${aaa[i][2]}秒执行一次`; // 遍历每个字段,找到非 * 的字段(修复数组越界问题)
} else { for (let i = 0; i < parts.length; i++) {
return `每${aaa[i][2] + aaa[i][3]}秒执行一次`; const part = parts[i].trim();
} if (part !== "*" && part !== "") {
} else if (i == 1) { // 转换为数字,避免非数字字符导致的错误
if (aaa[i].length === 3) { const num = parseInt(part, 10);
return `每${aaa[i][2]}分钟执行一次`; const validNum = isNaN(num) ? 1 : num;
} else {
return `每${aaa[i][2] + aaa[i][3]}分钟执行一次`; switch (i) {
} case 0:
} else if (i == 2) { return `每${validNum}秒执行一次`;
if (aaa[i].length === 3) { case 1:
return `每${aaa[i][2]}小时执行一次`; return `每${validNum}分钟执行一次`;
} else { case 2:
return `每${aaa[i][2] + aaa[i][3]}小时执行一次`; return `每${validNum}小时执行一次`;
} case 3:
} else if (i == 3) { return `每${validNum}天执行一次`;
if (aaa[i].length === 3) { case 4:
return `每${aaa[i][2]}天执行一次`; return `每${validNum}月执行一次`;
} else { case 5:
return `每${aaa[i][2] + aaa[i][3]}天执行一次`; return `每${validNum}周执行一次`;
} default:
} else if (i == 4) { return "未知频率";
if (aaa[i].length === 3) {
return `每${aaa[i][2]}月执行一次`;
} else {
return `每${aaa[i][2] + aaa[i][3]}月执行一次`;
}
}
} }
} }
} }
// 全是 * 时返回默认
return "每秒执行一次";
}; };
// 获取爬虫类型列表
const getSpiderTypeList = async () => { const getSpiderTypeList = async () => {
const res = await getSpiderList({ scrapydServerId: "1", project: "spiders" }); try {
console.log(res, 8888888888888); const res = await getSpiderList({ scrapydServerId: "1", project: "spiders" });
taskSelectOptions.value = res.data; taskSelectOptions.value = res?.data || []; // 兜底空数组
// taskSelectOptions.value = res.data } catch (err) {
ElMessage.error("获取爬虫列表失败");
console.error(err);
taskSelectOptions.value = [];
}
}; };
onMounted(() => { onMounted(() => {
if (props.spiderType !== "") { if (props.spiderType !== "") {
searchCondition.value.spiders = props.spiderType; searchCondition.value.spiders = props.spiderType;
...@@ -303,24 +374,6 @@ onMounted(() => { ...@@ -303,24 +374,6 @@ onMounted(() => {
getData(); getData();
getSpiderTypeList(); getSpiderTypeList();
}); });
// const parseCronExpression=(cronExpression: string)=> {
// try {
// const interval = cronParser.parseExpression(cronExpression);
// const nextDate = interval.next().toDate();
// const hours = nextDate.getHours();
// const minutes = nextDate.getMinutes();
// const seconds = nextDate.getSeconds();
// return {
// hours,
// minutes,
// seconds
// };
// } catch (err) {
// console.error('Error parsing cron expression:', err);
// return null;
// }
// }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -329,6 +382,8 @@ onMounted(() => { ...@@ -329,6 +382,8 @@ onMounted(() => {
padding: 18px; padding: 18px;
gap: 15px; gap: 15px;
width: auto !important; width: auto !important;
display: flex;
flex-wrap: wrap; // 修复卡片溢出问题
} }
.taskContent { .taskContent {
...@@ -338,7 +393,6 @@ onMounted(() => { ...@@ -338,7 +393,6 @@ onMounted(() => {
justify-content: center; justify-content: center;
height: 100%; height: 100%;
gap: 6px; gap: 6px;
/* 进一步减小项目之间的间距 */
padding: 5px 0; padding: 5px 0;
} }
...@@ -394,25 +448,11 @@ onMounted(() => { ...@@ -394,25 +448,11 @@ onMounted(() => {
padding: 2px; padding: 2px;
} }
// 去除按钮边框 /* 空数据提示 */
.el-button:focus { .empty-tip {
outline: none; width: 100%;
color: #ffffff; text-align: center;
border: none; color: #fff;
} padding: 50px 0;
</style>
<style>
/* .el-button{
color: #ffffff;
border: none;
} */
.el-input {
color: #ffffff;
}
.el-select__placeholder {
color: #ffffff;
} }
</style> </style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment