Commit 7c74e445 by yzh

feat:打包正确

parent d5e5b74c
<template>
<span>
<el-button
@click="changeAutoRefresh"
:size="size"
>{{ autoRefreshText }}</el-button> <span class="tips">{{ countDownText }}</span>
</span>
</template>
<script setup lang="ts">
/**
* 自动刷新组件
* emit: refresh - 刷新事件
*/
import { ref, computed, onBeforeUnmount } from 'vue';
const props = defineProps({
// 刷新频率,默认:3秒
frequency: { type: Number, default: 3000 },
// 按钮大小
size: { type: String, default: 'default' }
});
const emit = defineEmits(['refresh']);
// 定时器
const timer = ref<NodeJS.Timeout | null>(null);
const countDownTimer = ref<NodeJS.Timeout | null>(null);
// 倒计时
const countDown = ref(0);
const autoRefreshText = computed(() => {
if (timer.value == null) {
return '开启自动刷新';
} else {
return '关闭自动刷新';
}
});
const countDownText = computed(() => {
if (timer.value == null) {
return '';
} else {
return countDown.value + ' 秒后自动刷新';
}
});
function autoRefresh() {
emit('refresh');
resetCountDown();
}
function countDownReduce() {
countDown.value--;
}
function resetCountDown() {
countDown.value = Math.ceil(props.frequency * 0.001);
}
function openAutoRefresh() {
resetCountDown();
// 启动读秒计时器
countDownTimer.value = setInterval(() => {
countDownReduce();
}, 1000);
// 启动自动刷新计时器
timer.value = setInterval(() => {
autoRefresh();
}, props.frequency);
}
function closeAutoRefresh() {
if (timer.value) clearInterval(timer.value);
if (countDownTimer.value) clearInterval(countDownTimer.value);
timer.value = null;
countDownTimer.value = null;
}
function changeAutoRefresh() {
if (timer.value) {
closeAutoRefresh();
} else {
openAutoRefresh();
}
}
// 组件销毁前清除定时器
onBeforeUnmount(() => {
closeAutoRefresh();
});
</script>
<style lang="scss" scoped>
.tips {
margin-left: 10px;
color: #666666;
font-size: 12px;
}
.el-button:focus {
outline: none;
}
</style>
<template>
<el-dialog v-model="exportDialogVisible" title="导出" width="400" center align-center @close="close" draggable>
<div class="text-center p-0 gap-3">
<!-- <span style="color: #fff;font-size: 15px;">请选择导出数据时间与数据类型。</span>
<br> -->
<el-date-picker v-model="timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间"
end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" style="width: 280px"
value-format="YYYY-MM-DD HH:mm:ss" />
<el-checkbox-group v-model="checkList">
<el-checkbox label="DSN数据" value="dsn" />
<el-checkbox label="ITU数据" value="itu" />
<el-checkbox label="ST数据" value="spaceTrack" />
</el-checkbox-group>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="handleExport">
确定
</el-button>
</div>
</template>
</el-dialog>
<div>
<el-dialog v-model="exportDialogVisible" v-loading="fullscreenLoading" title="导出" width="500" center align-center
@close="close" draggable>
<el-form class="formStyle" :rules="rules" ref="formRef" :model="exportObject">
<el-form-item prop="timeValue">
<el-date-picker v-model="exportObject.timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始时间" end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item prop="spiderType">
<el-checkbox-group v-model="exportObject.spiderType" class="checkboxStyle">
<el-checkbox label="DSN数据" value="dsn" />
<el-checkbox label="ITU数据" value="itu" />
<el-checkbox label="ST数据" value="spaceTrack" />
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="handleExport">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { defineProps } from 'vue';
import { exportSpiderData } from '@/api/spiderData';
import axios from 'axios'
import Cookies from 'js-cookie'
import { ElLoading, ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface RuleForm {
//表单字段的类型
timeValue: string
spiderType: Array<string>
}
const formRef = ref<FormInstance>()
const rules = ref<FormRules<RuleForm>>({
timeValue: [
{
type: 'date',
required: true,
message: '请选择时间段',
trigger: 'change',
},
],
spiderType: [
{
type: 'array',
required: true,
message: '请至少选择一种数据类型',
trigger: 'change',
},
]
})
//表单绑定的字段
const exportObject = ref({
timeValue: "",
spiderType: []
})
const fullscreenLoading = ref(false)
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: '1'
}
})
const emit = defineEmits(['update:dialogVisible', 'confirm'])
const exportDialogVisible = ref(props.dialogVisible)
const checkList = ref([])
const timeValue = ref('')
// 导出方法
const handleExport = async () => {
// exportSpiderData({ times: timeValue.value, filters: checkList.value })
if (!formRef.value) return
await formRef.value.validate(async (valid, fields) => {
console.log('开始校验');
if (valid) {
console.log('校验通过');
const loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const res = await axios.post(
'/api/export/downloadFile',
{ times: exportObject.value.timeValue, filters: exportObject.value.spiderType },
{
responseType: 'blob',
headers: {
'Token': Cookies.get('Admin-Token') || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5MjA5NzM4fQ.6hVko0EQTuz7OYjXEafZYLpmkVEyiLhZ8aWHi0Pni_s', // 根据你的 token 存储方式调整
},
}
)
// 获取文件名
const disposition = res.headers['content-disposition']
let fileName = 'export.zip'
if (disposition) {
const match = disposition.match(/filename="?([^"]+)"?/)
if (match) fileName = decodeURIComponent(match[1])
}
const blob = new Blob([res.data], { type: 'application/zip' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
loading.close()
close()
ElMessage.success('导出成功');
} catch (e) {
console.error('导出失败', e)
close()
ElMessage.error('导出失败');
}
} else {
console.log('校验不通过');
}
})
try {
const res = await axios.post(
'/api/export/downloadFile',
{ times: timeValue.value, filters: checkList.value },
{
responseType: 'blob',
headers: {
'Token': localStorage.getItem('token') || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5MjA5NzM4fQ.6hVko0EQTuz7OYjXEafZYLpmkVEyiLhZ8aWHi0Pni_s', // 根据你的 token 存储方式调整
},
}
)
// 获取文件名
const disposition = res.headers['content-disposition']
let fileName = 'export.zip'
if (disposition) {
const match = disposition.match(/filename="?([^"]+)"?/)
if (match) fileName = decodeURIComponent(match[1])
}
const blob = new Blob([res.data], { type: 'application/zip' })
console.log(blob.type, blob.size);
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
} catch (e) {
console.error('导出失败', e)
}
}
// 关闭弹窗的方法
const close = () => {
formRef.value?.clearValidate()
exportDialogVisible.value = false
}
// 监听父组件传过来的值
......@@ -96,88 +147,21 @@ watch(() => exportDialogVisible.value,
}
)
// const triggerDownload = (blobData: any) => {
// const zipFile = new File([blobData], 'archive.zip', {
// type: 'application/zip',
// });
// console.log(zipFile);
// const fileUrl = URL.createObjectURL(zipFile);
// // 创建下载链接
// const downloadLink = document.createElement('a');
// downloadLink.href = fileUrl;
// // 使用File对象的名称作为下载文件名
// downloadLink.download = zipFile.name;
// // 将链接添加到页面并触发点击
// document.body.appendChild(downloadLink);
// downloadLink.click();
// // 清理资源
// setTimeout(() => {
// // 移除链接元素
// document.body.removeChild(downloadLink);
// // 释放临时URL
// URL.revokeObjectURL(fileUrl);
// }, 0);
// }
// const aaa = (res: any) => {
// // const blob = new Blob([res], { type: 'application/zip' })
// // const link = document.createElement('a')
// // link.style.display = 'none'
// // link.href = URL.createObjectURL(blob)
// // link.setAttribute('download', `${'111'}.zip`)
// // document.body.appendChild(link)
// // link.click()
// // document.body.removeChild(link)
// // const url = window.URL.createObjectURL(new Blob([res], { type: "application/zip" }))
// // const link = document.createElement('a')
// // link.style.display = 'none'
// // link.href = url
// // link.setAttribute('download', '222.zip')
// // document.body.appendChild(link)
// // link.click()
// // document.body.removeChild(link)
// const url = window.URL.createObjectURL(res);
// const a = document.createElement('a');
// a.style.display = 'none';
// a.href = url;
// a.download = 'example.zip';
// document.body.appendChild(a);
// a.click();
// window.URL.revokeObjectURL(url);
// }
// function downloadZipFromBinary(binaryString: any, fileName: string) {
// try {
// if (!fileName.endsWith('.zip')) {
// fileName += '.zip';
// }
// const uint8Array = new Uint8Array(binaryString);
// const blob = new Blob([uint8Array], { type: 'application/zip' });
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = fileName;
// document.body.appendChild(a);
// a.click();
// setTimeout(() => {
// document.body.removeChild(a);
// URL.revokeObjectURL(url);
// }, 100);
// } catch (error) {
// console.error('下载ZIP文件时发生错误:', error);
// throw error;
// }
// }
</script>
<style lang="scss" scoped>
.formStyle {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
</script>
\ No newline at end of file
.checkboxStyle {
display: flex;
gap: 30px;
/* 复选框之间的间距 */
}
</style>
\ No newline at end of file
<template>
<div>
<span>关键词搜索:</span>
<el-input v-model="input2" style="width: 240px" placeholder="Search" :prefix-icon="Search" />
</div>
</template>
<script setup lang="ts">
</script>
\ No newline at end of file
......@@ -88,19 +88,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
]
} as unknown as RouteRecordRaw,
{
path: '/spider',
component: Layout,
hidden: true,
children: [
{
path: 'list',
name: 'spider-list',
component: () => import('@/views/spider/index.vue'),
meta: { title: 'Spider', icon: 'List' }
}
]
} as unknown as RouteRecordRaw,
{
path: '/schedule',
......@@ -186,13 +174,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
meta: { title: "Spider日志", icon: "Notebook" },
hidden: true
},
{
path: "job",
name: "logs-project-spider-job",
component: () => import("@/views/log/LogJob.vue"),
meta: { title: "任务日志", icon: "Notebook" },
hidden: true
}
]
} as unknown as RouteRecordRaw,
......
......@@ -91,6 +91,8 @@ export interface QueryParams {
scrapydServerId?: string
order_prop?: string
order_type?: string
status?: any
job?:any
}
// 请求函数
......
......@@ -52,7 +52,6 @@ import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import SelectScrapydServer from '@/components/SelectScrapydServer.vue';
import SelectProject from '@/components/SelectProject.vue';
import AutoRefresh from '@/components/AutoRefresh.vue';
import JobCancelAll from '@/views/job/components/JobCancelAll.vue';
const props = defineProps({
......
<template>
<div class="app-container">
<AutoRefresh
@refresh="getData"
/>
<el-divider />
<div
class="log-content"
v-loading="listLoading"
>
<pre v-if="content">{{ content }}</pre>
<div v-else>暂无数据</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import AutoRefresh from '@/components/AutoRefresh.vue';
import { getJobLog } from '@/api/log';
const route = useRoute();
const scrapydServerId = ref('');
const project = ref('');
const spider = ref('');
const job = ref('');
const listLoading = ref(true);
const content = ref('');
const getData = async () => {
listLoading.value = true;
try {
const res = await getJobLog({
scrapydServerId: scrapydServerId.value,
project: project.value,
spider: spider.value,
job: job.value,
});
content.value = res as unknown as string || '';
} catch (error) {
console.error('获取任务日志失败:', error);
} finally {
listLoading.value = false;
}
}
onMounted(() => {
project.value = route.query.project as string;
spider.value = route.query.spider as string;
job.value = route.query.job as string;
scrapydServerId.value = route.query.scrapydServerId as string;
getData();
});
</script>
<style lang="scss" scoped>
.log-content {
overflow: auto;
text-align: left;
pre {
margin: 0;
}
}
</style>
......@@ -7,7 +7,7 @@
</el-form-item>
<el-form-item>
<div>
<el-input placeholder="请输入" style="width: 200px" />
<el-input placeholder="请输入" style="width: 190px" />
</div>
</el-form-item>
<el-form-item>
......@@ -15,13 +15,13 @@
</el-form-item>
<el-form-item>
<el-date-picker v-model="timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间"
end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" style="width: 275px" value-format="YYYY-MM-DD HH:mm:ss"/>
end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" style="width: 330px" value-format="YYYY-MM-DD HH:mm:ss"/>
</el-form-item>
<el-form-item>
<el-text class="mx-1">站点名称:</el-text>
</el-form-item>
<el-form-item>
<el-input placeholder="请输入" style="width: 200px" />
<el-input placeholder="请输入" style="width: 180.5px" />
</el-form-item>
<!-- <br></br> -->
<!-- <el-form-item>
......
......@@ -3,32 +3,27 @@
<div class="formStyle">
<el-form inline>
<el-form-item>
<el-text class="mx-1">目标名称:</el-text>
<el-text class="mx-1" >目标名称:</el-text>
</el-form-item>
<el-form-item>
<div>
<el-input placeholder="请输入" style="width: 200px" />
<el-input placeholder="请输入" v-model="searchTargetName" style="width: 190px" />
</div>
</el-form-item>
<el-form-item>
<el-text class="mx-1">获取时间:</el-text>
<el-text class="mx-1" >获取时间:</el-text>
</el-form-item>
<el-form-item>
<el-date-picker v-model="timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始时间" style="width: 250px" end-placeholder="结束时间"
<el-date-picker type="datetimerange" format="YYYY-MM-DD HH:mm:ss" v-model="searchTimeValue"
start-placeholder="开始时间" style="width: 330px" end-placeholder="结束时间"
date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item>
<el-text class="mx-1">SNS Notice ID:</el-text>
</el-form-item>
<el-form-item>
<el-input placeholder="请输入" style="width: 180.5px" />
<el-input placeholder="请输入" v-model="searchTargetId" style="width: 180.5px" />
</el-form-item>
<!-- <el-form-item>
<el-space>
<el-button type="primary" @click="handleExport">导出最近</el-button>
</el-space>
</el-form-item> -->
<el-form-item>
<el-space>
<el-button type="primary" @click="handleSearch">查询</el-button>
......@@ -109,9 +104,6 @@
<div class="pagination w-full flex flex-row-reverse pr-4 m-t-4">
<Pagination :total="pageObj.total" v-model:page="pageObj.pageNo" v-model:limit="pageObj.pageSize"
@pagination="getData" />
<!-- <el-pagination v-model:current-page="pageObj.pageNo" v-model:page-size="pageObj.pageSize" :page-sizes="[10]"
layout="total, sizes, prev, pager, next, jumper" :total="pageObj.total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" /> -->
</div>
</div>
......@@ -127,7 +119,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="SNS Notice ID:" label-position="left">
<el-input v-model="snsId" />
<el-input v-model="snsId" />
</el-form-item>
</el-col>
<el-col :span="12">
......@@ -276,8 +268,10 @@ import Pagination from '@/components/pagination/index.vue'
import exportDialog from '@/components/Export/index.vue'
import { getItuList, getItuDetail, deleteItuData } from '@/api/spiderData'
const searchTargetName = ref('')
const searchTimeValue = ref('')
const searchTargetId = ref('')
const showDeleteDialog = ref(false)
const timeValue = ref('')
const detailVisibleValue = ref(false)
// SNS Notice ID
const snsId = ref('')
......@@ -367,28 +361,14 @@ const getData = async () => {
pageObj.value.total = res.data.total
tableData.value = res.data.list
}
const handleSizeChange = (val: number) => {
// 如果修改后超过最大页面,强制跳转到第 1 页
if (pageObj.value.pageNo * val > pageObj.value.total) {
pageObj.value.pageNo = 1
}
getData()
}
const handleCurrentChange = (val: number) => {
// 触发 pagination 事件,重新加载列表
getData()
}
const handleClose = () => {
detailVisibleValue.value = false
}
const handleExportConfirm = () => {
}
const handleExport = () => {
showDeleteDialog.value = true
}
const handleSearch = async () => {
const res = await getItuList({ sat_name: targetName.value, ntc_id: snsId.value, times: timeValue.value })
const res = await getItuList({ sat_name: searchTargetName.value, ntc_id: searchTargetId.value, times: searchTimeValue.value , page: pageObj.value.pageNo, size: pageObj.value.pageSize })
tableData.value = res.data
}
onMounted(() => {
......
......@@ -7,7 +7,7 @@
</el-form-item>
<el-form-item>
<div>
<el-input placeholder="请输入" style="width: 210px" />
<el-input placeholder="请输入" style="width: 190px" />
</div>
</el-form-item>
<el-form-item>
......@@ -15,7 +15,7 @@
</el-form-item>
<el-form-item>
<el-date-picker v-model="timeValue" type="datetimerange" format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间"
end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" style="width: 250px"
end-placeholder="结束时间" date-format="YYYY/MM/DD ddd" time-format="A hh:mm:ss" style="width: 330px"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item>
......
......@@ -53,8 +53,6 @@ const handleExportConfirm = () => {
}
onMounted(() => {
console.log('数据展示');
console.log(route.query);
if (route.query.mode) {
modeValue.value = route.query.mode
mode.value = modeValue.value
......
......@@ -147,6 +147,9 @@ const currentMode = ref(props.mode)
// 关闭对话框
const handleClose = () => {
firstRuleFormRef.value?.resetFields()
secondRuleFormRef.value?.resetFields()
editPasswordFormRef.value?.resetFields()
currentVisible.value = false
}
......
......@@ -34,6 +34,7 @@ import { addSpiderTask } from '@/api/spiderTask'
import { AddMode } from './enum'
import { Crontab } from '@/components/Crontab/index'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
interface RuleForm {
taskName: string
......@@ -109,6 +110,7 @@ const handleClose = () => {
ruleForm.value.taskName = ''
ruleForm.value.spiderTypeValue
ruleForm.value.cronExpression = ''
ruleFormRef.value?.resetFields()
currentVisible.value = false
}
// 确定的方法
......@@ -129,6 +131,7 @@ const handleConfirm = async () => {
})
currentVisible.value = false
emit('getTaskList')
ElMessage.success('添加成功')
} else if (currentMode.value === AddMode.UPDATE_TASK) {
await addSpiderTask({
scrapyd_server_id: taskParams.value.scrapyd_server_id,
......@@ -141,6 +144,7 @@ const handleConfirm = async () => {
})
currentVisible.value = false
emit('getTaskList')
ElMessage.success('修改成功')
}
} else {
console.log('校验不通过');
......
......@@ -18,6 +18,7 @@
import { ref, watch } from 'vue'
import { defineProps } from 'vue';
import { deleteSpiderTask } from '@/api/spiderTask.ts'
import { ElMessage } from 'element-plus';
interface deleteDialogPropType {
dialogVisible: boolean,
jobId: string
......@@ -30,6 +31,7 @@ const handleDelelte = async () => {
await deleteSpiderTask({job_id: props.jobId})
emit('getUserList')
deleteDialogVisible.value = false
ElMessage.success('删除成功')
}
// 关闭弹窗的方法
......
......@@ -104,7 +104,7 @@ import { DeleteMode } from '@/components/Delete/enum.ts'
import { AddMode } from './enum'
import deleteDialog from './deleteDialog.vue'
import { CronExpressionParser } from 'cron-parser';
import { log } from 'echarts/types/src/util/log.js'
const props = defineProps({
spiderType: {
type: String,
......
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
v-loading="loading"
>
<el-form-item label="项目名称" prop="project">
<el-select
v-model="form.project"
placeholder="请选择项目"
clearable
filterable
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="爬虫名称" prop="spider">
<el-select
v-model="form.spider"
placeholder="请选择爬虫"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in spiderOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="Cron表达式" prop="cron">
<el-input
v-model="form.cron"
placeholder="例如: */5 * * * *"
>
<template #append>
<el-popover
placement="bottom"
title="Cron表达式帮助"
:width="300"
trigger="click"
>
<template #reference>
<el-button>帮助</el-button>
</template>
<div>
<p>Cron表达式格式:分 时 日 月 周</p>
<p>示例:</p>
<ul>
<li>每5分钟:*/5 * * * *</li>
<li>每小时:0 * * * *</li>
<li>每天凌晨1点:0 1 * * *</li>
<li>每周一3点:0 3 * * 1</li>
<li>每月1号零点:0 0 1 * *</li>
</ul>
</div>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item label="调度服务器" prop="schedule_type">
<el-radio-group v-model="form.schedule_type">
<el-radio
v-for="item in scheduleTypeOptions"
:key="item.value"
:label="item.value"
>{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择服务器" prop="scrapyd_server_id" v-if="form.schedule_type === ScheduleTypeEnum.ONLY_ONE_SERVER">
<el-select
v-model="form.scrapyd_server_id"
placeholder="请选择服务器"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in serverOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="爬虫参数" prop="options">
<el-input
v-model="form.options"
type="textarea"
:rows="4"
placeholder='{"param1": "value1", "param2": "value2"}'
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submitForm"
:loading="submitting"
>
确认
</el-button>
<el-button @click="cancelForm">取消</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import * as scheduleApi from '@/api/schedule'
import * as projectApi from '@/api/project'
import { ScheduleTypeOptions, ScheduleTypeEnum } from '@/enums/schedule-type-enum'
import type { QueryParams } from '@/utils/request'
const props = defineProps({
job_id: {
type: String,
default: ''
},
scrapydServerId: {
type: String,
default: ''
},
project: {
type: String,
default: ''
},
spider: {
type: String,
default: ''
}
})
const emit = defineEmits(['on-success', 'on-cancel'])
const formRef = ref<FormInstance>()
const loading = ref(false)
const submitting = ref(false)
interface OptionType {
label: string
value: string
}
const projectOptions = ref<OptionType[]>([])
const spiderOptions = ref<OptionType[]>([])
const serverOptions = ref<OptionType[]>([])
const scheduleTypeOptions = ref(ScheduleTypeOptions)
const form = reactive({
project: props.project || '',
spider: props.spider || '',
cron: '*/5 * * * *', // 默认每5分钟执行一次
schedule_type: ScheduleTypeEnum.ONLY_ONE_SERVER,
scrapyd_server_id: props.scrapydServerId || '',
options: '{}'
})
const rules = reactive({
project: [
{ required: true, message: '请选择项目', trigger: 'change' }
],
spider: [
{ required: true, message: '请选择爬虫', trigger: 'change' }
],
cron: [
{ required: true, message: '请输入Cron表达式', trigger: 'blur' }
],
schedule_type: [
{ required: true, message: '请选择调度方式', trigger: 'change' }
],
scrapyd_server_id: [
{
required: true,
message: '请选择服务器',
trigger: 'change',
validator: (_rule: any, value: any, callback: any) => {
if (form.schedule_type === ScheduleTypeEnum.ONLY_ONE_SERVER && !value) {
callback(new Error('请选择服务器'))
} else {
callback()
}
}
}
]
})
// 初始化数据
const initData = async () => {
loading.value = true
try {
// 获取项目列表
await getProjects()
// 获取服务器列表
await getScrapydServers()
// 如果有job_id,则获取任务详情
if (props.job_id) {
await getJobDetail()
}
// 如果有项目名,则获取爬虫列表
if (form.project) {
await getSpiders()
}
} catch (error) {
console.error('初始化数据失败:', error)
} finally {
loading.value = false
}
}
// 获取项目列表
const getProjects = async () => {
try {
const scrapyd_server_id = localStorage.getItem('scrapyd_server_id')
const res = await projectApi.getProjectList({ scrapydServerId: scrapyd_server_id! })
if (res.code === 0) {
projectOptions.value = res.data.map((item: any) => ({
label: item.name,
value: item.name
}))
}
} catch (error) {
console.error('获取项目列表失败:', error)
}
}
// 获取爬虫列表
const getSpiders = async () => {
if (!form.project) return
try {
const res = await projectApi.getSpiders({ project: form.project, scrapydServerId: localStorage.getItem('scrapyd_server_id')! })
if (res.code === 0 && Array.isArray(res.data)) {
spiderOptions.value = res.data.map((item: string) => ({
label: item,
value: item
}))
}
} catch (error) {
console.error('获取爬虫列表失败:', error)
}
}
// 获取服务器列表
const getScrapydServers = async () => {
try {
const res = await scheduleApi.getScrapydServerPage()
if (res.code === 0) {
serverOptions.value = res.data.list.map((item: any) => ({
label: item.server_name,
value: item.id
}))
}
} catch (error) {
console.error('获取服务器列表失败:', error)
}
}
// 获取任务详情
const getJobDetail = async () => {
try {
const res = await scheduleApi.getJobDetail({ job_id: props.job_id, scrapydServerId: localStorage.getItem('scrapyd_server_id')! })
if (res.code === 0) {
const jobData = res.data
form.project = jobData.kwargs.project || ''
form.spider = jobData.kwargs.spider || ''
form.cron = jobData.kwargs.cron || ''
form.schedule_type = jobData.kwargs.schedule_type || ScheduleTypeEnum.ONLY_ONE_SERVER
form.scrapyd_server_id = jobData.kwargs.scrapyd_server_id || ''
form.options = jobData.kwargs.options ? JSON.stringify(jobData.kwargs.options) : '{}'
}
} catch (error) {
console.error('获取任务详情失败:', error)
}
}
// 项目变更时获取对应的爬虫
const handleProjectChange = async () => {
form.spider = ''
await getSpiders()
}
// 提交表单
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
try {
let options = {}
try {
options = JSON.parse(form.options)
} catch (e) {
ElMessage.error('爬虫参数格式错误,请检查JSON格式')
submitting.value = false
return
}
const params: QueryParams = {
project: form.project,
spider: form.spider,
cron: form.cron,
schedule_type: form.schedule_type,
scrapydServerId: form.scrapyd_server_id,
options
}
if (form.schedule_type === ScheduleTypeEnum.ONLY_ONE_SERVER) {
params.scrapyd_server_id = form.scrapyd_server_id
}
if (props.job_id) {
params.job_id = props.job_id
}
const res = await scheduleApi.addJob(params)
if (res.code === 0) {
ElMessage({
type: 'success',
message: props.job_id ? '修改成功!' : '添加成功!'
})
emit('on-success')
}
} catch (error) {
console.error(props.job_id ? '修改任务失败:' : '添加任务失败:', error)
} finally {
submitting.value = false
}
}
})
}
// 取消表单
const cancelForm = () => {
emit('on-cancel')
}
// 监听项目变化
watch(() => form.project, async (newVal) => {
if (newVal) {
await getSpiders()
}
})
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
</style>
<template>
<el-form
:model="form"
ref="formRef"
label-width="100px"
size="small"
:rules="rules"
>
<el-form-item
label="服务名称"
prop="scrapyd_server_id"
>
<el-select
v-model="form.scrapyd_server_id"
placeholder="请选择服务器"
style="width: 100%"
:disabled="scrapydServerId !== ''"
@change="handleServerChange"
>
<el-option
v-for="server in serverOptions"
:key="server.value"
:label="server.label"
:value="server.value"
/>
</el-select>
</el-form-item>
<el-form-item
label="项目名称"
prop="project"
>
<el-select
v-model="form.project"
placeholder="请选择项目"
style="width: 100%"
:disabled="project !== ''"
@change="handleProjectChange"
>
<el-option
v-for="proj in projectOptions"
:key="proj.value"
:label="proj.label"
:value="proj.value"
/>
</el-select>
</el-form-item>
<el-form-item
label="Spider名称"
prop="spider"
>
<el-select
v-model="form.spider"
placeholder="请选择爬虫"
style="width: 100%"
:disabled="spider !== ''"
filterable
>
<el-option
v-for="item in spiderOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
label="其他参数"
prop="options"
>
<el-input
type="textarea"
rows="3"
v-model="form.options"
placeholder='任何参数都将作为spider参数传递,例如:{"setting": "DOWNLOAD_DELAY=2"}'
/>
</el-form-item>
<el-form-item>
<el-button
size="small"
@click="handleCancel"
>取 消</el-button>
<el-button
type="primary"
size="small"
@click="submit"
:loading="submitting"
>立即执行</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import * as projectApi from '@/api/project'
import * as systemApi from '@/api/system'
const props = defineProps({
scrapydServerId: {
type: String,
default: ''
},
project: {
type: String,
default: ''
},
spider: {
type: String,
default: ''
},
options: {
type: String,
default: ''
}
})
const emit = defineEmits(['on-success', 'on-cancel'])
const formRef = ref<FormInstance>()
const submitting = ref(false)
interface OptionType {
label: string
value: string
}
const serverOptions = ref<OptionType[]>([])
const projectOptions = ref<OptionType[]>([])
const spiderOptions = ref<OptionType[]>([])
const form = reactive({
scrapyd_server_id: props.scrapydServerId || '',
project: props.project || '',
spider: props.spider || '',
options: props.options || '{}'
})
const rules = reactive({
scrapyd_server_id: [
{ required: true, message: '请选择服务器', trigger: 'change' }
],
project: [
{ required: true, message: '请选择项目', trigger: 'change' }
],
spider: [
{ required: true, message: '请选择爬虫', trigger: 'change' }
]
})
// 初始化数据
const initData = async () => {
await getServerOptions()
if (form.scrapyd_server_id) {
await getProjectOptions()
if (form.project) {
await getSpiderOptions()
}
}
}
// 获取服务器选项
const getServerOptions = async () => {
try {
const res = await systemApi.getScrapydServerList()
if (res.code === 0) {
serverOptions.value = res.data.map((server: any) => ({
label: server.server_name,
value: server.id
}))
}
} catch (error) {
console.error('获取服务器列表失败:', error)
}
}
// 获取项目选项
const getProjectOptions = async () => {
if (!form.scrapyd_server_id) return
try {
const res = await projectApi.listProjects({ scrapydServerId: form.scrapyd_server_id })
if (res.code === 0) {
projectOptions.value = res.data.map((project: string) => ({
label: project,
value: project
}))
}
} catch (error) {
console.error('获取项目列表失败:', error)
}
}
// 获取爬虫选项
const getSpiderOptions = async () => {
if (!form.scrapyd_server_id || !form.project) return
try {
const res = await projectApi.listSpiders({
scrapydServerId: form.scrapyd_server_id,
project: form.project
})
if (res.code === 0) {
spiderOptions.value = res.data.map((spider: string) => ({
label: spider,
value: spider
}))
}
} catch (error) {
console.error('获取爬虫列表失败:', error)
}
}
// 服务器变更处理
const handleServerChange = async () => {
form.project = ''
form.spider = ''
projectOptions.value = []
spiderOptions.value = []
await getProjectOptions()
}
// 项目变更处理
const handleProjectChange = async () => {
form.spider = ''
spiderOptions.value = []
await getSpiderOptions()
}
// 取消按钮
const handleCancel = () => {
emit('on-cancel')
}
// 提交表单
const submit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
try {
// 解析可选参数
let parsedOptions = {}
if (form.options) {
try {
parsedOptions = JSON.parse(form.options)
} catch (e) {
ElMessage.error('其他参数格式错误,请检查JSON格式')
submitting.value = false
return
}
}
// 执行爬虫
const res = await projectApi.schedule({
scrapydServerId: form.scrapyd_server_id,
project: form.project,
spider: form.spider,
option: parsedOptions
})
if (res.code === 0) {
ElMessage({
type: 'success',
message: '任务提交成功: ' + res.data.jobid
})
emit('on-success')
}
} catch (error) {
console.error('提交执行任务失败:', error)
} finally {
submitting.value = false
}
}
})
}
onMounted(() => {
initData()
})
</script>
<template>
<el-button
size="small"
type="text"
@click="handleSchedule"
>
<el-icon><VideoPlay /></el-icon>
</el-button>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { VideoPlay } from '@element-plus/icons-vue'
import { schedule } from '@/api/project'
const props = defineProps({
spider: { type: String, default: '' },
project: { type: String, default: '' },
scrapydServerId: { type: String, default: '' },
options: { type: String, default: '' }
})
const emit = defineEmits(['success'])
// 处理运行爬虫
const handleSchedule = async () => {
try {
// 解析可选参数
let parsedOptions = {}
if (props.options) {
try {
parsedOptions = JSON.parse(props.options)
} catch (e) {
ElMessage.error('参数格式错误,请检查JSON格式')
return
}
}
const res = await schedule({
scrapydServerId: props.scrapydServerId,
project: props.project,
spider: props.spider,
option: parsedOptions
})
if (res.code === 0) {
ElMessage({
type: 'success',
message: '爬虫已启动: ' + res.data.jobid
})
emit('success')
} else {
ElMessage.error(res.msg || '运行失败')
}
} catch (error) {
console.error('运行爬虫失败:', error)
ElMessage.error('运行爬虫失败')
}
}
</script>
<style lang="scss" scoped>
</style>
<template>
<div class="app-container">
<div class="page-header">
<el-button @click="goBack">
<el-icon><Back /></el-icon>返回
</el-button>
<h2>爬虫管理: {{ project }}</h2>
</div>
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>爬虫列表</span>
<div class="right-panel">
<el-input
v-model="searchQuery"
placeholder="搜索爬虫"
clearable
class="search-input"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="refreshSpiderList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</template>
<el-table v-loading="loading" :data="filteredSpiderList" stripe border style="width: 100%">
<el-table-column label="序号" width="60" align="center">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="spider" label="爬虫名称" align="center"/>
<el-table-column label="调度日志" width="100" align="center">
<template #default="scope">
<router-link
:to="{
path: '/schedule-log/list',
query: {
project: project,
spider: scope.row.spider,
scrapydServerId: scrapydServerId
}
}"
target="_blank"
>
<el-button type="info" size="small">
<el-icon><Document /></el-icon>日志
</el-button>
</router-link>
</template>
</el-table-column>
<el-table-column label="运行统计" width="100" align="center">
<template #default="scope">
<router-link
:to="{
path: '/stats/list',
query: {
project: project,
spider: scope.row.spider,
scrapydServerId: scrapydServerId
}
}"
target="_blank"
>
<el-button type="warning" size="small">
<el-icon><Histogram /></el-icon>统计
</el-button>
</router-link>
</template>
</el-table-column>
<el-table-column label="操作" width="220">
<template #default="scope">
<el-button type="primary" size="small" @click="handleRunSpider(scope.row)">
<el-icon><VideoPlay /></el-icon>运行
</el-button>
<el-button type="success" size="small" @click="viewLogs(scope.row)">
<el-icon><Document /></el-icon>查看日志
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 运行爬虫对话框 -->
<el-dialog v-model="runDialogVisible" title="运行爬虫" width="500px">
<el-form :model="runForm" label-width="120px" :rules="rules" ref="runFormRef">
<el-form-item label="爬虫名称">
<el-input v-model="runForm.spider" disabled />
</el-form-item>
<el-form-item label="任务ID" prop="jobid">
<el-input v-model="runForm.jobid" placeholder="请输入任务ID" />
</el-form-item>
<el-form-item label="爬虫参数">
<el-input
v-model="runForm.spider_args"
type="textarea"
:rows="4"
placeholder="请输入爬虫参数,格式为: key=value key2=value2"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="runDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRunSpider" :loading="submitLoading">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getSpiderList, scheduleJob } from '@/api/project'
import { Back, Refresh, VideoPlay, Document, Search, Histogram } from '@element-plus/icons-vue'
import { v4 as uuidv4 } from 'uuid'
const route = useRoute()
const router = useRouter()
const project = ref('')
const scrapydServerId = ref('')
const loading = ref(false)
const spiderList = ref<any[]>([])
const runDialogVisible = ref(false)
const submitLoading = ref(false)
const searchQuery = ref('')
const runForm = ref({
spider: '',
jobid: '',
spider_args: ''
})
const runFormRef = ref()
// 根据搜索条件过滤爬虫列表
const filteredSpiderList = computed(() => {
if (!searchQuery.value) return spiderList.value
return spiderList.value.filter(item =>
item.spider.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
// 表单验证规则
const rules = {
jobid: [
{ required: true, message: '请输入任务ID', trigger: 'blur' }
]
}
// 处理搜索
const handleSearch = () => {
// 搜索已由计算属性处理
}
// 返回项目列表
const goBack = () => {
router.push('/project')
}
// 获取爬虫列表
const fetchSpiderList = async () => {
if (!project.value || !scrapydServerId.value) return
loading.value = true
try {
const response = await getSpiderList({
project: project.value,
scrapydServerId: scrapydServerId.value
})
spiderList.value = response.data
} catch (error) {
console.error('获取爬虫列表失败:', error)
ElMessage.error('获取爬虫列表失败')
} finally {
loading.value = false
}
}
// 刷新爬虫列表
const refreshSpiderList = () => {
fetchSpiderList()
}
// 运行爬虫
const handleRunSpider = (row: any) => {
runDialogVisible.value = true
runForm.value = {
spider: row.spider,
jobid: uuidv4(),
spider_args: ''
}
}
// 提交运行爬虫
const submitRunSpider = async () => {
if (!runFormRef.value) return
await runFormRef.value.validate(async (valid: any) => {
if (valid) {
submitLoading.value = true
try {
await scheduleJob({
project: project.value,
spider: runForm.value.spider,
scrapydServerId: scrapydServerId.value,
jobid: runForm.value.jobid,
spider_args: runForm.value.spider_args,
})
ElMessage.success('爬虫已启动')
runDialogVisible.value = false
} catch (error) {
console.error('启动爬虫失败:', error)
ElMessage.error('启动爬虫失败')
} finally {
submitLoading.value = false
}
}
})
}
// 查看日志
const viewLogs = (row: any) => {
router.push({
path: '/logs/spider',
query: {
project: project.value,
spider: row.spider,
scrapydServerId: scrapydServerId.value
}
})
}
onMounted(() => {
project.value = route.query.project as string
scrapydServerId.value = window.localStorage.getItem('scrapyd_server_id') || ''
if (!project.value) {
ElMessage.error('缺少项目参数')
router.push('/project')
return
}
if (!scrapydServerId.value) {
ElMessage.error('缺少服务器ID参数')
router.push('/project')
return
}
fetchSpiderList()
})
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.page-header {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
h2 {
margin: 0;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.right-panel {
display: flex;
gap: 10px;
}
.search-input {
width: 200px;
}
.el-icon {
margin-right: 5px;
}
</style>
<template>
<div class="app-container">
<div class="page-header">
<el-button @click="goBack">
<el-icon><Back /></el-icon>返回
</el-button>
<h2>爬虫日志: {{ project }} / {{ spider }}</h2>
</div>
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>日志列表</span>
<div class="right-panel">
<el-button type="primary" @click="refreshLogList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</template>
<el-table v-loading="loading" :data="logList" stripe style="width: 100%">
<el-table-column prop="logfile" label="日志文件" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button type="primary" size="small" @click="viewLog(scope.row)">
查看日志内容
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 日志内容对话框 -->
<el-dialog v-model="logDialogVisible" :title="currentLog" width="80%" top="5vh">
<div v-loading="logLoading" class="log-content">
<pre>{{ logContent }}</pre>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getSpiderLogs, getJobLog } from '@/api/log'
import { Back, Refresh } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const project = ref('')
const spider = ref('')
const loading = ref(false)
const logList = ref([])
const logDialogVisible = ref(false)
const logLoading = ref(false)
const logContent = ref('')
const currentLog = ref('')
// 返回爬虫列表
const goBack = () => {
router.push({
path: '/spider',
query: { project: project.value }
})
}
// 获取日志列表
const fetchLogList = async () => {
if (!project.value || !spider.value) return
loading.value = true
try {
const response = await getSpiderLogs({
project: project.value,
spider: spider.value,
scrapydServerId: localStorage.getItem('scrapydServerId')!
})
logList.value = response.data.logs.map((logfile: any) => ({ logfile }))
} catch (error) {
console.error('获取日志列表失败:', error)
ElMessage.error('获取日志列表失败')
} finally {
loading.value = false
}
}
// 刷新日志列表
const refreshLogList = () => {
fetchLogList()
}
// 查看日志内容
const viewLog = async (row: any) => {
currentLog.value = row.logfile
logDialogVisible.value = true
logContent.value = ''
logLoading.value = true
try {
const response = await getJobLog({
project: project.value,
spider: spider.value,
job: row.logfile.replace('.log', ''),
scrapydServerId: localStorage.getItem('scrapydServerId')!
})
logContent.value = response.data.log || '无日志内容'
} catch (error) {
console.error('获取日志内容失败:', error)
ElMessage.error('获取日志内容失败')
logContent.value = '获取日志内容失败'
} finally {
logLoading.value = false
}
}
onMounted(() => {
project.value = route.query.project as string
spider.value = route.query.spider as string
if (!project.value || !spider.value) {
ElMessage.error('缺少必要参数')
router.push('/project')
return
}
fetchLogList()
})
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.page-header {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
h2 {
margin: 0;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.right-panel {
display: flex;
gap: 10px;
}
.log-content {
max-height: 70vh;
overflow-y: auto;
background: #f5f7fa;
padding: 16px;
border-radius: 4px;
pre {
white-space: pre-wrap;
font-family: monospace;
margin: 0;
}
}
.el-icon {
margin-right: 5px;
}
</style>
<template>
<div class="stats-table">
<el-table
v-bind="$attrs"
:data="data"
border
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column
align="center"
label="序号"
width="60"
>
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
label="项目名"
align="center"
width="150px"
prop="project"
>
<template #default="scope">
{{ scope.row.project || '-' }}
</template>
</el-table-column>
<el-table-column
label="Spider"
align="center"
prop="spider"
>
<template #default="scope">
{{ scope.row.spider }}
</template>
</el-table-column>
<el-table-column
label="收集数量"
align="center"
width="80px"
prop="item_scraped_count"
>
<template #default="scope">
{{ scope.row.item_scraped_count }}
</template>
</el-table-column>
<el-table-column
label="丢弃数量"
align="center"
width="80px"
prop="item_dropped_count"
>
<template #default="scope">
{{ scope.row.item_dropped_count }}
</template>
</el-table-column>
<el-table-column
label="错误日志"
align="center"
width="110px"
prop="log_error_count"
sortable="custom"
>
<template #default="scope">
{{ scope.row.log_error_count }}
</template>
</el-table-column>
<el-table-column
label="持续时间"
align="right"
width="110px"
prop="duration"
sortable="custom"
>
<template #default="scope">
{{ scope.row.duration_str }}
</template>
</el-table-column>
<el-table-column
label="开始时间"
align="center"
width="170px"
prop="start_time"
>
<template #default="scope">
{{ scope.row.start_time }}
</template>
</el-table-column>
<el-table-column
label="结束时间"
align="center"
width="170px"
prop="finish_time"
>
<template #default="scope">
{{ scope.row.finish_time }}
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="180px"
>
<template #default="scope">
<el-button
type="primary"
size="small"
@click="viewDetail(scope.row)"
>
详情
</el-button>
<el-button
type="danger"
size="small"
@click="confirmRemove(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
<el-table-column
label="运行日志"
align="center"
width="80px"
>
<template #default="scope">
<router-link
:to="{
name: 'SpiderLogs',
query: {
scrapydServerId: scope.row.scrapyd_server_id,
project: scope.row.project,
spider: scope.row.spider,
job: scope.row.spider_job_id
}
}"
target="_blank"
>
<el-icon><Document /></el-icon> 日志
</router-link>
</template>
</el-table-column>
</el-table>
<!-- 详情对话框 -->
<el-dialog
v-model="dialogVisible"
title="统计详情"
width="70%"
>
<div v-if="currentDetail" class="stats-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目名">{{ currentDetail.project }}</el-descriptions-item>
<el-descriptions-item label="爬虫名">{{ currentDetail.spider }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ currentDetail.start_time }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ currentDetail.finish_time }}</el-descriptions-item>
<el-descriptions-item label="收集数量">{{ currentDetail.item_scraped_count }}</el-descriptions-item>
<el-descriptions-item label="丢弃数量">{{ currentDetail.item_dropped_count }}</el-descriptions-item>
<el-descriptions-item label="持续时间">{{ currentDetail.duration_str }}</el-descriptions-item>
<el-descriptions-item label="结束原因">{{ currentDetail.finish_reason || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="detail-section" v-if="currentDetail.stats">
<h3>统计详情</h3>
<el-table :data="statsTableData" border style="width: 100%">
<el-table-column prop="key" label="指标名称" min-width="250px" />
<el-table-column prop="value" label="值" min-width="100px" />
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, defineProps, defineEmits, computed } from 'vue'
import { Document } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import * as statsApi from '@/api/stats'
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['sort-change'])
const dialogVisible = ref(false)
const currentDetail = ref<Record<string, any> | null>(null)
// 统计数据表格展示
const statsTableData = computed(() => {
if (!currentDetail.value || !currentDetail.value.stats) return []
return Object.entries(currentDetail.value.stats)
.filter(([key]) => key !== 'spider' && key !== 'project') // 过滤掉已显示在上方的字段
.map(([key, value]) => ({
key,
value: String(value)
}))
.sort((a, b) => a.key.localeCompare(b.key))
})
// 查看详情
const viewDetail = async (row: Record<string, any>) => {
try {
const res = await statsApi.getStatsDetail({
scrapydServerId: row.scrapyd_server_id,
spider_job_id: row.spider_job_id
})
if (res.code === 0) {
currentDetail.value = res.data
dialogVisible.value = true
}
} catch (error) {
console.error('获取统计详情失败:', error)
}
}
// 确认删除
const confirmRemove = (row: Record<string, any>) => {
ElMessageBox.confirm('确认删除该统计记录?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await statsApi.removeStats({
scrapydServerId: row.scrapyd_server_id,
spider_job_id: row.spider_job_id
})
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
emit('sort-change', { prop: '', order: '' }) // 触发刷新
}
} catch (error) {
console.error('删除统计记录失败:', error)
}
})
.catch(() => {
// 取消操作
})
}
// 排序变化
const handleSortChange = (val: { prop: string, order: string }) => {
emit('sort-change', val)
}
</script>
<style lang="scss" scoped>
.stats-table {
margin-bottom: 20px;
}
.stats-detail {
.detail-section {
margin-top: 20px;
h3 {
margin-bottom: 15px;
font-weight: 500;
font-size: 16px;
}
}
}
</style>
......@@ -43,7 +43,6 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import StatsTable from '@/views/stats/components/StatsTable.vue'
import StatsTool from '@/views/stats/components/StatsTool.vue'
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn';
......
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