Files
xinfra/frontend/src/views/task/TaskCenter.vue
T

326 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="page-head">
<div>
<h1>任务中心</h1>
<p>AWX 交付任务与 Wayne 部署服务的执行记录</p>
</div>
<div class="task-filters">
<el-select v-model="sourceFilter" size="small" class="filter-select" @change="loadTasks">
<el-option label="全部来源" value="all" />
<el-option label="AWX" value="awx" />
<el-option label="Wayne" value="wayne" />
</el-select>
<el-button size="small" :loading="loadingTasks || loadingLogs" @click="refreshCurrent">刷新</el-button>
</div>
</div>
<div class="task-layout">
<div class="panel task-list-panel">
<div class="panel-head">
<h3>任务列表</h3>
<span class="meta">{{ lastLoadedText }}</span>
</div>
<div class="panel-body">
<div class="task-list">
<button
v-for="task in tasks"
:key="task.id"
type="button"
class="task-row"
:class="{ active: task.id === selectedTaskId }"
@click="selectTask(task.id)"
>
<span :class="['task-status', task.status_class]">● {{ task.status_text }}</span>
<span class="task-main">
<span class="task-name">{{ task.name }}</span>
<span class="task-runner mono">{{ task.runner }}</span>
</span>
</button>
</div>
<div v-if="!loadingTasks && !tasks.length" class="empty-state">
暂无任务记录
</div>
<div class="pagination">
<span>共 {{ tasks.length }} 条 · 当前业务线:{{ currentName }}</span>
</div>
</div>
</div>
<div class="panel task-log-panel">
<div class="panel-head">
<h3>
<span>任务日志</span>
<span class="selected-name">{{ selectedTaskName }}</span>
</h3>
<span class="meta">{{ selectedTaskMeta }}</span>
</div>
<div class="panel-body log-stream">
<div v-for="(log, index) in logs" :key="index" :class="['task-log-line', log.class]">
<span class="t">{{ log.time }}</span>{{ log.message }}
</div>
<div v-if="loadingLogs" class="task-log-line">
<span class="t">...</span>加载中
</div>
<div v-if="!loadingLogs && !logs.length" class="empty-state">
请选择任务查看日志
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { taskLogApi, type TaskLogLine, type TaskLogSummary } from '@/api/taskLog'
import { useBusinessLineStore } from '@/stores/businessLine'
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
const { currentName } = useBusinessLineMockProfile()
const businessLineStore = useBusinessLineStore()
const sourceFilter = ref('all')
const selectedTaskId = ref('')
const tasks = ref<TaskLogSummary[]>([])
const logs = ref<TaskLogLine[]>([])
const loadingTasks = ref(false)
const loadingLogs = ref(false)
const lastLoadedAt = ref<Date | null>(null)
const selectedTask = computed(() => tasks.value.find((task) => task.id === selectedTaskId.value))
const selectedTaskName = computed(() => selectedTask.value?.name || '未选择')
const selectedTaskMeta = computed(() => selectedTask.value ? `${selectedTask.value.source.toUpperCase()} · ${selectedTask.value.runner}` : '接口日志')
const lastLoadedText = computed(() => lastLoadedAt.value ? `更新于 ${formatTime(lastLoadedAt.value)}` : '')
async function loadTasks() {
loadingTasks.value = true
try {
const data = await taskLogApi.list({
source: sourceFilter.value,
businessLineId: businessLineStore.current?.id,
})
tasks.value = data.items
lastLoadedAt.value = new Date()
if (!tasks.value.some((task) => task.id === selectedTaskId.value)) {
selectedTaskId.value = tasks.value[0]?.id || ''
}
if (selectedTaskId.value) {
await loadLogs(selectedTaskId.value)
} else {
logs.value = []
}
} catch (error) {
logs.value = [{ time: formatTime(new Date()), message: error instanceof Error ? error.message : '任务日志加载失败', class: 'err' }]
} finally {
loadingTasks.value = false
}
}
async function loadLogs(taskId: string) {
loadingLogs.value = true
try {
const data = await taskLogApi.get(taskId)
logs.value = data.lines
lastLoadedAt.value = new Date()
} catch (error) {
logs.value = [{ time: formatTime(new Date()), message: error instanceof Error ? error.message : '任务日志加载失败', class: 'err' }]
} finally {
loadingLogs.value = false
}
}
function selectTask(taskId: string) {
if (selectedTaskId.value === taskId) {
return
}
selectedTaskId.value = taskId
void loadLogs(taskId)
}
function refreshCurrent() {
void loadTasks()
}
function formatTime(date: Date) {
return date.toLocaleTimeString('zh-CN', { hour12: false })
}
watch(
() => businessLineStore.current?.id,
() => {
void loadTasks()
},
)
onMounted(() => {
void loadTasks()
})
</script>
<style scoped>
/* 公共样式已在 global.css 中定义 */
.task-layout {
display: grid;
grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.task-list-panel,
.task-log-panel {
min-width: 0;
}
.task-list-panel .panel-body {
padding-top: 0;
}
.task-list {
max-height: calc(100vh - 245px);
min-height: 360px;
overflow-y: auto;
}
.task-row {
width: 100%;
min-height: 62px;
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 10px 16px;
border: 0;
border-bottom: 1px solid var(--line-soft);
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.task-row:hover,
.task-row.active {
background: var(--bg-panel-2);
}
.task-status {
font-size: 12px;
white-space: nowrap;
}
.task-status.ok {
color: var(--accent);
}
.task-status.warn {
color: var(--warn);
}
.task-status.err {
color: var(--err);
}
.task-main {
min-width: 0;
display: flex;
flex-direction: column;
gap: 5px;
}
.task-name,
.task-runner,
.selected-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-name {
color: var(--text-hi);
font-size: 12.5px;
font-weight: 600;
}
.task-runner {
color: var(--text-dim);
font-size: 11px;
}
.task-log-panel .panel-head {
gap: 14px;
}
.task-log-panel .panel-head h3 {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.selected-name {
color: var(--text-dim);
font-weight: 500;
}
.log-stream {
max-height: calc(100vh - 210px);
min-height: 520px;
overflow-y: auto;
overflow-x: auto;
}
.task-filters {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.filter-select {
width: 160px;
}
.empty-state {
padding: 18px 0;
color: var(--text-dim);
font-size: 13px;
}
@media (max-width: 1180px) {
.task-layout {
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
}
}
@media (max-width: 1024px) {
.task-layout {
grid-template-columns: 1fr;
}
.task-list,
.log-stream {
max-height: none;
}
.log-stream {
min-height: 420px;
}
}
@media (max-width: 640px) {
.filter-select {
width: 100%;
}
.task-filters {
width: 100%;
}
.task-row {
grid-template-columns: 64px minmax(0, 1fr);
padding: 10px 12px;
}
}
</style>