|
|
|
|
@ -8,7 +8,7 @@
|
|
|
|
|
<p class="page-desc">Task History Management</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 状态筛选 -->
|
|
|
|
|
<!-- 状态筛选 Tab (保留原有的状态切换) -->
|
|
|
|
|
<div class="filter-tabs">
|
|
|
|
|
<div v-for="tab in statusTabs" :key="tab.key" class="tab-item"
|
|
|
|
|
:class="{ active: currentStatus === tab.key }"
|
|
|
|
|
@ -20,85 +20,124 @@
|
|
|
|
|
|
|
|
|
|
<!-- 主内容区 -->
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
<div class="ui-card solid table-card">
|
|
|
|
|
<!-- 桌面表头 (移动端隐藏) -->
|
|
|
|
|
<div class="ui-card modern-table-card">
|
|
|
|
|
|
|
|
|
|
<!-- 【新增】工具栏:搜索与类型筛选 -->
|
|
|
|
|
<div class="toolbar-section">
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<i class="fas fa-search search-icon"></i>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model="searchKeyword"
|
|
|
|
|
placeholder="搜索任务ID或名称..."
|
|
|
|
|
class="search-input"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="type-filter">
|
|
|
|
|
<i class="fas fa-filter filter-icon"></i>
|
|
|
|
|
<select v-model="selectedTaskType" class="filter-select">
|
|
|
|
|
<option value="all">所有类型</option>
|
|
|
|
|
<option value="perturbation">通用防护 (Perturbation)</option>
|
|
|
|
|
<option value="finetune">微调验证 (Finetune)</option>
|
|
|
|
|
<option value="evaluate">数据评估 (Evaluate)</option>
|
|
|
|
|
<option value="heatmap">热力图 (Heatmap)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 桌面表头 (汉化) -->
|
|
|
|
|
<div class="list-header-row desktop-only-flex">
|
|
|
|
|
<div class="col-id sortable" @click="handleSort('task_id', $event)">ID <i :class="getSortIcon('task_id')"></i></div>
|
|
|
|
|
<div class="col-info sortable" @click="handleSort('name', $event)">任务名称 <i :class="getSortIcon('name')"></i></div>
|
|
|
|
|
<div class="col-type sortable" @click="handleSort('task_type', $event)">类型 <i :class="getSortIcon('task_type')"></i></div>
|
|
|
|
|
<div class="col-time sortable" @click="handleSort('created_at', $event)">时间 <i :class="getSortIcon('created_at')"></i></div>
|
|
|
|
|
<div class="col-status sortable" @click="handleSort('status', $event)">状态 <i :class="getSortIcon('status')"></i></div>
|
|
|
|
|
<div class="col-id sortable" @click="handleSort('task_id', $event)">
|
|
|
|
|
ID <i :class="getSortIcon('task_id')"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-info sortable" @click="handleSort('name', $event)">
|
|
|
|
|
任务名称 <i :class="getSortIcon('name')"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-type sortable" @click="handleSort('task_type', $event)">
|
|
|
|
|
类型 <i :class="getSortIcon('task_type')"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-time sortable" @click="handleSort('created_at', $event)">
|
|
|
|
|
创建时间 <i :class="getSortIcon('created_at')"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-status sortable" @click="handleSort('status', $event)">
|
|
|
|
|
状态 <i :class="getSortIcon('status')"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-action">操作</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 列表内容 -->
|
|
|
|
|
<div class="list-body">
|
|
|
|
|
<div v-if="paginatedTasks.length === 0" class="empty-state">暂无符合条件的任务</div>
|
|
|
|
|
<div v-if="paginatedTasks.length === 0" class="empty-state">
|
|
|
|
|
<i class="fas fa-inbox"></i>
|
|
|
|
|
<p>暂无符合条件的任务</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-for="task in paginatedTasks" :key="task.task_id" class="list-row">
|
|
|
|
|
|
|
|
|
|
<!-- === 1. 移动端/高缩放 专用结构 (默认隐藏) === -->
|
|
|
|
|
<!-- === 移动端/窄屏视图 === -->
|
|
|
|
|
<div class="mobile-only-block">
|
|
|
|
|
<div class="mobile-row-header">
|
|
|
|
|
<span class="id-tag">#{{ task.task_id }}</span>
|
|
|
|
|
<span class="status-badge-mobile" :class="normalizeStatus(task.status)">
|
|
|
|
|
<span class="id-badge">#{{ task.task_id }}</span>
|
|
|
|
|
<span class="status-pill" :class="normalizeStatus(task.status)">
|
|
|
|
|
{{ formatStatusLabel(task.status) }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 移动端名称 -->
|
|
|
|
|
<div class="mobile-task-name">{{ getTaskName(task) }}</div>
|
|
|
|
|
|
|
|
|
|
<div class="mobile-meta-row">
|
|
|
|
|
<span class="type-badge">{{ task.task_type }}</span>
|
|
|
|
|
<span class="type-tag">{{ formatTypeLabel(task.task_type) }}</span>
|
|
|
|
|
<span class="time-text">{{ formatDate(task.created_at) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- === 2. 桌面端 专用列 (移动端隐藏) === -->
|
|
|
|
|
<!-- 必须严格对应 Grid 的列顺序 -->
|
|
|
|
|
|
|
|
|
|
<!-- Col 1: ID -->
|
|
|
|
|
<!-- === 桌面端列 === -->
|
|
|
|
|
<div class="col-id desktop-only-cell">
|
|
|
|
|
<span class="id-tag">#{{ task.task_id }}</span>
|
|
|
|
|
<span class="id-text">#{{ task.task_id }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Col 2: Info (名称) -->
|
|
|
|
|
<div class="col-info desktop-only-cell">
|
|
|
|
|
<span class="task-name">{{ getTaskName(task) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Col 3: Type -->
|
|
|
|
|
<div class="col-type desktop-only-cell">
|
|
|
|
|
<span class="type-badge">{{ task.task_type }}</span>
|
|
|
|
|
<span class="type-tag">{{ formatTypeLabel(task.task_type) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Col 4: Time -->
|
|
|
|
|
<div class="col-time desktop-only-cell">
|
|
|
|
|
{{ formatDate(task.created_at) }}
|
|
|
|
|
<span class="time-text">{{ formatDate(task.created_at) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Col 5: Status -->
|
|
|
|
|
<div class="col-status desktop-only-cell">
|
|
|
|
|
<span class="status-dot" :class="normalizeStatus(task.status)"></span>
|
|
|
|
|
{{ formatStatusLabel(task.status) }}
|
|
|
|
|
<span class="status-pill" :class="normalizeStatus(task.status)">
|
|
|
|
|
{{ formatStatusLabel(task.status) }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Col 6: Action (共用,但样式微调) -->
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
<div class="col-action">
|
|
|
|
|
<button
|
|
|
|
|
v-if="userStore.role === 'admin'"
|
|
|
|
|
class="action-btn"
|
|
|
|
|
title="查看日志"
|
|
|
|
|
@click="handleViewLogs(task)"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-terminal"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
v-if="['pending','waiting','processing','running'].includes(task.status)"
|
|
|
|
|
class="ui-btn glass sm icon-only danger"
|
|
|
|
|
class="action-btn danger"
|
|
|
|
|
title="取消任务"
|
|
|
|
|
@click="handleCancel(task)"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-stop"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button v-if="task.status === 'completed'" class="ui-btn glass sm icon-only" title="预览结果" @click="handlePreview(task)">
|
|
|
|
|
<button v-if="task.status === 'completed'" class="action-btn primary" title="预览结果" @click="handlePreview(task)">
|
|
|
|
|
<i class="fas fa-eye"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button v-if="task.status === 'completed'" class="ui-btn glass sm icon-only" title="下载结果" @click="handleDownload(task)">
|
|
|
|
|
<button v-if="task.status === 'completed'" class="action-btn success" title="下载结果" @click="handleDownload(task)">
|
|
|
|
|
<i class="fas fa-download"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
@ -107,9 +146,13 @@
|
|
|
|
|
|
|
|
|
|
<!-- 分页器 -->
|
|
|
|
|
<div class="pagination-footer">
|
|
|
|
|
<button class="page-btn" :disabled="currentPage <= 1" @click="currentPage--"><i class="fas fa-chevron-left"></i></button>
|
|
|
|
|
<span class="page-info">{{ currentPage }} / {{ totalPages || 1 }} 页</span>
|
|
|
|
|
<button class="page-btn" :disabled="currentPage >= totalPages" @click="currentPage++"><i class="fas fa-chevron-right"></i></button>
|
|
|
|
|
<button class="page-btn" :disabled="currentPage <= 1" @click="currentPage--">
|
|
|
|
|
<i class="fas fa-chevron-left"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<span class="page-info">第 {{ currentPage }} 页 / 共 {{ totalPages || 1 }} 页</span>
|
|
|
|
|
<button class="page-btn" :disabled="currentPage >= totalPages" @click="currentPage++">
|
|
|
|
|
<i class="fas fa-chevron-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -124,15 +167,23 @@
|
|
|
|
|
|
|
|
|
|
<!-- 日志弹窗 -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="showLogModal" class="log-overlay" @click.self="showLogModal = false">
|
|
|
|
|
<div v-if="showLogModal" class="log-overlay" @click.self="showLogModal = false" @wheel.stop @touchmove.stop>
|
|
|
|
|
<div class="ui-card solid log-card">
|
|
|
|
|
<div class="log-header">
|
|
|
|
|
<h3>运行日志 Task #{{ currentLogTaskId }}</h3>
|
|
|
|
|
<div class="log-title-group">
|
|
|
|
|
<i class="fas fa-terminal"></i>
|
|
|
|
|
<h3>系统日志 Task #{{ currentLogTaskId }}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="close-btn" @click="showLogModal = false"><i class="fas fa-times"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="log-content">
|
|
|
|
|
<pre v-if="logContent">{{ logContent }}</pre>
|
|
|
|
|
<div v-else class="loading-log"><i class="fas fa-spinner fa-spin"></i> 加载日志中...</div>
|
|
|
|
|
<div class="log-body-wrapper">
|
|
|
|
|
<div class="log-content allow-scroll">
|
|
|
|
|
<pre v-if="logContent">{{ logContent }}</pre>
|
|
|
|
|
<div v-else class="loading-log">
|
|
|
|
|
<i class="fas fa-circle-notch fa-spin"></i>
|
|
|
|
|
<span>正在加载日志...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -144,12 +195,19 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
import { useTaskStore } from '@/stores/taskStore'
|
|
|
|
|
import { useUserStore } from '@/stores/userStore'
|
|
|
|
|
import { getTaskResultImages, cancelTask, getTaskLogs } from '@/api/task'
|
|
|
|
|
import JSZip from 'jszip'
|
|
|
|
|
import ImagePreviewModal from '@/components/ImagePreviewModal.vue'
|
|
|
|
|
|
|
|
|
|
const taskStore = useTaskStore()
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
|
|
|
|
// 筛选状态
|
|
|
|
|
const currentStatus = ref('all')
|
|
|
|
|
const searchKeyword = ref('') // 新增:搜索关键词
|
|
|
|
|
const selectedTaskType = ref('all') // 新增:类型筛选
|
|
|
|
|
|
|
|
|
|
const statusTabs = [
|
|
|
|
|
{ key: 'all', label: '全部' },
|
|
|
|
|
{ key: 'running', label: '进行中' },
|
|
|
|
|
@ -161,19 +219,38 @@ const currentPage = ref(1)
|
|
|
|
|
const pageSize = ref(10)
|
|
|
|
|
const showPreview = ref(false)
|
|
|
|
|
const previewTaskId = ref(null)
|
|
|
|
|
const previewTaskType = ref('')
|
|
|
|
|
|
|
|
|
|
// 日志状态
|
|
|
|
|
const showLogModal = ref(false)
|
|
|
|
|
const currentLogTaskId = ref(null)
|
|
|
|
|
const logContent = ref('')
|
|
|
|
|
const previewTaskType = ref('')
|
|
|
|
|
|
|
|
|
|
const normalizeStatus = (status) => {
|
|
|
|
|
if (['pending', 'processing', 'waiting', 'running'].includes(status)) return 'running'
|
|
|
|
|
return status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 状态汉化
|
|
|
|
|
const formatStatusLabel = (s) => {
|
|
|
|
|
const map = { running: '运行中', processing: '处理中', pending: '排队中', waiting: '排队中', completed: '已完成', failed: '失败' }
|
|
|
|
|
const map = {
|
|
|
|
|
running: '运行中', processing: '处理中', pending: '排队中', waiting: '排队中',
|
|
|
|
|
completed: '已完成', failed: '失败'
|
|
|
|
|
}
|
|
|
|
|
return map[s] || s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 类型汉化
|
|
|
|
|
const formatTypeLabel = (t) => {
|
|
|
|
|
const map = {
|
|
|
|
|
perturbation: '通用防护',
|
|
|
|
|
finetune: '微调验证',
|
|
|
|
|
evaluate: '数据评估',
|
|
|
|
|
heatmap: '热力图'
|
|
|
|
|
}
|
|
|
|
|
return map[t] || t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getTaskName = (t) => {
|
|
|
|
|
if (t.description && t.description.trim()) return t.description
|
|
|
|
|
if (t.task_type === 'finetune' && t.finetune?.finetune_name) return t.finetune.finetune_name
|
|
|
|
|
@ -186,11 +263,33 @@ const formatDate = (iso) => iso ? new Date(iso).toLocaleString('zh-CN', { hour12
|
|
|
|
|
|
|
|
|
|
const handleStatusChange = (status) => { currentStatus.value = status; currentPage.value = 1 }
|
|
|
|
|
|
|
|
|
|
// === 核心过滤逻辑修改 ===
|
|
|
|
|
const filteredAndSortedTasks = computed(() => {
|
|
|
|
|
let result = [...(taskStore.tasks || [])]
|
|
|
|
|
|
|
|
|
|
// 1. 状态筛选
|
|
|
|
|
if (currentStatus.value !== 'all') {
|
|
|
|
|
result = result.filter(t => normalizeStatus(t.status) === currentStatus.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 类型筛选 (新增)
|
|
|
|
|
if (selectedTaskType.value !== 'all') {
|
|
|
|
|
result = result.filter(t => t.task_type === selectedTaskType.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 关键词搜索 (新增)
|
|
|
|
|
if (searchKeyword.value.trim()) {
|
|
|
|
|
const keyword = searchKeyword.value.toLowerCase().trim()
|
|
|
|
|
result = result.filter(t => {
|
|
|
|
|
// 搜索 ID
|
|
|
|
|
const idMatch = String(t.task_id).includes(keyword)
|
|
|
|
|
// 搜索 任务名称
|
|
|
|
|
const nameMatch = getTaskName(t).toLowerCase().includes(keyword)
|
|
|
|
|
return idMatch || nameMatch
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 排序
|
|
|
|
|
if (sortRules.value.length > 0) {
|
|
|
|
|
result.sort((a, b) => {
|
|
|
|
|
const rule = sortRules.value[0]
|
|
|
|
|
@ -239,6 +338,23 @@ const handleCancel = async (task) => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleViewLogs = async (task) => {
|
|
|
|
|
currentLogTaskId.value = task.task_id
|
|
|
|
|
logContent.value = ''
|
|
|
|
|
showLogModal.value = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await getTaskLogs(task.task_id)
|
|
|
|
|
if (res && res.logs) {
|
|
|
|
|
logContent.value = res.logs
|
|
|
|
|
} else {
|
|
|
|
|
logContent.value = '暂无日志信息'
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logContent.value = '获取日志失败: ' + e.message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDownload = async (task) => {
|
|
|
|
|
if (task.status !== 'completed') return alert('任务未完成')
|
|
|
|
|
try {
|
|
|
|
|
@ -266,129 +382,316 @@ const handleDownload = async (task) => {
|
|
|
|
|
link.href = url
|
|
|
|
|
link.download = `task_${task.task_id}.zip`
|
|
|
|
|
link.click()
|
|
|
|
|
} catch (e) { alert('下载出错') }
|
|
|
|
|
} catch (e) { alert('下载出错: ' + e.message) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => taskStore.fetchTasks())
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* === 1. 基础布局 === */
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Layout & Container
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.view-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
/* margin: auto 配合 flex column,在高度足够时居中,不足时置顶 */
|
|
|
|
|
margin: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
padding: 40px 20px 120px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-section { flex: 0 0 auto; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: flex-end; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
|
|
|
.page-header { font-size: 2.5rem; margin: 0; line-height: 1; color: var(--color-contrast-dark); }
|
|
|
|
|
.page-desc { color: #999; margin-top: 5px; }
|
|
|
|
|
.filter-tabs { display: flex; background: rgba(255,255,255,0.5); padding: 4px; border-radius: 12px; gap: 5px; flex-wrap: wrap;}
|
|
|
|
|
.tab-item { padding: 8px 20px; border-radius: 8px; font-size: 0.9rem; color: #999; cursor: pointer; font-weight: 600; white-space: nowrap; }
|
|
|
|
|
.tab-item.active { background: var(--color-contrast-dark); }
|
|
|
|
|
width: 100%; margin: auto; display: flex; flex-direction: column;
|
|
|
|
|
box-sizing: border-box; padding: 40px 20px 120px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-section {
|
|
|
|
|
flex: 0 0 auto; margin-bottom: 30px;
|
|
|
|
|
display: flex; justify-content: space-between; align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
.page-header {
|
|
|
|
|
font-size: 2.2rem; margin: 0; line-height: 1.1;
|
|
|
|
|
color: var(--color-contrast-dark); letter-spacing: -0.5px;
|
|
|
|
|
}
|
|
|
|
|
.page-desc { color: #94a3b8; margin-top: 6px; font-weight: 500; }
|
|
|
|
|
|
|
|
|
|
.filter-tabs {
|
|
|
|
|
display: flex; background: rgba(255,255,255,0.6);
|
|
|
|
|
padding: 5px; border-radius: 12px; gap: 5px;
|
|
|
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
}
|
|
|
|
|
.tab-item {
|
|
|
|
|
padding: 8px 20px; border-radius: 8px; font-size: 0.9rem;
|
|
|
|
|
color: #64748b; cursor: pointer; font-weight: 600;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
.tab-item:hover { color: var(--color-contrast-dark); background: rgba(0,0,0,0.03); }
|
|
|
|
|
.tab-item.active {
|
|
|
|
|
background: var(--color-contrast-dark); color: #fff;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-content { flex: 1; width: 100%; max-width: 1400px; margin: 0 auto; }
|
|
|
|
|
.table-card { height: auto; min-height: 400px; display: flex; flex-direction: column; padding: 0; background: #fff; }
|
|
|
|
|
|
|
|
|
|
/* === 2. 桌面端 Grid 布局 (默认) === */
|
|
|
|
|
.list-header-row, .list-row {
|
|
|
|
|
.modern-table-card {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
box-shadow: 0 20px 40px -5px rgba(0,0,0,0.05);
|
|
|
|
|
border: 1px solid rgba(0,0,0,0.02);
|
|
|
|
|
display: flex; flex-direction: column;
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Toolbar (New)
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.toolbar-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 15px;
|
|
|
|
|
padding: 20px 30px 10px 30px;
|
|
|
|
|
border-bottom: 1px dashed #f1f5f9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 300px;
|
|
|
|
|
}
|
|
|
|
|
.search-icon {
|
|
|
|
|
position: absolute; top: 50%; left: 12px; transform: translateY(-50%);
|
|
|
|
|
color: #94a3b8; font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
.search-input {
|
|
|
|
|
width: 100%; padding: 10px 10px 10px 35px;
|
|
|
|
|
border: 1px solid #e2e8f0; border-radius: 8px;
|
|
|
|
|
font-size: 0.9rem; color: #334155;
|
|
|
|
|
transition: all 0.2s; background: #f8fafc;
|
|
|
|
|
}
|
|
|
|
|
.search-input:focus {
|
|
|
|
|
background: #fff; border-color: var(--color-accent-secondary);
|
|
|
|
|
outline: none; box-shadow: 0 0 0 3px rgba(255, 159, 28, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.type-filter {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
.filter-icon {
|
|
|
|
|
position: absolute; top: 50%; left: 12px; transform: translateY(-50%);
|
|
|
|
|
color: #94a3b8; font-size: 0.9rem; pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
.filter-select {
|
|
|
|
|
padding: 10px 30px 10px 35px;
|
|
|
|
|
border: 1px solid #e2e8f0; border-radius: 8px;
|
|
|
|
|
font-size: 0.9rem; color: #334155;
|
|
|
|
|
background: #f8fafc; cursor: pointer;
|
|
|
|
|
appearance: none; /* remove default arrow */
|
|
|
|
|
}
|
|
|
|
|
.filter-select:focus {
|
|
|
|
|
background: #fff; border-color: var(--color-accent-secondary);
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Table Header & Body
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.list-header-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
/* 严格的 6 列定义 */
|
|
|
|
|
grid-template-columns: 80px 1.5fr 100px 180px 120px 160px;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
grid-template-columns: 80px 2fr 120px 180px 140px 160px;
|
|
|
|
|
padding: 20px 30px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
border-bottom: 1px solid #f1f1f1;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-bottom: 2px solid #e2e8f0; /* 加深线条 */
|
|
|
|
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-header-row {
|
|
|
|
|
background: #f8f9fa; font-weight: 700; color: #666; border-bottom: 1px solid #eee;
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
.list-body { flex: 1; overflow-y: auto; padding: 0; }
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
|
|
|
height: 300px; color: #cbd5e1; font-size: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.empty-state i { font-size: 3rem; margin-bottom: 15px; opacity: 0.5; }
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Table Rows (Clean Style)
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.list-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 80px 2fr 120px 180px 140px 160px;
|
|
|
|
|
padding: 18px 30px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
/* 分隔线设计 */
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
background: #fff;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移除悬浮特效,仅保留背景色微调 */
|
|
|
|
|
.list-row:hover {
|
|
|
|
|
background-color: #f8fafc; /* 极淡的灰色 */
|
|
|
|
|
/* transform: none; box-shadow: none; removed */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.id-text { font-family: monospace; color: #94a3b8; font-weight: 600; }
|
|
|
|
|
.task-name {
|
|
|
|
|
font-weight: 600; color: #334155; font-size: 0.95rem;
|
|
|
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
|
|
|
padding-right: 20px;
|
|
|
|
|
}
|
|
|
|
|
.time-text { color: #64748b; font-size: 0.85rem; font-variant-numeric: tabular-nums; }
|
|
|
|
|
|
|
|
|
|
.type-tag {
|
|
|
|
|
display: inline-block; padding: 4px 10px; border-radius: 6px;
|
|
|
|
|
font-size: 0.75rem; font-weight: 600;
|
|
|
|
|
background: #f1f5f9; color: #475569;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-body { overflow: visible; padding: 0 10px; }
|
|
|
|
|
.list-row:hover { background: #fcfcfc; }
|
|
|
|
|
/* Status Pills */
|
|
|
|
|
.status-pill {
|
|
|
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
|
|
|
padding: 4px 12px; border-radius: 20px;
|
|
|
|
|
font-size: 0.75rem; font-weight: 700;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
.status-pill.completed { background: #dcfce7; color: #166534; }
|
|
|
|
|
.status-pill.running { background: #e0f2fe; color: #075985; }
|
|
|
|
|
.status-pill.failed { background: #fee2e2; color: #991b1b; }
|
|
|
|
|
|
|
|
|
|
/* 通用元素样式 */
|
|
|
|
|
/* Actions */
|
|
|
|
|
.col-action { display: flex; justify-content: flex-end; gap: 8px; }
|
|
|
|
|
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
|
|
|
|
.status-dot.running { background: var(--color-accent-secondary); box-shadow: 0 0 5px var(--color-accent-secondary); }
|
|
|
|
|
.status-dot.completed { background: #2e7d32; }
|
|
|
|
|
.status-dot.failed { background: #c62828; }
|
|
|
|
|
.type-badge { font-size: 0.8rem; background: rgba(255, 159, 28, 0.1); color: var(--color-accent-secondary); padding: 2px 6px; border-radius: 4px; }
|
|
|
|
|
.id-tag { background: #eee; padding: 2px 6px; border-radius: 4px; font-weight: bold; color: #666; font-size: 0.8rem; }
|
|
|
|
|
.task-name { font-weight: 600; font-size: 0.95rem; }
|
|
|
|
|
.ui-btn.sm { padding: 6px 12px; font-size: 0.9rem; }
|
|
|
|
|
.ui-btn.icon-only { width: 32px; height: 32px; padding: 0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; }
|
|
|
|
|
.ui-btn.icon-only.danger { color: #dc3545; background: rgba(220, 53, 69, 0.1); }
|
|
|
|
|
.ui-btn.icon-only.danger:hover { background: #dc3545; color: #fff; }
|
|
|
|
|
.pagination-footer { padding: 20px; display: flex; justify-content: center; align-items: center; border-top: 1px solid #eee; margin-top: auto; }
|
|
|
|
|
.page-btn { background: none; border: 1px solid #ddd; width: 30px; height: 30px; border-radius: 4px; cursor: pointer; margin: 0 10px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
.sort-icon { font-size: 0.8rem; margin-left: 4px; }
|
|
|
|
|
.action-btn {
|
|
|
|
|
width: 34px; height: 34px; border-radius: 8px;
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
border: none; cursor: pointer; transition: all 0.2s;
|
|
|
|
|
background: transparent; color: #94a3b8;
|
|
|
|
|
}
|
|
|
|
|
.action-btn:hover { background: #f1f5f9; color: #334155; }
|
|
|
|
|
.action-btn.primary:hover { background: #e0f2fe; color: #0284c7; }
|
|
|
|
|
.action-btn.success:hover { background: #dcfce7; color: #16a34a; }
|
|
|
|
|
.action-btn.danger:hover { background: #fee2e2; color: #dc2626; }
|
|
|
|
|
|
|
|
|
|
/* Sorting */
|
|
|
|
|
.sortable { cursor: pointer; user-select: none; transition: color 0.2s; }
|
|
|
|
|
.sortable:hover { color: #334155; }
|
|
|
|
|
.sort-icon { margin-left: 5px; font-size: 0.7rem; }
|
|
|
|
|
.sort-icon.active { color: var(--color-accent-secondary); }
|
|
|
|
|
.sort-icon.dim { opacity: 0.2; }
|
|
|
|
|
|
|
|
|
|
/* === 3. 显隐控制类 === */
|
|
|
|
|
.mobile-only-block { display: none; } /* 默认隐藏移动端块 */
|
|
|
|
|
.sort-icon.dim { opacity: 0.3; }
|
|
|
|
|
|
|
|
|
|
/* Pagination */
|
|
|
|
|
.pagination-footer {
|
|
|
|
|
padding: 20px 30px;
|
|
|
|
|
display: flex; justify-content: flex-end; align-items: center;
|
|
|
|
|
border-top: 1px solid #e2e8f0; /* 分隔线 */
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
.page-info { margin: 0 15px; color: #64748b; font-size: 0.9rem; font-weight: 500; }
|
|
|
|
|
.page-btn {
|
|
|
|
|
width: 36px; height: 36px; border-radius: 8px;
|
|
|
|
|
border: 1px solid #e2e8f0; background: #fff; color: #64748b;
|
|
|
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.page-btn:hover:not(:disabled) { border-color: #cbd5e1; color: #334155; background: #f8fafc; }
|
|
|
|
|
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; border-color: #f1f5f9; }
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Mobile Adaptation
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.mobile-only-block { display: none; }
|
|
|
|
|
.desktop-only-flex { display: grid; }
|
|
|
|
|
.desktop-only-cell { display: block; } /* Grid 单元格默认显示 */
|
|
|
|
|
.desktop-only-cell { display: block; }
|
|
|
|
|
|
|
|
|
|
/* === 4. 高缩放/窄屏适配 (宽度 < 900px) === */
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.view-container {
|
|
|
|
|
padding-top: 60px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.view-container { padding: 80px 15px 100px; }
|
|
|
|
|
.header-section { flex-direction: column; align-items: flex-start; gap: 15px; }
|
|
|
|
|
.filter-tabs { width: 100%; overflow-x: auto; padding-bottom: 5px; }
|
|
|
|
|
|
|
|
|
|
/* 隐藏桌面端元素 */
|
|
|
|
|
.desktop-only-flex, .desktop-only-cell { display: none !important; }
|
|
|
|
|
|
|
|
|
|
/* 显示移动端元素 */
|
|
|
|
|
.mobile-only-block { display: block; }
|
|
|
|
|
.filter-tabs { width: 100%; overflow-x: auto; padding-bottom: 5px; -webkit-overflow-scrolling: touch; }
|
|
|
|
|
|
|
|
|
|
/* 移动端隐藏工具栏的部分样式,或者调整布局 */
|
|
|
|
|
.toolbar-section { flex-direction: column; align-items: stretch; padding: 15px; }
|
|
|
|
|
.search-box { width: 100%; }
|
|
|
|
|
|
|
|
|
|
.list-header-row, .desktop-only-cell { display: none !important; }
|
|
|
|
|
.mobile-only-block { display: block; width: 100%; }
|
|
|
|
|
|
|
|
|
|
/* 将每一行改为 Flex Column 卡片式布局 */
|
|
|
|
|
.list-body { padding: 0; }
|
|
|
|
|
.list-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
border: 1px solid #eee;
|
|
|
|
|
display: flex; flex-direction: column; gap: 12px;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
border-bottom: 10px solid #f1f5f9; /* 移动端用粗线条分隔 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端内部布局 */
|
|
|
|
|
.mobile-row-header {
|
|
|
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
|
|
|
border-bottom: 1px dashed #e0e0e0; padding-bottom: 8px; margin-bottom: 5px;
|
|
|
|
|
|
|
|
|
|
.mobile-row-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
.id-badge { background: #f1f5f9; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; color: #64748b; font-weight: 700; }
|
|
|
|
|
|
|
|
|
|
.mobile-task-name { font-size: 1.1rem; font-weight: 700; color: #1e293b; margin: 5px 0; }
|
|
|
|
|
.mobile-meta-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #94a3b8; }
|
|
|
|
|
|
|
|
|
|
.col-action {
|
|
|
|
|
margin-top: 10px; padding-top: 15px; border-top: 1px dashed #e2e8f0;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge-mobile { display: inline-block; font-size: 0.8rem; padding: 2px 8px; border-radius: 99px; font-weight: 600; }
|
|
|
|
|
.status-badge-mobile.running { background: rgba(255, 159, 28, 0.1); color: var(--color-accent-secondary); }
|
|
|
|
|
.status-badge-mobile.completed { background: #e8f5e9; color: #2e7d32; }
|
|
|
|
|
.status-badge-mobile.failed { background: #ffebee; color: #c62828; }
|
|
|
|
|
|
|
|
|
|
.mobile-task-name { font-size: 1.1rem; display: block; margin-bottom: 5px; font-weight: 600; color: var(--color-contrast-dark); }
|
|
|
|
|
|
|
|
|
|
.mobile-meta-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #888; }
|
|
|
|
|
|
|
|
|
|
.col-action { margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; }
|
|
|
|
|
.ui-btn.icon-only { width: 40px; height: 40px; font-size: 1.1rem; }
|
|
|
|
|
.action-btn { width: 44px; height: 44px; font-size: 1.1rem; background: #f8fafc; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 弹窗样式 */
|
|
|
|
|
.log-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 2000; display: flex; justify-content: center; align-items: center; }
|
|
|
|
|
.log-card { width: 600px; height: 500px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; background: #fff; border-radius: 12px; }
|
|
|
|
|
.log-header { padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; font-weight: bold; }
|
|
|
|
|
.log-content { flex: 1; padding: 20px; overflow-y: auto; background: #1e1e1e; color: #0f0; font-family: monospace; font-size: 0.9rem; margin: 0; }
|
|
|
|
|
.log-content pre { margin: 0; white-space: pre-wrap; word-break: break-all; }
|
|
|
|
|
.loading-log { color: #888; text-align: center; margin-top: 50px; }
|
|
|
|
|
.close-btn { background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #999; }
|
|
|
|
|
/*
|
|
|
|
|
------------------------------------------
|
|
|
|
|
Log Modal (Dark Theme - VS Code Style)
|
|
|
|
|
------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
.log-overlay {
|
|
|
|
|
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
|
|
|
background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
display: flex; justify-content: center; align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.log-card {
|
|
|
|
|
width: 900px; height: 80vh; max-width: 95vw;
|
|
|
|
|
display: flex; flex-direction: column;
|
|
|
|
|
background: #1e1e2e; border-radius: 12px; overflow: hidden;
|
|
|
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
|
|
|
|
|
border: 1px solid #333;
|
|
|
|
|
}
|
|
|
|
|
.log-header {
|
|
|
|
|
padding: 15px 20px; background: #252535; border-bottom: 1px solid #333;
|
|
|
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
|
|
|
color: #e0e0e0; flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.log-title-group { display: flex; align-items: center; gap: 10px; }
|
|
|
|
|
.log-title-group i { color: var(--color-accent-secondary); }
|
|
|
|
|
.log-header h3 { margin: 0; font-size: 1rem; font-family: sans-serif; letter-spacing: 0.5px; }
|
|
|
|
|
.close-btn {
|
|
|
|
|
background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #888;
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.close-btn:hover { color: #fff; }
|
|
|
|
|
.log-body-wrapper { flex: 1; position: relative; overflow: hidden; background: #1e1e2e; }
|
|
|
|
|
.log-content {
|
|
|
|
|
width: 100%; height: 100%; padding: 20px;
|
|
|
|
|
overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
.log-content::-webkit-scrollbar { width: 10px; }
|
|
|
|
|
.log-content::-webkit-scrollbar-track { background: #1e1e2e; }
|
|
|
|
|
.log-content::-webkit-scrollbar-thumb { background-color: #444; border-radius: 5px; border: 2px solid #1e1e2e; }
|
|
|
|
|
.log-content pre {
|
|
|
|
|
margin: 0; white-space: pre-wrap; word-break: break-all;
|
|
|
|
|
font-family: 'Consolas', 'Monaco', 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 0.9rem; line-height: 1.6; color: #d4d4d4;
|
|
|
|
|
}
|
|
|
|
|
.loading-log {
|
|
|
|
|
height: 100%; display: flex; flex-direction: column;
|
|
|
|
|
align-items: center; justify-content: center; gap: 15px; color: #888;
|
|
|
|
|
}
|
|
|
|
|
.loading-log i { font-size: 2rem; color: var(--color-accent-secondary); }
|
|
|
|
|
</style>
|