Commit cff37174 by 周田

Merge branch 'yzh' into 'main'

Yzh

See merge request !9
parents c5b55285 d8f4cbdf
VITE_APP_BASE_API = '/api' VITE_APP_BASE_API = 'http://127.0.0.1:5001/api'
...@@ -22,3 +22,5 @@ dist-ssr ...@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
minimal-repro/*
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"cron-parser": "^5.4.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.10", "element-plus": "^2.9.10",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
......
...@@ -26,6 +26,9 @@ importers: ...@@ -26,6 +26,9 @@ importers:
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
cron-parser:
specifier: ^5.4.0
version: 5.4.0
echarts: echarts:
specifier: ^5.6.0 specifier: ^5.6.0
version: 5.6.0 version: 5.6.0
...@@ -706,6 +709,10 @@ packages: ...@@ -706,6 +709,10 @@ packages:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'} engines: {node: '>=12.13'}
cron-parser@5.4.0:
resolution: {integrity: sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==}
engines: {node: '>=18'}
css-tree@3.1.0: css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
...@@ -918,6 +925,10 @@ packages: ...@@ -918,6 +925,10 @@ packages:
lodash@4.17.21: lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
magic-string@0.30.17: magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
...@@ -1967,6 +1978,10 @@ snapshots: ...@@ -1967,6 +1978,10 @@ snapshots:
dependencies: dependencies:
is-what: 4.1.16 is-what: 4.1.16
cron-parser@5.4.0:
dependencies:
luxon: 3.7.2
css-tree@3.1.0: css-tree@3.1.0:
dependencies: dependencies:
mdn-data: 2.12.2 mdn-data: 2.12.2
...@@ -2179,6 +2194,8 @@ snapshots: ...@@ -2179,6 +2194,8 @@ snapshots:
lodash@4.17.21: {} lodash@4.17.21: {}
luxon@3.7.2: {}
magic-string@0.30.17: magic-string@0.30.17:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
......
...@@ -95,6 +95,7 @@ export const spiderTaskApi = { ...@@ -95,6 +95,7 @@ export const spiderTaskApi = {
pauseJob: '/schedule/pauseJob', pauseJob: '/schedule/pauseJob',
resumeJob: '/schedule/resumeJob', resumeJob: '/schedule/resumeJob',
jobDetail: '/schedule/jobDetail', jobDetail: '/schedule/jobDetail',
taskCount:'/schedule/getAllJobCount'
} as const } as const
// 爬虫数据相关接口 // 爬虫数据相关接口
...@@ -105,4 +106,22 @@ export const spiderDataApi = { ...@@ -105,4 +106,22 @@ export const spiderDataApi = {
ituList: '/itu/list', ituList: '/itu/list',
ituDetail: '/itu/detail', ituDetail: '/itu/detail',
ituDataDelete: '/itu/delete', ituDataDelete: '/itu/delete',
stList: '/spaceTrack/list',
stDetail: '/spaceTrack/detail',
stDataDelete: '/sspaceTrackt/delete',
exportSpiderData: '/export/downloadFile',
esaList: '/esa/list',
esaMissionDetail: '/esa/missionDetail',
esaStationDetail: '/esa/stationDetail',
} as const
// 爬虫数据相关接口
export const dataApi = {
dataStatistics: '/statistic/dataStatistics',
taskStatistics: '/statistic/taskStatistics',
performanceStatistics: '/statistic/performanceStatistics',
allSpiderTaskStatistics: '/statistic/getAllTaskStatistics',
} as const } as const
...@@ -3,7 +3,7 @@ import type { ApiResponse, QueryParams, UserQueryParams } from '@/utils/request' ...@@ -3,7 +3,7 @@ import type { ApiResponse, QueryParams, UserQueryParams } from '@/utils/request'
import { spiderDataApi } from './apiPaths' import { spiderDataApi } from './apiPaths'
// 获取dsn数据列表 // 获取dsn数据列表
export function getDsnlist(data: UserQueryParams) { export function getDsnList(data: UserQueryParams) {
return request({ return request({
url: spiderDataApi.dsnList, url: spiderDataApi.dsnList,
method: POST, method: POST,
...@@ -30,7 +30,7 @@ export function deleteDsnData(data: UserQueryParams) { ...@@ -30,7 +30,7 @@ export function deleteDsnData(data: UserQueryParams) {
} }
// 获取itu数据列表 // 获取itu数据列表
export function getItulist(data: UserQueryParams) { export function getItuList(data: UserQueryParams) {
return request({ return request({
url: spiderDataApi.ituList, url: spiderDataApi.ituList,
method: POST, method: POST,
...@@ -55,3 +55,67 @@ export function deleteItuData(data: UserQueryParams) { ...@@ -55,3 +55,67 @@ export function deleteItuData(data: UserQueryParams) {
data data
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>
} }
// 获取st数据列表
export function getStList(data: UserQueryParams) {
return request({
url: spiderDataApi.stList,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 获取st数据详情
export function getStDetail(data: UserQueryParams) {
return request({
url: spiderDataApi.stDetail,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 删除st数据
export function deleteStData(data: UserQueryParams) {
return request({
url: spiderDataApi.stDataDelete,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 导出爬虫数据
export function exportSpiderData(data: UserQueryParams) {
return request({
url: spiderDataApi.exportSpiderData,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 获取esa数据列表
export function getESAList(data: UserQueryParams) {
return request({
url: spiderDataApi.esaList,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 获取esa任务详情
export function getESAMissionDetail(data: UserQueryParams) {
return request({
url: spiderDataApi.esaMissionDetail,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
// 获取esa站点详情
export function getESAStationDetail(data: UserQueryParams) {
return request({
url: spiderDataApi.esaStationDetail,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
...@@ -64,3 +64,12 @@ export function getTaskData(data: UserQueryParams) { ...@@ -64,3 +64,12 @@ export function getTaskData(data: UserQueryParams) {
data data
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>
} }
// 获取爬虫任务统计
export function getTaskCount(data: UserQueryParams) {
return request({
url: spiderTaskApi.taskCount,
method: POST,
data
}) as unknown as Promise<ApiResponse>
}
\ No newline at end of file
import { request, POST } from '@/utils/request'
import type { ApiResponse, QueryParams } from '@/utils/request'
import { dataApi } from './apiPaths'
// 获取爬虫静态数据列表
export function getStatsDataList(params: QueryParams): Promise<ApiResponse> {
return request({
url: dataApi.dataStatistics,
method: POST,
params
}) as unknown as Promise<ApiResponse>
}
// 获取爬虫任务列表
export function getSpiderTaskList(params: QueryParams): Promise<ApiResponse> {
return request({
url: dataApi.taskStatistics,
method: POST,
params
}) as unknown as Promise<ApiResponse>
}
// 获取性能统计列表
export function getPerformanceList(params: QueryParams): Promise<ApiResponse> {
return request({
url: dataApi.performanceStatistics,
method: POST,
params
}) as unknown as Promise<ApiResponse>
}
// 获取爬虫任务统计列表
export function getAllSpiderTaskStatistics(params: QueryParams): Promise<ApiResponse> {
return request({
url: dataApi.allSpiderTaskStatistics,
method: POST,
params
}) as unknown as Promise<ApiResponse>
}
\ No newline at end of file
<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>
<!-- 基于 ruoyi-vue3 的 Pagination 重构,核心是简化无用的属性,并使用 ts 重写 --> <!-- 基于 ruoyi-vue3 的 Pagination 重构,核心是简化无用的属性,并使用 ts 重写 -->
<template> <template>
<el-config-provider :locale="zhCn">
<el-pagination <el-pagination
v-show="total > 0" v-show="total > 0"
v-model:current-page="currentPage" v-model:current-page="currentPage"
...@@ -9,15 +10,27 @@ ...@@ -9,15 +10,27 @@
:pager-count="pagerCount" :pager-count="pagerCount"
:total="total" :total="total"
class="mt-4" class="mt-4"
layout="total, sizes, prev, pager, next, jumper" layout="total,sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
jumper-text="跳转"
/> />
</el-config-provider>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
// ElConfigProvider 组件
import { ElConfigProvider } from 'element-plus';
// 引入中文包
import zhCn from 'element-plus/es/locale/lang/zh-cn';
defineOptions({ name: 'Pagination' }) defineOptions({ name: 'Pagination' })
// 更改分页文字
zhCn.el.pagination.total = '共 `{total} 条`';
zhCn.el.pagination.goto = '跳至';
zhCn.el.pagination.pagesize = '条/页';
zhCn.el.pagination.pageClassifier = '页';
const props = defineProps({ const props = defineProps({
// 总条目数 // 总条目数
total: { total: {
......
<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
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { createRouter, createWebHashHistory, createWebHistory, type RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue' import Layout from '@/layout/index.vue'
...@@ -40,12 +40,12 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -40,12 +40,12 @@ export const constantRoutes: Array<RouteRecordRaw> = [
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: '/dashboard', redirect: '/osStatus/list',
hidden: true, hidden: true,
children: [{ children: [{
path: '/dashboard', path: '/osStatus/list',
name: 'Dashboard', name: 'statusMonitor',
component: () => import('@/views/home/home.vue'), component: () => import('@/views/os-status/index.vue'),
meta: { title: '控制台', icon: 'Monitor' } meta: { title: '控制台', icon: 'Monitor' }
}] }]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -88,19 +88,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -88,19 +88,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
] ]
} as unknown as 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', path: '/schedule',
...@@ -186,13 +174,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -186,13 +174,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
meta: { title: "Spider日志", icon: "Notebook" }, meta: { title: "Spider日志", icon: "Notebook" },
hidden: true 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, } as unknown as RouteRecordRaw,
...@@ -216,14 +198,13 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -216,14 +198,13 @@ export const constantRoutes: Array<RouteRecordRaw> = [
{ {
path: "/osStatus", path: "/osStatus",
// redirect: "list",
component: Layout, component: Layout,
children: [ children: [
{ {
path: "list", path: "list",
name: "statusMonitor", name: "statusMonitor",
component: () => import("@/views/os-status/index.vue"), component: () => import("@/views/os-status/index.vue"),
meta: { title: "状态监控", icon: "Key" } meta: { title: "状态监控", icon: "Monitor" }
}, },
] ]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -237,7 +218,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -237,7 +218,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: "list", path: "list",
name: "taskInformation", name: "taskInformation",
component: () => import("@/views/os-taskInformation/index.vue"), component: () => import("@/views/os-taskInformation/index.vue"),
meta: { title: "任务信息", icon: "Key" } meta: { title: "任务信息", icon: "List" }
}, },
] ]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -250,7 +231,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -250,7 +231,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: "list", path: "list",
name: "taskRecord", name: "taskRecord",
component: () => import("@/views/os-taskRecord/index.vue"), component: () => import("@/views/os-taskRecord/index.vue"),
meta: { title: "任务执行记录", icon: "Key" } meta: { title: "任务执行记录", icon: "Notebook" }
}, },
] ]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -263,7 +244,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -263,7 +244,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: "list", path: "list",
name: "dataDisplay", name: "dataDisplay",
component: () => import("@/views/os-dataDisplay/index.vue"), component: () => import("@/views/os-dataDisplay/index.vue"),
meta: { title: "数据展示", icon: "Key" } meta: { title: "数据展示", icon: "DataAnalysis" }
}, },
] ]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -289,7 +270,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -289,7 +270,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: "list", path: "list",
name: "osSystem", name: "osSystem",
component: () => import("@/views/os-system/index.vue"), component: () => import("@/views/os-system/index.vue"),
meta: { title: "系统管理", icon: "Key" } meta: { title: "系统管理", icon: "Setting" }
}, },
] ]
} as unknown as RouteRecordRaw, } as unknown as RouteRecordRaw,
...@@ -315,14 +296,14 @@ export const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -315,14 +296,14 @@ export const constantRoutes: Array<RouteRecordRaw> = [
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHashHistory(),
routes: constantRoutes, routes: constantRoutes,
scrollBehavior: () => ({ top: 0 }) scrollBehavior: () => ({ top: 0 })
}) })
export function resetRouter() { export function resetRouter() {
const newRouter = createRouter({ const newRouter = createRouter({
history: createWebHistory(), history: createWebHashHistory(),
routes: constantRoutes, routes: constantRoutes,
scrollBehavior: () => ({ top: 0 }) scrollBehavior: () => ({ top: 0 })
}) })
......
...@@ -63,3 +63,37 @@ ...@@ -63,3 +63,37 @@
--el-segmented-item-hover-color: #FFFFFF; --el-segmented-item-hover-color: #FFFFFF;
--el-segmented-item-active-bg-color: #1977d5d7; --el-segmented-item-active-bg-color: #1977d5d7;
} }
.el-dialog {
background-image: url("@/assets/picture/dialog1.png");
background-size: 100% 100%;
background-repeat: no-repeat;
.el-form-item__label {
color: #ffffff;
}
.el-dialog__header {
color: #ffffff;
}
.el-dialog__title {
color: #ffffff;
}
.el-textarea__inner {
background-color: #1d5484;
color: #ffffff;
box-shadow: none;
}
}
// .el-date-editor {
// .el-date-editor {
// color: #ffffff;
// }
// }
.el-date-editor .el-range-input{
color: #ffffff;
}
\ No newline at end of file
import Cookies from 'js-cookie' // import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token' const TokenKey = 'Admin-Token'
export function getToken(): string { export function getToken(): string {
return Cookies.get(TokenKey) || '' return localStorage.getItem(TokenKey) || ''
} }
export function setToken(token: string) { export function setToken(token: string) {
return Cookies.set(TokenKey, token) return localStorage.setItem(TokenKey, token)
} }
export function removeToken() { export function removeToken() {
return Cookies.remove(TokenKey) return localStorage.removeItem(TokenKey)
} }
...@@ -7,7 +7,7 @@ const instance = axios.create({ ...@@ -7,7 +7,7 @@ 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/json; charset=utf-8' 'Content-Type': 'application/zip; charset=utf-8'
} }
}) })
...@@ -88,10 +88,11 @@ export interface QueryParams { ...@@ -88,10 +88,11 @@ export interface QueryParams {
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
[key: string]: any status?: any
job?:any
} }
// 请求函数 // 请求函数
...@@ -119,4 +120,12 @@ export interface UserQueryParams { ...@@ -119,4 +120,12 @@ export interface UserQueryParams {
spider?: string spider?: string
cron?: string cron?: string
options?: string options?: string
sat_name?: string
ntc_id?: string
filters?: any
norad_cat_id?: string
object_name?: string
name?:string
spacecraft?:string
station?:string
} }
\ No newline at end of file
...@@ -52,7 +52,6 @@ import { ref, computed, onMounted, watch } from 'vue'; ...@@ -52,7 +52,6 @@ import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import SelectScrapydServer from '@/components/SelectScrapydServer.vue'; import SelectScrapydServer from '@/components/SelectScrapydServer.vue';
import SelectProject from '@/components/SelectProject.vue'; import SelectProject from '@/components/SelectProject.vue';
import AutoRefresh from '@/components/AutoRefresh.vue';
import JobCancelAll from '@/views/job/components/JobCancelAll.vue'; import JobCancelAll from '@/views/job/components/JobCancelAll.vue';
const props = defineProps({ 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>
...@@ -42,6 +42,7 @@ const redirect = ref<string | undefined>(undefined) ...@@ -42,6 +42,7 @@ const redirect = ref<string | undefined>(undefined)
// 监听路由变化 // 监听路由变化
if (route.query?.redirect) { if (route.query?.redirect) {
console.log(route.query.redirect);
redirect.value = route.query.redirect as string redirect.value = route.query.redirect as string
} }
...@@ -60,7 +61,7 @@ const handleLogin = () => { ...@@ -60,7 +61,7 @@ const handleLogin = () => {
loading.value = true loading.value = true
try { try {
await userStore.login(loginForm) await userStore.login(loginForm)
router.push({ path: redirect.value || '/' }) router.push({ path: '/' })
} catch (error: any) { } catch (error: any) {
console.error('登录失败:', error) console.error('登录失败:', error)
} finally { } finally {
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
<div> <div>
<el-table :data="tableData" style="width: 100%" border :row-style="{ height: '45px' }" <el-table :data="tableData" style="width: 100%" border :row-style="{ height: '45px' }"
:header-cell-style="{ textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> :header-cell-style="{ textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
<el-table-column type="selection" width="40" /> <!-- <el-table-column type="selection" width="40" /> -->
<el-table-column property="number" label="序号" width="55" /> <el-table-column property="number" label="序号" width="55" />
<el-table-column property="targetName" label="目标名称" show-overflow-tooltip /> <el-table-column property="targetName" label="目标名称" show-overflow-tooltip />
<el-table-column property="stationPosition" label="站点位置" show-overflow-tooltip /> <el-table-column property="stationPosition" label="站点位置" show-overflow-tooltip />
......
<template> <template>
<div class="text-left p-s"> <!-- <div class="text-left p-s">
<div class="segmentedStyle"> <div class="segmentedStyle">
<el-segmented v-model="mode" :options="sizeOptions" style="margin-bottom: 1rem" size="small" /> <el-segmented v-model="mode" :options="sizeOptions" style="margin-bottom: 1rem" size="small" />
</div> </div>
</div> </div> -->
<dsnDataTab v-if="mode === 'DSN数据'"> <dsnDataTab>
</dsnDataTab> </dsnDataTab>
<newsDataTab v-if="mode === '新闻'"> <!-- <newsDataTab v-if="mode === '新闻'">
</newsDataTab> </newsDataTab> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
......
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
<div> <div>
<div class="backStyle" v-if="route.query.jump === 'yes'" @click="goToStatus" /> <div class="backStyle" v-if="route.query.jump === 'yes'" @click="goToStatus" />
<div class="text-left p-4 "> <div class="text-left p-4 ">
<div class="custom-style"> <div class="custom-style flex gap-4">
<el-segmented v-model="mode" :options="sizeOptions" style="margin-bottom: 1rem" size="default" /> <el-segmented v-model="mode" :options="sizeOptions" style="margin-bottom: 1rem" size="default" />
<el-button type="primary" @click="handleExport">导出</el-button>
</div> </div>
</div> </div>
<!-- 综合数据页面组件 --> <!-- 综合数据页面组件 -->
<allDataTab v-if="mode === '综合数据'"> <!-- <allDataTab v-if="mode === '综合数据'">
</allDataTab> </allDataTab> -->
<!-- DSN数据页面组件 --> <!-- DSN数据页面组件 -->
<dsnDataTab v-if="mode === 'DSN数据'"> <dsnDataTab v-if="mode === 'DSN数据'">
</dsnDataTab> </dsnDataTab>
...@@ -18,7 +19,12 @@ ...@@ -18,7 +19,12 @@
<!-- ST数据页面组件 --> <!-- ST数据页面组件 -->
<stDataTab v-if="mode === 'ST数据'"> <stDataTab v-if="mode === 'ST数据'">
</stDataTab> </stDataTab>
<!-- ESA数据页面组件 -->
<esDataTab v-if="mode === 'ESA数据'">
</esDataTab>
</div> </div>
<exportDialog v-model:dialogVisible="showDeleteDialog" @confirm="handleExportConfirm" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -29,24 +35,46 @@ import allDataTab from './components/allDataTab.vue' ...@@ -29,24 +35,46 @@ import allDataTab from './components/allDataTab.vue'
import ituDataTab from './components/ituDataTab.vue' import ituDataTab from './components/ituDataTab.vue'
import stDataTab from './components/stDataTab.vue' import stDataTab from './components/stDataTab.vue'
import dsnDataTab from './components/dsnData/dsnTab.vue' import dsnDataTab from './components/dsnData/dsnTab.vue'
import esDataTab from './components/esDataTab.vue'
import exportDialog from '@/components/Export/index.vue'
import { genFileId } from 'element-plus'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
const mode = ref('综合数据') const mode = ref('DSN数据')
const sizeOptions = ['综合数据', 'DSN数据', 'ITU数据', 'ST数据'] const showDeleteDialog = ref(false)
const sizeOptions = ['DSN数据', 'ITU数据', 'ST数据', 'ESA数据']
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const modeValue = ref<any>('数据展示') const modeValue = ref<any>('数据展示')
const upload = ref<UploadInstance>()
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
upload.value!.handleStart(file)
}
const submitUpload = () => {
upload.value!.submit()
}
const goToStatus = () => { const goToStatus = () => {
router.push({ router.push({
path: '/osStatus/list', path: '/osStatus/list',
}) })
} }
const handleExport = () => {
showDeleteDialog.value = true
}
const handleExportConfirm = () => {
}
onMounted(() => { onMounted(() => {
console.log('数据展示');
console.log(route.query);
if (route.query.mode) { if (route.query.mode) {
modeValue.value = route.query.mode modeValue.value = route.query.mode
mode.value = modeValue.value mode.value = modeValue.value
} }
console.log(mode.value)
}) })
</script> </script>
......
...@@ -36,14 +36,17 @@ const pageObj = ref({ ...@@ -36,14 +36,17 @@ const pageObj = ref({
pageNo: 1 pageNo: 1
}) })
const handleDetails = (row: any) => { const handleDetails = (row: any) => {
console.log(row); // console.log(row);
router.push({ router.push({
path: '/osTaskInformation/list', path: '/osTaskInformation/list',
query: { query: {
jump: 'yes' jump: 'yes',
spiderType: row.spider,
page:'spiderManager'
} }
}) })
} }
// 获取爬虫数据的方法
const getData = async () => { const getData = async () => {
const res = await getSpiderList({scrapydServerId:'1',project:'spiders'}) const res = await getSpiderList({scrapydServerId:'1',project:'spiders'})
tableData.value = res.data tableData.value = res.data
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
<span>数据统计</span> <span>数据统计</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>总数据量: {{ totalDataNumber }} 1230</span> <span>总数据量: {{ totalDataNumber }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>采集的页面数量: {{ totalPageNumber }} 85</span> <span>采集的页面数量: {{ totalPageNumber }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>采集的目标数量: {{ totalTargetNumber }} 569</span> <span>采集的目标数量: {{ totalTargetNumber }}</span>
</div> </div>
</div> </div>
<div class="dataCard" @click="goToTaskRecordPage"> <div class="dataCard" @click="goToTaskRecordPage">
...@@ -20,13 +20,13 @@ ...@@ -20,13 +20,13 @@
<span>任务执行统计</span> <span>任务执行统计</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行成功统计: {{ successTask }} 36</span> <span>任务执行成功统计: {{ successTask }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行失败统计: {{ failTask }} 0</span> <span>任务执行失败统计: {{ failTask }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务异常数统计: {{ unusualTask }} 1</span> <span>任务异常数统计: {{ unusualTask }}</span>
</div> </div>
</div> </div>
<div class="dataCard"> <div class="dataCard">
...@@ -34,13 +34,13 @@ ...@@ -34,13 +34,13 @@
<span>性能统计</span> <span>性能统计</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>平均成功率: {{ speed }} 98%</span> <span>平均成功率: {{ speed }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>平均错误率: {{ errorRate }} 0%</span> <span>平均错误率: {{ errorRate }}</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>平均异常率: {{ errorRate }} 2%</span> <span>平均异常率: {{ unusualRate }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -49,48 +49,26 @@ ...@@ -49,48 +49,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getStatsDataList, getSpiderTaskList, getPerformanceList } from '@/api/staticData';
import { onMounted, ref } from 'vue';
const router = useRouter()
const totalDataNumber = ref('')
const totalPageNumber = ref('')
const totalTargetNumber = ref('')
const successTask = ref('')
const failTask = ref('')
const unusualTask = ref('')
const speed = ref('')
const errorRate = ref('')
const unusualRate = ref('')
const router = useRouter();
defineProps({
totalDataNumber: {
type: String,
default: ''
},
totalPageNumber: {
type: String,
default: ''
},
totalTargetNumber: {
type: String,
default: ''
},
successTask: {
type: String,
default: ''
},
failTask: {
type: String,
default: ''
},
unusualTask: {
type: String,
default: ''
},
speed: {
type: String,
default: ''
},
errorRate: {
type: String,
default: ''
}
})
const goToAllDataPage = () => { const goToAllDataPage = () => {
router.push({ router.push({
path: '/osDataDisplay/list', path: '/osDataDisplay/list',
query: { query: {
jump: 'yes', jump: 'yes',
mode: '综合数据' mode: 'DSN数据'
} }
}) })
} }
...@@ -105,7 +83,25 @@ const goToTaskRecordPage = () => { ...@@ -105,7 +83,25 @@ const goToTaskRecordPage = () => {
}) })
} }
// 获取数据统计数据的方法
const getData = async () => {
const staticData = await getStatsDataList({})
const spiderTask = await getSpiderTaskList({})
const performance = await getPerformanceList({})
totalDataNumber.value = staticData.data.totalData
totalPageNumber.value = staticData.data.ituPage
totalTargetNumber.value = staticData.data.spaceTrackItemCount
successTask.value = spiderTask.data.successCount
failTask.value = spiderTask.data.failCount
unusualTask.value = spiderTask.data.exceptionCount
speed.value = performance.data.success
errorRate.value = performance.data.error
unusualRate.value = performance.data.exception
}
onMounted(() => {
getData();
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
<template> <template>
<div class="flex gap-10"> <div class="flex gap-10">
<span class="textStyle">QB数据管理</span> <span class="textStyle">QB数据管理</span>
<div class="dataCard" @click="goToAllDataPage"> <!-- <div class="dataCard" @click="goToAllDataPage">
<div class="titleStyle"> <div class="titleStyle">
<span>综合数据</span> <span>综合数据</span>
</div> </div>
<div class="iconStyle" /> <div class="iconStyle" />
</div> </div> -->
<div class="dataCard" @click="goToDSNDataPage"> <div class="dataCard" @click="goToDSNDataPage">
<div class="titleStyle"> <div class="titleStyle">
<span>DSN数据</span> <span>DSN数据</span>
...@@ -110,10 +110,10 @@ const goToSTDataPage = () => { ...@@ -110,10 +110,10 @@ const goToSTDataPage = () => {
background-image: url("@/assets/picture/wenjianjia.png"); background-image: url("@/assets/picture/wenjianjia.png");
background-size: 100% 120%; background-size: 100% 120%;
background-repeat: no-repeat; background-repeat: no-repeat;
margin-top: 3%; margin-top: -5%;
display: flex; display: flex;
// border: 1.5px solid rgb(193, 188, 188); // border: 1.5px solid rgb(193, 188, 188);
height: 50%; height: 65%;
width: 50%; width: 50%;
} }
...@@ -123,7 +123,7 @@ const goToSTDataPage = () => { ...@@ -123,7 +123,7 @@ const goToSTDataPage = () => {
background-repeat: no-repeat; background-repeat: no-repeat;
// background: #c6ebfc; // background: #c6ebfc;
// border: 1.5px solid rgb(193, 188, 188); // border: 1.5px solid rgb(193, 188, 188);
width: 260px; width: 360px;
height: 100%; height: 100%;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
......
...@@ -6,16 +6,16 @@ ...@@ -6,16 +6,16 @@
<span>DSN爬取任务</span> <span>DSN爬取任务</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务采集目标数: {{ totalDataNumber }} 638</span> <span>DSN爬虫任务数: {{ dsnTotalTaskNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行成功统计: {{ totalPageNumber }} 79</span> <span>任务执行成功统计: {{ dsnTaskSuccessNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>采集速度: {{ totalTargetNumber }} 324</span> <span>任务执行失败统计: {{ dsnTaskFailNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>错误率: {{ totalTargetNumber }} 1%</span> <span>错误率: {{ dsnErrorRate }} </span>
</div> </div>
</div> </div>
<div class="dataCard" @click="goToITUTaskRecordPage"> <div class="dataCard" @click="goToITUTaskRecordPage">
...@@ -23,16 +23,16 @@ ...@@ -23,16 +23,16 @@
<span>ITU爬取任务</span> <span>ITU爬取任务</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务采集目标数: {{ totalDataNumber }} 148</span> <span>ITU爬虫任务数: {{ ituTotalTaskNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行成功统计: {{ totalPageNumber }} 98</span> <span>任务执行成功统计: {{ ituTaskSuccessNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>采集速度: {{ totalTargetNumber }} 322</span> <span>任务执行失败统计: {{ ituTaskFailNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>错误率: {{ totalTargetNumber }} 1%</span> <span>错误率: {{ ituErrorRate }} </span>
</div> </div>
</div> </div>
<div class="dataCard" @click="goToSTTaskRecordPage"> <div class="dataCard" @click="goToSTTaskRecordPage">
...@@ -40,16 +40,16 @@ ...@@ -40,16 +40,16 @@
<span>ST爬取任务</span> <span>ST爬取任务</span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务采集目标数: {{ totalDataNumber }} 136</span> <span>ST爬虫任务数: {{ stTotalTaskNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行成功统计: {{ totalPageNumber }} 86</span> <span>任务执行成功统计: {{ stTaskSuccessNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>采集速度: {{ totalTargetNumber }} 342</span> <span>采集速度: {{ stTaskFailNumber }} </span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>错误率: {{ totalTargetNumber }} 0%</span> <span>错误率: {{ stErrorRate }} </span>
</div> </div>
</div> </div>
</div> </div>
...@@ -57,71 +57,80 @@ ...@@ -57,71 +57,80 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getAllSpiderTaskStatistics } from '@/api/staticData';
import { onMounted,ref } from 'vue';
import { getSpiderTaskList } from '@/api/spiderTask'
const router = useRouter() const router = useRouter()
defineProps({
totalDataNumber: { const dsnTotalTaskNumber = ref('')
type: String, const dsnTaskSuccessNumber = ref('')
default: '' const dsnTaskFailNumber = ref('')
}, const dsnErrorRate = ref('')
totalPageNumber: { const ituTotalTaskNumber = ref('')
type: String, const ituTaskSuccessNumber = ref('')
default: '' const ituTaskFailNumber = ref('')
}, const ituErrorRate = ref('')
totalTargetNumber: { const stTotalTaskNumber = ref('')
type: String, const stTaskSuccessNumber = ref('')
default: '' const stTaskFailNumber = ref('')
}, const stErrorRate = ref('')
successTask: {
type: String,
default: ''
},
failTask: {
type: String,
default: ''
},
unusualTask: {
type: String,
default: ''
},
speed: {
type: String,
default: ''
},
errorRate: {
type: String,
default: ''
}
})
const goToDSNTaskRecordPage = () => { const goToDSNTaskRecordPage = () => {
router.push({ router.push({
path: '/osTaskRecord/list', path: '/osTaskInformation/list',
query: { query: {
mode: 'dsn', spiderType: 'dsn_now',
jump: 'yes' jump: 'yes',
page: 'statusMonitor'
} }
}) })
} }
const goToITUTaskRecordPage = () => { const goToITUTaskRecordPage = () => {
router.push({ router.push({
path: '/osTaskRecord/list', path: '/osTaskInformation/list',
query: { query: {
mode: 'itu', spiderType: 'itu_space_explorer',
jump: 'yes' jump: 'yes',
page: 'statusMonitor'
} }
}) })
} }
const goToSTTaskRecordPage = () => { const goToSTTaskRecordPage = () => {
router.push({ router.push({
path: '/osTaskRecord/list', path: '/osTaskInformation/list',
query: { query: {
mode: 'st', spiderType: 'api_spider',
jump: 'yes' jump: 'yes',
page: 'statusMonitor'
} }
}) })
} }
// 获取任务统计数据的方法
const getData = async () => {
const res = await getAllSpiderTaskStatistics({})
const dsnTask = await getSpiderTaskList({ spiders: 'dsn_now' })
const ituTask = await getSpiderTaskList({ spiders: 'itu_space_explorer' })
const stTask = await getSpiderTaskList({ spiders: 'api_spider' })
dsnTotalTaskNumber.value = dsnTask.data.length
dsnTaskSuccessNumber.value = res.data.dsn_now.successCount
dsnTaskFailNumber.value = res.data.dsn_now.failCount
dsnErrorRate.value = res.data.dsn_now.errorRate
ituTotalTaskNumber.value = ituTask.data.length
ituTaskSuccessNumber.value = res.data.itu_space_explorer.successCount
ituTaskFailNumber.value = res.data.itu_space_explorer.failCount
ituErrorRate.value = res.data.itu_space_explorer.errorRate
stTotalTaskNumber.value = stTask.data.length
stTaskSuccessNumber.value = res.data.space_track.successCount
stTaskFailNumber.value = res.data.space_track.failCount
stErrorRate.value = res.data.space_track.errorRate
}
onMounted(() => {
getData()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
<template> <template>
<el-dialog v-model="currentVisible" :title="currentMode === AddMode.ADD_MODE ? '新增用户' : '编辑用户'" width="610" center align-center <el-dialog v-model="currentVisible" :title="currentMode === AddMode.ADD_MODE ? '新增用户' : '编辑用户'" width="480" center align-center
@close="handleClose" draggable> @close="handleClose" draggable>
<div v-if="currentMode === AddMode.ADD_MODE"> <div v-if="currentMode === AddMode.ADD_MODE">
<el-form :rules="firstFormRules" ref="firstRuleFormRef" :model="firstForm"> <el-form :rules="firstFormRules" ref="firstRuleFormRef" :model="firstForm">
<el-form-item label="用户账号:" prop="userAccount"> <el-form-item label="用户账号:" prop="userAccount" label-width="100px">
<el-input v-model="firstForm.userAccount" /> <el-input v-model="firstForm.userAccount" style="width: 300px;"/>
</el-form-item> </el-form-item>
<el-form-item label="用户名称:" prop="userName"> <el-form-item label="用户名称:" prop="userName" label-width="100px">
<el-input v-model="firstForm.userName" /> <el-input v-model="firstForm.userName" style="width: 300px"/>
</el-form-item> </el-form-item>
<el-form-item label="用户状态:"> <el-form-item label="用户状态:" label-width="100px">
<el-select v-model="userStatusValue" placeholder="请选择状态" style="width: 100%;"> <el-select v-model="userStatusValue" placeholder="请选择状态" style="width: 300px;">
<el-option v-for="item in userStatusOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in userStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户密码:" prop="userPassword"> <el-form-item label="用户密码:" prop="userPassword" label-width="100px">
<el-input v-model="firstForm.userPassword" /> <el-input v-model="firstForm.userPassword" style="width: 300px;"/>
</el-form-item> </el-form-item>
<el-form-item label="用户角色:"> <el-form-item label="用户角色:" label-width="100px">
<el-select v-model="userRoleValue" placeholder="请选择角色" style="width: 100%;"> <el-select v-model="userRoleValue" placeholder="请选择角色" style="width: 300px;">
<el-option v-for="item in userRoleOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in userRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -26,24 +26,24 @@ ...@@ -26,24 +26,24 @@
</div> </div>
<div v-if="currentMode === AddMode.UPDATE_MODE"> <div v-if="currentMode === AddMode.UPDATE_MODE">
<el-form :inline="true" :rules="secondFormRules" ref="secondRuleFormRef" :model="secondForm"> <el-form :inline="true" :rules="secondFormRules" ref="secondRuleFormRef" :model="secondForm">
<el-form-item label="用户名称:" prop="userName"> <el-form-item label="用户名称:" prop="userName" label-width="100px">
<el-input v-model="secondForm.userName" /> <el-input v-model="secondForm.userName" style="width: 300px;"/>
</el-form-item> </el-form-item>
<el-form-item label="用户角色:"> <el-form-item label="用户角色:" label-width="100px">
<el-select v-model="userRoleValue" placeholder="请选择角色" style="width: 180px;"> <el-select v-model="userRoleValue" placeholder="请选择角色" style="width: 300px;">
<el-option v-for="item in userRoleOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in userRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="m-t-4" /> <div class="m-t-4" />
<el-form :inline="true" :rules="editPasswordRules" ref="editPasswordFormRef" :model="editPasswordForm"> <el-form :inline="true" :rules="editPasswordRules" ref="editPasswordFormRef" :model="editPasswordForm">
<el-form-item label="用户状态:"> <el-form-item label="用户状态:" label-width="100px">
<el-select v-model="userStatusValue" placeholder="请选择状态" style="width: 180px;"> <el-select v-model="userStatusValue" placeholder="请选择状态" style="width: 300px;">
<el-option v-for="item in userStatusOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in userStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户密码:" prop="userPassword"> <el-form-item label="用户密码:" prop="userPassword" label-width="100px">
<el-input v-model="editPasswordForm.userPassword" placeholder="不修改请留空" /> <el-input v-model="editPasswordForm.userPassword" placeholder=" " style="width: 300px;"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
...@@ -100,12 +100,12 @@ const editPasswordFormRef = ref<FormInstance>() ...@@ -100,12 +100,12 @@ const editPasswordFormRef = ref<FormInstance>()
const firstFormRules = ref<FormRules>({ const firstFormRules = ref<FormRules>({
userAccount: [ userAccount: [
{ required: true, message: '请输入用户账号', trigger: 'blur' }, { required: true, message: '请输入用户账号', trigger: 'blur' },
{ type: 'string', min: 1, max: 20, message: '用户账号长度1-20字符', trigger: 'blur' }, { type: 'string', min: 3, max: 20, message: '用户账号长度3-20字符', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '只能包含英文字母或数字', trigger: 'blur' } { pattern: /^[A-Za-z0-9]+$/, message: '用户账号只能包含英文字母或数字', trigger: 'blur' }
], ],
userName: [ userName: [
{ required: true, message: '请输入用户名称', trigger: 'blur' }, { required: true, message: '请输入用户名称', trigger: 'blur' },
{ type: 'string', min: 1, max: 20, message: '用户名称长度1-20字符', trigger: 'blur' }, { type: 'string', min: 2, max: 20, message: '用户名称长度2-20字符', trigger: 'blur' },
], ],
userPassword: [ userPassword: [
{ required: true, message: '请输入用户密码', trigger: 'blur' }, { required: true, message: '请输入用户密码', trigger: 'blur' },
...@@ -126,38 +126,38 @@ const editPasswordRules = ref<FormRules>({ ...@@ -126,38 +126,38 @@ const editPasswordRules = ref<FormRules>({
], ],
}) })
// 状态选项(统一值类型,避免类型错误)
const userStatusValue = ref(true) const userStatusValue = ref(true)
const userRoleValue = ref(1) const userRoleValue = ref(1)
const userStatusOptions = [ const userStatusOptions = [
{ value: true, label: '启用' }, { value: true, label: '启用' },
{ value: false, label: '停用' } // 与userStatusValue类型一致(boolean) { value: false, label: '停用' }
] ]
const userRoleOptions = [ const userRoleOptions = [
{ value: false, label: '管理员' }, { value: 1, label: '管理员' },
{ value: true, label: '普通用户' } // 统一值类型 { value: 0, label: '普通用户' }
] ]
const emit = defineEmits(['update:dialogVisible', 'getUserList']) const emit = defineEmits(['update:dialogVisible', 'getUserList'])
// 组件状态 // 组件状态
const currentVisible = ref(props.dialogVisible) const currentVisible = ref(props.dialogVisible)
const currentMode = ref(props.mode) const currentMode = ref(props.mode)
// 关闭对话框 // 关闭对话框的方法
const handleClose = () => { const handleClose = () => {
firstRuleFormRef.value?.resetFields()
secondRuleFormRef.value?.resetFields()
editPasswordFormRef.value?.resetFields()
currentVisible.value = false currentVisible.value = false
} }
// 确认操作 // 创建用户与编辑用户的方法
const handleConfirm = async () => { const handleConfirm = async () => {
try { try {
if (currentMode.value === AddMode.ADD_MODE) { if (currentMode.value === AddMode.ADD_MODE) {
if (!firstRuleFormRef.value) { if (!firstRuleFormRef.value) {
return; return;
} }
// 使用Promise形式的validate(不传入回调),确保await生效
const valid = await firstRuleFormRef.value.validate(); const valid = await firstRuleFormRef.value.validate();
if (valid) { if (valid) {
await addUser({ await addUser({
...@@ -172,7 +172,6 @@ const handleConfirm = async () => { ...@@ -172,7 +172,6 @@ const handleConfirm = async () => {
currentVisible.value = false; currentVisible.value = false;
} }
} else if (currentMode.value === AddMode.UPDATE_MODE) { } else if (currentMode.value === AddMode.UPDATE_MODE) {
// 编辑模式逻辑(略,可参考新增模式调整)
if (!secondRuleFormRef.value) return; if (!secondRuleFormRef.value) return;
const valid = await secondRuleFormRef.value.validate(); const valid = await secondRuleFormRef.value.validate();
if (valid) { if (valid) {
...@@ -211,6 +210,7 @@ watch(() => props.mode, (newVal) => { ...@@ -211,6 +210,7 @@ watch(() => props.mode, (newVal) => {
currentMode.value = newVal currentMode.value = newVal
}) })
// 监听参数变化,将参数展示到对话框中
watch( watch(
[() => props.dialogVisible, () => props.mode, () => props.nickName, () => props.userPassword, () => props.userStatus, () => props.userRole], [() => props.dialogVisible, () => props.mode, () => props.nickName, () => props.userPassword, () => props.userStatus, () => props.userRole],
([newVisible, newMode, newNickName, newPwd, newStatus, newRole]) => { ([newVisible, newMode, newNickName, newPwd, newStatus, newRole]) => {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="text-left p-8"> <div class="text-left p-8">
<el-form inline> <el-form inline>
<el-form-item> <el-form-item>
<el-button type="primary" plain @click="openAddUserDialog">创建用户</el-button> <el-button test-element="userSystem-AddUser" type="primary" plain @click="openAddUserDialog">创建用户</el-button>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button> <el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<el-table-column property="create_time" label="创建时间" width="280" show-overflow-tooltip /> <el-table-column property="create_time" label="创建时间" width="280" show-overflow-tooltip />
<el-table-column label="操作" width="220"> <el-table-column label="操作" width="220">
<template #default="scope"> <template #default="scope">
<el-button type="primary" plain @click="handleEdit(scope.row)"> <el-button type="primary" plain @click="handleEdit(scope.row)" id="editUser">
编辑 编辑
</el-button> </el-button>
<el-button type="danger" plain @click="handleDelete(scope.row)"> <el-button type="danger" plain @click="handleDelete(scope.row)">
...@@ -48,10 +48,10 @@ import { ref, onMounted } from 'vue' ...@@ -48,10 +48,10 @@ import { ref, onMounted } from 'vue'
import Pagination from '@/components/pagination/index.vue' import Pagination from '@/components/pagination/index.vue'
import deleteDialog from '@/components/Delete/index.vue' import deleteDialog from '@/components/Delete/index.vue'
import addUserDialog from './components/addUserDialog.vue' import addUserDialog from './components/addUserDialog.vue'
import { getUserList, addUser, deleteUser, updateUser, batchDeleteUser } from '@/api/user.ts' import { getUserList } from '@/api/user.ts'
import { DeleteMode } from '@/components/Delete/enum.ts' import { DeleteMode } from '@/components/Delete/enum.ts'
import { AddMode } from './components/enum.ts'; import { AddMode } from './components/enum.ts';
import { ElMessage } from 'element-plus'
const userId = ref<any>([]) const userId = ref<any>([])
const userIds = ref<any>() const userIds = ref<any>()
const nickName = ref<string>('') const nickName = ref<string>('')
...@@ -65,10 +65,11 @@ const dialogVisible = ref<boolean>(false) ...@@ -65,10 +65,11 @@ const dialogVisible = ref<boolean>(false)
const mode = ref<AddMode>(AddMode.ADD_MODE) const mode = ref<AddMode>(AddMode.ADD_MODE)
const tableData = ref() const tableData = ref()
const pageObj = ref({ const pageObj = ref({
total: 12, total: 0,
pageSize: 10, pageSize: 10,
pageNo: 1 pageNo: 1
}) })
// 编辑用户信息的方法
const handleEdit = async (row: any) => { const handleEdit = async (row: any) => {
mode.value = AddMode.UPDATE_MODE mode.value = AddMode.UPDATE_MODE
nickName.value = row.nickname nickName.value = row.nickname
...@@ -78,41 +79,49 @@ const handleEdit = async (row: any) => { ...@@ -78,41 +79,49 @@ const handleEdit = async (row: any) => {
userPassword.value = row.password userPassword.value = row.password
dialogVisible.value = true dialogVisible.value = true
userId.value = row.id userId.value = row.id
// console.log(row);
} }
// 弹窗关闭的方法
const handleDelete = async (row: any) => { const handleDelete = async (row: any) => {
showDeleteDialog.value = true showDeleteDialog.value = true
deleteMode.value = DeleteMode.SINGLE_DELETE deleteMode.value = DeleteMode.SINGLE_DELETE
userId.value = row.id userId.value = row.id
console.log(userId.value); console.log(userId.value);
} }
// 批量删除用户的方法
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if(userIds.value == undefined){
ElMessage({
message: '请先选择要删除的用户',
type: 'warning'
})
return
}else{
deleteMode.value = DeleteMode.BATCH_DELETE deleteMode.value = DeleteMode.BATCH_DELETE
showDeleteDialog.value = true showDeleteDialog.value = true
}
} }
// 多选框改变后的方法
const handleSelectionChange = (data: any) => { const handleSelectionChange = (data: any) => {
let array = [] let array = []
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
array.push(data[i].id) array.push(data[i].id)
} }
userIds.value = array userIds.value = array
console.log(userIds.value);
} }
const openAddUserDialog = () => { const openAddUserDialog = () => {
mode.value = AddMode.ADD_MODE mode.value = AddMode.ADD_MODE
dialogVisible.value = true dialogVisible.value = true
} }
// 获取用户列表数据的方法
const getUserListData = async () => { const getUserListData = async () => {
const userList = await getUserList({ const userList = await getUserList({
// page: 1, page: pageObj.value.pageNo,
size: 10 size: pageObj.value.pageSize
}) })
tableData.value = userList.data tableData.value = userList.data.list
pageObj.value.total = tableData.value.length pageObj.value.total = userList.data.total
console.log(tableData.value.length); console.log(userList);
} }
onMounted(async () => { onMounted(async () => {
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
align-center @close="handleClose" draggable> align-center @close="handleClose" draggable>
<el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-width="100px"> <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-width="100px">
<el-form-item label="任务名称:" v-if="currentMode === AddMode.ADD_TASK" prop="taskName"> <el-form-item label="任务名称:" v-if="currentMode === AddMode.ADD_TASK" prop="taskName">
<el-input v-model="ruleForm.taskName" placeholder="请输入任务名称"style="width: 90%" /> <el-input v-model="ruleForm.taskName" placeholder="请输入任务名称" style="width: 90%" />
</el-form-item> </el-form-item>
<el-form-item label="所属爬虫:" v-if="currentMode === AddMode.ADD_TASK" prop="spiderTypeValue"> <el-form-item label="所属爬虫:" v-if="currentMode === AddMode.ADD_TASK" prop="spiderTypeValue">
<el-select v-model="ruleForm.spiderTypeValue" placeholder="请选择所属爬虫" style="width: 90%"> <el-select v-model="ruleForm.spiderTypeValue" placeholder="请选择所属爬虫" style="width: 90%" >
<el-option v-for="item in spiderTypeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in spiderTypeOptions" :key="item.spider" :label="item.spider"
:value="item.spider" :id="item.spider" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="执行频率:" prop="cronExpression"> <el-form-item label="执行频率:" prop="cronExpression">
...@@ -33,6 +34,8 @@ import { addSpiderTask } from '@/api/spiderTask' ...@@ -33,6 +34,8 @@ import { addSpiderTask } from '@/api/spiderTask'
import { AddMode } from './enum' import { AddMode } from './enum'
import { Crontab } from '@/components/Crontab/index' 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 { getSpiderList } from '@/api/system.ts'
interface RuleForm { interface RuleForm {
taskName: string taskName: string
...@@ -57,7 +60,7 @@ const props = defineProps<addDialogPropType>() ...@@ -57,7 +60,7 @@ 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: 1, max: 50, message: '任务名称应在1-50个字符之间', trigger: 'blur' }, { min: 2, max: 50, message: '任务名称应在2-20个字符之间', trigger: 'blur' },
], ],
spiderTypeValue: [ spiderTypeValue: [
{ {
...@@ -72,7 +75,20 @@ const rules = ref<FormRules<RuleForm>>({ ...@@ -72,7 +75,20 @@ const rules = ref<FormRules<RuleForm>>({
message: '请输入执行频率', message: '请输入执行频率',
trigger: 'change', trigger: 'change',
}, },
{ min: 1, message: '执行频率应为cron表达式', trigger: 'blur' }, {
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值
...@@ -80,22 +96,7 @@ const emit = defineEmits(['update:dialogVisible', 'confirm', 'update:mode', 'get ...@@ -80,22 +96,7 @@ const emit = defineEmits(['update:dialogVisible', 'confirm', 'update:mode', 'get
// 组件的状态 // 组件的状态
const currentVisible = ref(props.dialogVisible) const currentVisible = ref(props.dialogVisible)
const currentMode = ref(props.mode) const currentMode = ref(props.mode)
const taskName = ref('') const spiderTypeOptions = ref()
const spiderTypeValue = ref('')
const spiderTypeOptions = ref([
{
value: 'api_spider',
label: 'api_spider'
},
{
value: 'dsn_now',
label: 'dsn_now'
},
{
value: 'itu_space_explorer',
label: 'itu_space_explorer'
},
])
const taskParams = ref({ const taskParams = ref({
scrapyd_server_id: '1', scrapyd_server_id: '1',
schedule_type: '0', schedule_type: '0',
...@@ -108,10 +109,13 @@ const handleClose = () => { ...@@ -108,10 +109,13 @@ const handleClose = () => {
ruleForm.value.taskName = '' ruleForm.value.taskName = ''
ruleForm.value.spiderTypeValue ruleForm.value.spiderTypeValue
ruleForm.value.cronExpression = '' ruleForm.value.cronExpression = ''
ruleFormRef.value?.resetFields()
currentVisible.value = false currentVisible.value = false
} }
// 确定的方法 // 确定的方法
const handleConfirm = async () => { const handleConfirm = async () => {
console.log(ruleForm.value.cronExpression);
if (!ruleFormRef.value) return if (!ruleFormRef.value) return
await ruleFormRef.value.validate(async (valid, fields) => { await ruleFormRef.value.validate(async (valid, fields) => {
console.log('开始校验'); console.log('开始校验');
...@@ -128,6 +132,7 @@ const handleConfirm = async () => { ...@@ -128,6 +132,7 @@ const handleConfirm = async () => {
}) })
currentVisible.value = false currentVisible.value = false
emit('getTaskList') emit('getTaskList')
ElMessage.success('添加成功')
} else if (currentMode.value === AddMode.UPDATE_TASK) { } else if (currentMode.value === AddMode.UPDATE_TASK) {
await addSpiderTask({ await addSpiderTask({
scrapyd_server_id: taskParams.value.scrapyd_server_id, scrapyd_server_id: taskParams.value.scrapyd_server_id,
...@@ -140,13 +145,19 @@ const handleConfirm = async () => { ...@@ -140,13 +145,19 @@ const handleConfirm = async () => {
}) })
currentVisible.value = false currentVisible.value = false
emit('getTaskList') emit('getTaskList')
ElMessage.success('修改成功')
} }
} else { } else {
console.log('校验不通过'); console.log('校验不通过');
} }
}) })
} }
const getData = async () => {
const res = await getSpiderList({scrapydServerId:'1',project:'spiders'})
spiderTypeOptions.value = res.data
console.log(res);
}
// 监听props变化,同步给组件内部 // 监听props变化,同步给组件内部
watch(() => props.dialogVisible, watch(() => props.dialogVisible,
(newVal) => { (newVal) => {
...@@ -180,13 +191,13 @@ watch( ...@@ -180,13 +191,13 @@ watch(
} }
if (!newVisible) { if (!newVisible) {
// 清空表单 // 清空表单
cronExpression.value = '' // cronExpression.value = ''
} }
}, },
{ immediate: true } { immediate: true }
); );
onMounted(() => { onMounted(() => {
getData()
}) })
</script> </script>
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { defineProps } from 'vue'; import { defineProps } from 'vue';
import { deleteSpiderTask } from '@/api/spiderTask.ts' import { deleteSpiderTask } from '@/api/spiderTask.ts'
import { ElMessage } from 'element-plus';
interface deleteDialogPropType { interface deleteDialogPropType {
dialogVisible: boolean, dialogVisible: boolean,
jobId: string jobId: string
...@@ -30,6 +31,7 @@ const handleDelelte = async () => { ...@@ -30,6 +31,7 @@ const handleDelelte = async () => {
await deleteSpiderTask({job_id: props.jobId}) await deleteSpiderTask({job_id: props.jobId})
emit('getUserList') emit('getUserList')
deleteDialogVisible.value = false deleteDialogVisible.value = false
ElMessage.success('删除成功')
} }
// 关闭弹窗的方法 // 关闭弹窗的方法
......
<template> <template>
<div> <div>
<div class="backStyle" v-if="route.query.jump === 'yes'" @click="goToTaskInformation" /> <div class="backStyle" v-if="route.query.jump === 'yes'" @click="goToTaskInformation" />
<div class="m-t-10" /> <div class="m-t-8" />
<div > <div>
<taskCard successTask="100" failTask="10" unusualTask="1" /> <taskCard :spiderType="spiderType" failTask="10" unusualTask="1" />
<!-- <div class="pagination w-full flex flex-row-reverse pr-18 m-t-0"> <!-- <div class="pagination w-full flex flex-row-reverse pr-18 m-t-0">
<Pagination :total="pageObj.total" v-model:page="pageObj.pageNo" v-model:limit="pageObj.pageSize" <Pagination :total="pageObj.total" v-model:page="pageObj.pageNo" v-model:limit="pageObj.pageSize"
@pagination="getData" /> @pagination="getData" />
...@@ -23,6 +23,7 @@ import { AddMode } from './components/enum' ...@@ -23,6 +23,7 @@ import { AddMode } from './components/enum'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const spiderType = ref<any>(route.query.spiderType)
const taskValue = ref('') const taskValue = ref('')
const taskList = ref([]) const taskList = ref([])
const taskOptions = [ const taskOptions = [
...@@ -46,8 +47,18 @@ const pageObj = ref({ ...@@ -46,8 +47,18 @@ const pageObj = ref({
}) })
const goToTaskInformation = () => { const goToTaskInformation = () => {
if(route.query.page === 'spiderManager'){
router.push({ path: '/osSpiderManager/list' }) router.push({ path: '/osSpiderManager/list' })
}else{
router.push({ path: '/osStatus/list' })
}
} }
onMounted(async () => {
// if(route.query.spiderType){
// spiderType.value = route.query.spiderType
// }
})
</script> </script>
<style scoped> <style scoped>
......
...@@ -9,21 +9,13 @@ ...@@ -9,21 +9,13 @@
<el-text class="mx-1">时间:</el-text> <el-text class="mx-1">时间:</el-text>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-date-picker v-model="timeValue" type="datetimerange" start-placeholder="Start date" <el-config-provider :locale="zhCn">
end-placeholder="End date" format="YYYY-MM-DD HH:mm:ss" date-format="YYYY/MM/DD ddd" <el-date-picker v-model="timeValue" type="datetimerange" start-placeholder="开始时间"
time-format="A hh:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"/> end-placeholder="结束时间" format="YYYY-MM-DD HH:mm:ss" date-format="YYYY/MM/DD ddd"
</el-form-item> time-format="A hh:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
<!-- <el-form-item> </el-config-provider>
<el-text class="mx-1">调度状态:</el-text>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<div>
<el-select v-model="taskValue" placeholder="请选择" style="width: 220px">
<el-option v-for="item in taskOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</el-form-item> -->
<el-form-item>
<el-space> <el-space>
<el-button type="primary" @click="searchData">查询</el-button> <el-button type="primary" @click="searchData">查询</el-button>
<el-button type="primary" @click="getData">重置表格</el-button> <el-button type="primary" @click="getData">重置表格</el-button>
...@@ -38,7 +30,6 @@ ...@@ -38,7 +30,6 @@
<el-table :data="tableData" style="width: 100%" border :header-cell-style="{ textAlign: 'center' }" <el-table :data="tableData" style="width: 100%" border :header-cell-style="{ textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }"> :cell-style="{ textAlign: 'center' }">
<el-table-column property="number" label="序号" width="55" type="index" /> <el-table-column property="number" label="序号" width="55" type="index" />
<el-table-column property="project" label="项目名称" show-overflow-tooltip />
<el-table-column property="spider" label="所属爬虫" show-overflow-tooltip /> <el-table-column property="spider" label="所属爬虫" show-overflow-tooltip />
<el-table-column property="schedule_mode" label="调度模式" show-overflow-tooltip /> <el-table-column property="schedule_mode" label="调度模式" show-overflow-tooltip />
<el-table-column property="run_status" label="调度状态" show-overflow-tooltip> <el-table-column property="run_status" label="调度状态" show-overflow-tooltip>
...@@ -52,16 +43,12 @@ ...@@ -52,16 +43,12 @@
{{ scope.row.message || '-' }} {{ scope.row.message || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="run_status" label="运行状态" show-overflow-tooltip /> <el-table-column property="run_status" label="运行状态" show-overflow-tooltip>
<!-- <el-table-column property="duration" label="持续时间" show-overflow-tooltip /> -->
<el-table-column property="create_time" label="调度时间" width="200" show-overflow-tooltip />
<!-- <el-table-column label="运行日志" width="120">
<template #default="scope"> <template #default="scope">
<el-button size="small" plain type="primary" @click="handleDetails(scope.row)"> {{ scope.row.run_status === "unknown" ? '已结束' : scope.row.run_status }}
日志
</el-button>
</template> </template>
</el-table-column> --> </el-table-column>
<el-table-column property="create_time" label="调度时间" width="200" show-overflow-tooltip />
</el-table> </el-table>
</div> </div>
<div class="pagination w-full flex flex-row-reverse pr-4 m-t-4"> <div class="pagination w-full flex flex-row-reverse pr-4 m-t-4">
...@@ -77,7 +64,17 @@ import Pagination from '@/components/pagination/index.vue' ...@@ -77,7 +64,17 @@ import Pagination from '@/components/pagination/index.vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getSpiderTaskRecord } from '@/api/spiderTask.ts' import { getSpiderTaskRecord } from '@/api/spiderTask.ts'
import { ElMessage } from 'element-plus'
// ElConfigProvider 组件
import { ElConfigProvider } from 'element-plus';
// 引入中文包
import zhCn from 'element-plus/es/locale/lang/zh-cn';
defineOptions({ name: 'Pagination' })
// 更改分页文字
zhCn.el.pagination.total = '共 `{total} 条`';
zhCn.el.pagination.goto = '跳至';
zhCn.el.pagination.pagesize = '条/页';
zhCn.el.pagination.pageClassifier = '页';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const taskValue = ref('') const taskValue = ref('')
...@@ -103,17 +100,17 @@ const pageObj = ref({ ...@@ -103,17 +100,17 @@ const pageObj = ref({
pageNo: 1, pageNo: 1,
pagerCount: 5, pagerCount: 5,
}) })
const handleDetails = (row: any) => { // 前往运行日志,暂时不要
console.log(row); // const handleDetails = (row: any) => {
router.push({ // console.log(row);
path: '/os-log/list', // router.push({
query: { // path: '/os-log/list',
id: row.id // query: {
} // id: row.id
}) // }
} // })
// }
const goToStatus = () => { const goToStatus = () => {
if (route.query.mode === '状态监控') { if (route.query.mode === '状态监控') {
router.push({ router.push({
path: '/osStatus/list', path: '/osStatus/list',
...@@ -123,8 +120,6 @@ const goToStatus = () => { ...@@ -123,8 +120,6 @@ const goToStatus = () => {
path: '/osTaskInformation/list', path: '/osTaskInformation/list',
}) })
} }
} }
// 获取任务执行记录列表 // 获取任务执行记录列表
const getData = async () => { const getData = async () => {
...@@ -132,14 +127,18 @@ const getData = async () => { ...@@ -132,14 +127,18 @@ const getData = async () => {
pageObj.value.total = res.data.total pageObj.value.total = res.data.total
tableData.value = res.data.list tableData.value = res.data.list
} }
// 数据的方法
const searchData = async () => { const searchData = async () => {
if (!timeValue.value) {
ElMessage.warning('请先选择时间段')
return
}
let resTime = [] let resTime = []
const startTime = timeValue.value[0] const startTime = timeValue.value[0]
const endTime = timeValue.value[1] const endTime = timeValue.value[1]
resTime.push(startTime) resTime.push(startTime)
resTime.push(endTime) resTime.push(endTime)
const res = await getSpiderTaskRecord({ page: pageObj.value.pageNo, size: pageObj.value.pageSize, status: 'total' ,times: resTime}) const res = await getSpiderTaskRecord({ page: pageObj.value.pageNo, size: pageObj.value.pageSize, status: 'total', times: resTime })
pageObj.value.total = res.data.total pageObj.value.total = res.data.total
tableData.value = res.data.list tableData.value = res.data.list
} }
......
<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 @@ ...@@ -43,7 +43,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import StatsTable from '@/views/stats/components/StatsTable.vue'
import StatsTool from '@/views/stats/components/StatsTool.vue' import StatsTool from '@/views/stats/components/StatsTool.vue'
import { ElConfigProvider } from 'element-plus' import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'; import zhCn from 'element-plus/es/locale/lang/zh-cn';
......
...@@ -24,13 +24,14 @@ export default defineConfig({ ...@@ -24,13 +24,14 @@ export default defineConfig({
} }
} }
}, },
base: './',
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 9529, port: 9529,
proxy: { proxy: {
// 代理API请求,使用更精确的路径匹配 // 代理API请求,使用更精确的路径匹配
'/api': { '/api': {
target: 'http://192.168.0.200:5001/', target: 'http://192.168.0.176:5001/',
changeOrigin: true, changeOrigin: true,
}, },
} }
......
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