344 lines
12 KiB
Vue
344 lines
12 KiB
Vue
<template>
|
|
<div class="monitor-container">
|
|
<div class="control-panel">
|
|
<div class="panel-left">
|
|
<el-radio-group v-model="filterStatus" size="small" class="status-group" @change="handleFilter">
|
|
<el-radio-button label="">{{ $t('crawlTask.allKeywords') }}</el-radio-button>
|
|
<el-radio-button label="0">{{ $t('crawlTask.enabled') }}</el-radio-button>
|
|
<el-radio-button label="1">{{ $t('crawlTask.disabled') }}</el-radio-button>
|
|
</el-radio-group>
|
|
|
|
<el-input
|
|
v-model="searchText"
|
|
:placeholder="$t('crawlTask.searchPlaceholder')"
|
|
prefix-icon="el-icon-search"
|
|
size="small"
|
|
class="search-box"
|
|
clearable
|
|
@keyup.enter.native="handleFilter"
|
|
/>
|
|
|
|
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearchClick">
|
|
{{ $t('crawlTask.searchBtn') }}
|
|
</el-button>
|
|
</div>
|
|
|
|
<div class="panel-right">
|
|
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAddDialog">
|
|
{{ $t('crawlTask.addKeyword') }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-loading="loading" class="task-list">
|
|
<el-empty v-if="list.length === 0" :description="$t('crawlTask.emptyResult')" />
|
|
|
|
<div
|
|
v-for="item in list"
|
|
:key="item.id"
|
|
class="task-row"
|
|
:class="item.state === 'running' ? 'is-active' : 'is-paused'"
|
|
>
|
|
<div class="col-base">
|
|
<div class="status-indicator">
|
|
<div class="status-dot"></div>
|
|
</div>
|
|
<div class="info-content">
|
|
<div class="task-name-row">
|
|
<span class="task-id">#{{ item.id }}</span>
|
|
<span class="task-name">{{ item.task_name }}</span>
|
|
</div>
|
|
<div class="task-meta">
|
|
<el-tag size="mini" effect="plain" type="info">{{ item.source }}</el-tag>
|
|
<span class="time-label"><i class="el-icon-time"></i> {{ item.create_time }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-metrics">
|
|
<div class="metric-block main">
|
|
<span class="m-label">{{ $t('crawlTask.metricExperts') }}</span>
|
|
<span class="m-value expert-count">{{ item.expert_count }}</span>
|
|
</div>
|
|
<div class="divider"></div>
|
|
<div class="metric-block">
|
|
<span class="m-label">{{ $t('crawlTask.metricPages') }}</span>
|
|
<span class="m-value">{{ item.last_page }} <small>/ {{ item.total_pages }}</small></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-progress">
|
|
<div class="prog-text">
|
|
<span>{{ $t('crawlTask.progress') }}</span>
|
|
<span class="percent">{{ item.progress }}%</span>
|
|
</div>
|
|
<el-progress
|
|
:percentage="item.progress"
|
|
:show-text="false"
|
|
:stroke-width="6"
|
|
:color="progressStrokeColor(item)"
|
|
/>
|
|
</div>
|
|
|
|
<div class="col-action">
|
|
<div class="switch-wrapper">
|
|
<span class="state-text" :class="item.state">{{
|
|
item.state === 'running' ? $t('crawlTask.stateRunning') : $t('crawlTask.stateStopped')
|
|
}}</span>
|
|
<el-switch
|
|
v-model="item.state"
|
|
active-value="running"
|
|
inactive-value="paused"
|
|
active-color="#13ce66"
|
|
inactive-color="#c0c4cc"
|
|
@change="handleToggleTask(item)"
|
|
/>
|
|
</div>
|
|
<el-button
|
|
type="primary"
|
|
size="mini"
|
|
plain
|
|
icon="el-icon-refresh-right"
|
|
:loading="runOnceLoadingId === item.id"
|
|
@click="handleRunOnce(item)"
|
|
>
|
|
{{ $t('crawlTask.runOnceBtn') }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pagination-container">
|
|
<el-pagination
|
|
background
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:current-page.sync="currentPage"
|
|
:page-size="pageSize"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:total="total"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handlePageChange"
|
|
/>
|
|
</div>
|
|
|
|
<el-dialog :title="$t('crawlTask.addKeyword')" :visible.sync="addDialogVisible" width="460px" @closed="resetAddForm">
|
|
<el-form label-width="100px" size="small">
|
|
<el-form-item :label="$t('crawlTask.keyword')">
|
|
<el-input v-model="addForm.field" :placeholder="$t('crawlTask.keywordPlaceholder')" clearable />
|
|
</el-form-item>
|
|
<el-form-item :label="$t('crawlTask.runOnce')">
|
|
<el-switch v-model="addForm.runNow" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<div slot="footer">
|
|
<el-button size="small" @click="addDialogVisible = false">{{ $t('crawlTask.cancel') }}</el-button>
|
|
<el-button type="primary" size="small" :loading="addLoading" @click="submitAddKeyword">{{ $t('crawlTask.confirm') }}</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
searchText: '',
|
|
filterStatus: '',
|
|
currentPage: 1,
|
|
pageSize: 10,
|
|
total: 0,
|
|
list: [],
|
|
addDialogVisible: false,
|
|
addLoading: false,
|
|
runOnceLoadingId: null,
|
|
addForm: { field: '', runNow: false }
|
|
};
|
|
},
|
|
created() {
|
|
this.fetchList();
|
|
},
|
|
methods: {
|
|
/** 停止态用灰条,避免 exception 大红;运行中未满用蓝,满格用绿 */
|
|
progressStrokeColor(item) {
|
|
if (item.state !== 'running') return '#c0c4cc';
|
|
if (item.progress >= 100) return '#67c23a';
|
|
return '#409eff';
|
|
},
|
|
// 数据标准化逻辑
|
|
normalizeItem(item) {
|
|
const total = Number(item.total_pages || 0);
|
|
const current = Number(item.last_page || 0);
|
|
let progress = total > 0 ? (current / total) * 100 : 0;
|
|
|
|
return {
|
|
id: item.expert_fetch_id || item.id,
|
|
task_name: item.field || '-',
|
|
source: (item.source || 'PubMed'),
|
|
expert_count: item.expert_count || 0,
|
|
total_pages: total,
|
|
last_page: current,
|
|
progress: Number(progress.toFixed(1)),
|
|
state: Number(item.state) === 0 ? 'running' : 'paused',
|
|
create_time: item.ctime_text || '-'
|
|
};
|
|
},
|
|
async fetchList() {
|
|
this.loading = true;
|
|
try {
|
|
const params = {
|
|
keyword: this.searchText,
|
|
pageIndex: this.currentPage,
|
|
pageSize: this.pageSize,
|
|
state: this.filterStatus !== '' ? this.filterStatus : undefined
|
|
};
|
|
const res = await this.$api.post('api/expert_manage/getFetchList', params);
|
|
if (res.code === 0) {
|
|
this.list = res.data.list.map(this.normalizeItem);
|
|
this.total = res.data.total;
|
|
}
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async handleToggleTask(item) {
|
|
const isNowRunning = item.state === 'running';
|
|
const newState = isNowRunning ? '0' : '1';
|
|
try {
|
|
const res = await this.$api.post('api/expert_manage/editFetchField', {
|
|
expert_fetch_id: item.id,
|
|
state: newState
|
|
});
|
|
if (res.code === 0) {
|
|
this.$message.success(isNowRunning ? this.$t('crawlTask.taskRunningMsg') : this.$t('crawlTask.taskStoppedMsg'));
|
|
} else {
|
|
item.state = isNowRunning ? 'paused' : 'running';
|
|
this.$message.error(res.msg || this.$t('crawlTask.operationFail'));
|
|
}
|
|
} catch (e) {
|
|
item.state = isNowRunning ? 'paused' : 'running';
|
|
}
|
|
},
|
|
async handleRunOnce(item) {
|
|
this.runOnceLoadingId = item.id;
|
|
try {
|
|
const res = await this.$api.post('/api/expert_finder/fetchOneField', { field: item.task_name });
|
|
if (res.code === 0) {
|
|
this.$message.success(this.$t('crawlTask.runOnceQueued'));
|
|
this.fetchList();
|
|
}
|
|
} finally {
|
|
this.runOnceLoadingId = null;
|
|
}
|
|
},
|
|
// 其余分页/弹窗逻辑
|
|
handleFilter() { this.currentPage = 1; this.fetchList(); },
|
|
handleSearchClick() { this.handleFilter(); },
|
|
handleSizeChange(val) { this.pageSize = val; this.fetchList(); },
|
|
handlePageChange(val) { this.currentPage = val; this.fetchList(); },
|
|
openAddDialog() { this.addDialogVisible = true; },
|
|
resetAddForm() { this.addForm = { field: '', runNow: false }; this.addLoading = false; },
|
|
async submitAddKeyword() {
|
|
if (!this.addForm.field.trim()) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
|
|
this.addLoading = true;
|
|
try {
|
|
const res = await this.$api.post('api/expert_manage/addFetchField', { field: this.addForm.field });
|
|
if (res.code === 0) {
|
|
this.$message.success(this.$t('crawlTask.addKeywordSuccess'));
|
|
if (this.addForm.runNow) {
|
|
await this.$api.post('api/expert_finder/fetchOneField', { field: this.addForm.field });
|
|
}
|
|
this.addDialogVisible = false;
|
|
this.fetchList();
|
|
}
|
|
} finally { this.addLoading = false; }
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.monitor-container {
|
|
padding: 20px;
|
|
background: #f5f7fa;
|
|
min-height: calc(100vh - 100px);
|
|
}
|
|
|
|
/* 控制面板 */
|
|
.control-panel {
|
|
background: #fff;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
|
}
|
|
.panel-left { display: flex; gap: 15px; }
|
|
.search-box { width: 220px; }
|
|
|
|
/* 任务行卡片设计 */
|
|
.task-row {
|
|
background: #fff;
|
|
margin-bottom: 12px;
|
|
padding: 4px 15px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
transition: all 0.3s;
|
|
border-left: 5px solid #dcdfe6;
|
|
}
|
|
.task-row:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
}
|
|
.task-row.is-active { border-left-color: #409eff; }
|
|
.task-row.is-paused { background: #fbfbfc; border-left-color: #909399; opacity: 0.9; }
|
|
|
|
/* 1. 基础信息列 */
|
|
.col-base { flex: 1.5; display: flex; align-items: flex-start; gap: 15px; }
|
|
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #909399; margin-top: 6px; }
|
|
.is-active .status-dot { background: #13ce66; box-shadow: 0 0 8px #13ce66; animation: pulse 2s infinite; }
|
|
|
|
.task-id { font-family: monospace; color: #909399; font-size: 12px; margin-right: 8px; }
|
|
.task-name { font-size: 14px; font-weight: bold; color: #303133; }
|
|
.task-meta { margin-top: 6px; display: flex; align-items: center; gap: 12px; font-size: 12px; color: #909399; }
|
|
|
|
/* 2. 指标展示列 */
|
|
.col-metrics { flex: 1.2; display: flex; align-items: center; justify-content: space-around; }
|
|
.metric-block { text-align: center; }
|
|
.m-label { font-size: 12px; color: #909399; display: block; margin-bottom: 4px; }
|
|
.m-value { font-size: 18px; font-weight: 600; color: #606266; }
|
|
.m-value small { font-weight: normal; font-size: 12px; color: #888; }
|
|
.expert-count { color: #006699; font-size: 16px; } /* 专家数高亮 */
|
|
.divider { width: 1px; height: 35px; background: #ebeef5; }
|
|
|
|
/* 3. 进度条列 */
|
|
.col-progress { flex: 1.2; padding: 0 30px; }
|
|
.prog-text { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 6px; color: #606266; }
|
|
.percent { font-weight: bold; color: #409eff; }
|
|
|
|
/* 4. 操作列 */
|
|
.col-action { flex: 1.1; display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
|
|
.switch-wrapper {
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-right: 6px;
|
|
transform: translateX(-6px);
|
|
}
|
|
.state-text { font-size: 11px; font-weight: bold; }
|
|
.state-text.running { color: #13ce66; }
|
|
.state-text.paused { color: #909399; }
|
|
|
|
/* 分页 */
|
|
.pagination-container { margin-top: 25px; text-align: right; }
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
</style> |