style(frontend): 全站背景图 webp 化(bg.jpg 4MB→251KB 压缩 + bg.webp)+品牌工具页默认策略调优(品牌检测默认改精确匹配 Simple、Terms 后置)+任务面板/登录页/亚马逊控制台观感微调
This commit is contained in:
@@ -11,22 +11,22 @@
|
||||
class="input-tab"
|
||||
:class="{ active: inputMode === 'asin' }"
|
||||
@click="inputMode = 'asin'"
|
||||
>粘贴 ASIN</button>
|
||||
>输入ASIN</button>
|
||||
<button
|
||||
type="button"
|
||||
class="input-tab"
|
||||
:class="{ active: inputMode === 'file' }"
|
||||
@click="inputMode = 'file'"
|
||||
>Excel 文件</button>
|
||||
>上传文件</button>
|
||||
</div>
|
||||
|
||||
<div v-if="inputMode === 'asin'" class="input-panel">
|
||||
<div class="hint">每行一个 ASIN,支持批量粘贴。</div>
|
||||
<div class="hint">请输入ASIN,一行一个</div>
|
||||
<textarea
|
||||
v-model="asinText"
|
||||
class="asin-textarea"
|
||||
rows="9"
|
||||
placeholder="每行一个 ASIN,例如: B0xxxxxxxx"
|
||||
placeholder="请输入ASIN,一行一个"
|
||||
></textarea>
|
||||
<div class="asin-count">已输入 <b>{{ asinLines.length }}</b> 个 ASIN</div>
|
||||
</div>
|
||||
@@ -46,24 +46,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">采集国家(可多选)</div>
|
||||
<div class="section-title">采集国家(单选)</div>
|
||||
<div class="country-group">
|
||||
<label
|
||||
v-for="country in COUNTRY_OPTIONS"
|
||||
:key="country.code"
|
||||
class="country-chip"
|
||||
:class="{ selected: selectedCountries.includes(country.code) }"
|
||||
:class="{ selected: selectedCountryCode === country.code }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
name="variant-country"
|
||||
:value="country.code"
|
||||
v-model="selectedCountries"
|
||||
v-model="selectedCountryCode"
|
||||
/>
|
||||
<span>{{ country.label }}({{ country.code }})</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="country-hint" :class="{ error: selectedCountries.length === 0 }">
|
||||
{{ selectedCountries.length === 0 ? '至少选择一个国家' : `已选 ${selectedCountries.length} 个国家` }}
|
||||
<p class="country-hint" :class="{ error: !selectedCountryCode }">
|
||||
{{ selectedCountryCode ? `当前选择:${countryLabel(selectedCountryCode)}(${selectedCountryCode})` : '请选择一个采集国家' }}
|
||||
</p>
|
||||
|
||||
<div v-if="!hasBridge" class="bridge-tip">
|
||||
@@ -77,57 +78,54 @@
|
||||
:disabled="!hasBridge || submitting || !canSubmit"
|
||||
@click="submitTask"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '开始采集(添加到任务队列)' }}
|
||||
{{ submitting ? '提交中...' : '开始添加任务' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="loading-msg">
|
||||
任务在远程批处理队列执行,提交后到右侧查看进度并下载结果文件。
|
||||
提交后任务开始执行,进度和结果在右侧查看,完成后可下载结果文件。
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<section class="right-panel">
|
||||
<div class="panel-toolbar">
|
||||
<div class="queue-header">
|
||||
<span class="queue-title">任务队列</span>
|
||||
<button type="button" class="btn-refresh" @click="refreshList">刷新任务状态</button>
|
||||
<div v-if="totalCount" class="toolbar-pagination">
|
||||
</div>
|
||||
<div class="queue-body">
|
||||
<div v-if="!currentTasks.length" class="empty-tasks">暂无变体采集任务</div>
|
||||
<div v-else class="queue-grid">
|
||||
<div v-for="item in currentTasks" :key="`vc-${item.task_id}`" class="task-card">
|
||||
<div class="task-card-head">
|
||||
<span class="task-card-id">任务 {{ item.task_id }}</span>
|
||||
<span class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</span>
|
||||
</div>
|
||||
<div class="task-card-meta">
|
||||
<div class="files">国家:{{ countryText(item) || '-' }}</div>
|
||||
<div class="files">任务创建时间:{{ formatDateTime(item.create_time || item.updated_at) }}</div>
|
||||
<div class="files">最后更新时间:{{ formatDateTime(item.update_time || item.updated_at) }}</div>
|
||||
<div v-if="taskMeta(item).source" class="files">来源:{{ taskMeta(item).source }}</div>
|
||||
</div>
|
||||
<div class="task-card-msg">{{ latestMessage(item) }}</div>
|
||||
<div class="task-card-actions">
|
||||
<button type="button" class="act-btn" @click="showDetail(item)">查看任务详情</button>
|
||||
<button v-if="hasResultFiles(item)" type="button" class="act-btn ok"
|
||||
@click="downloadResult(item)">下载结果</button>
|
||||
<button v-if="hasSourceFile(item)" type="button" class="act-btn"
|
||||
@click="downloadSource(item)">下载源文件</button>
|
||||
<button type="button" class="act-btn warn" @click="reexportTask(item)">重新导出数据</button>
|
||||
<button type="button" class="act-btn danger" @click="deleteTask(item)">删除任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTasks.length" class="pagination">
|
||||
<span class="page-info">共 {{ totalCount }} 条 · 第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<div class="page-btns">
|
||||
<button type="button" class="page-btn" :disabled="currentPage <= 1" @click="goPage(currentPage - 1)">上一页</button>
|
||||
<span class="page-now">{{ currentPage }}</span>
|
||||
<button type="button" class="page-btn" :disabled="currentPage >= totalPages" @click="goPage(currentPage + 1)">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TaskCenterPanel
|
||||
title="变体采集任务"
|
||||
:cards="variantCards"
|
||||
:current-items="currentTaskViews"
|
||||
:history-items="historyTaskViews"
|
||||
current-title="当前任务"
|
||||
current-empty-text="暂无运行中的变体采集任务"
|
||||
history-empty-text="暂无历史记录"
|
||||
>
|
||||
<template #item-actions="{ item }">
|
||||
<template v-if="itemSource(item)">
|
||||
<button type="button" class="act-btn" @click="refreshTask(itemSource(item))">更新</button>
|
||||
<button type="button" class="act-btn" @click="showDetail(itemSource(item))">详情</button>
|
||||
<button v-if="hasResultFiles(itemSource(item))" type="button" class="act-btn ok"
|
||||
@click="downloadResult(itemSource(item))">下载结果</button>
|
||||
<button v-if="hasSourceFile(itemSource(item))" type="button" class="act-btn"
|
||||
@click="downloadSource(itemSource(item))">下载源文件</button>
|
||||
<button type="button" class="act-btn warn" @click="reexportTask(itemSource(item))">重新导出</button>
|
||||
</template>
|
||||
</template>
|
||||
<template #history-item-actions="{ item }">
|
||||
<template v-if="itemSource(item)">
|
||||
<button type="button" class="act-btn" @click="refreshTask(itemSource(item))">更新</button>
|
||||
<button type="button" class="act-btn" @click="showDetail(itemSource(item))">详情</button>
|
||||
<button v-if="hasResultFiles(itemSource(item))" type="button" class="act-btn ok"
|
||||
@click="downloadResult(itemSource(item))">下载结果</button>
|
||||
<button v-if="hasSourceFile(itemSource(item))" type="button" class="act-btn"
|
||||
@click="downloadSource(itemSource(item))">下载源文件</button>
|
||||
<button type="button" class="act-btn warn" @click="reexportTask(itemSource(item))">重新导出</button>
|
||||
</template>
|
||||
</template>
|
||||
</TaskCenterPanel>
|
||||
</section>
|
||||
</div>
|
||||
</AmazonToolPageShell>
|
||||
@@ -162,8 +160,6 @@
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'
|
||||
import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue'
|
||||
import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types'
|
||||
import {
|
||||
getPywebviewApi,
|
||||
type VariantTaskListItem,
|
||||
@@ -190,7 +186,7 @@ const PAGE_SIZE = 8
|
||||
const inputMode = ref<'asin' | 'file'>('asin')
|
||||
const asinText = ref('')
|
||||
const selectedFiles = ref<string[]>([])
|
||||
const selectedCountries = ref<string[]>([])
|
||||
const selectedCountryCode = ref<string>('DE')
|
||||
const submitting = ref(false)
|
||||
|
||||
const tasks = reactive<Record<string, VariantEntry>>({})
|
||||
@@ -212,29 +208,15 @@ const canSubmit = computed(() =>
|
||||
inputMode.value === 'asin' ? asinLines.value.length > 0 : selectedFiles.value.length > 0,
|
||||
)
|
||||
|
||||
// ---- 右侧任务面板统一视图(TaskCenterPanel)----
|
||||
/** 当前任务:排队中(0) / 执行中(1) 的任务(当前分页内) */
|
||||
const currentTaskViews = computed<TaskItemView[]>(() =>
|
||||
currentTasks.value.filter((item) => isBusy(item.status)).map(toVariantTaskView),
|
||||
)
|
||||
|
||||
/** 历史任务:已完成(2) / 失败(3) 的任务(当前分页内) */
|
||||
const historyTaskViews = computed<TaskItemView[]>(() =>
|
||||
currentTasks.value.filter((item) => !isBusy(item.status)).map(toVariantTaskView),
|
||||
)
|
||||
|
||||
const variantCards = computed<TaskStatCard[]>(() => [
|
||||
{ label: '运行中任务', value: currentTaskViews.value.length },
|
||||
{ label: '已结束任务', value: historyTaskViews.value.length },
|
||||
{ label: '成功任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'success').length },
|
||||
{ label: '失败任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'failed').length },
|
||||
])
|
||||
|
||||
function itemSource(item: TaskItemView): VariantEntry {
|
||||
return item.source as VariantEntry
|
||||
function countryLabel(code: string) {
|
||||
return COUNTRY_OPTIONS.find((row) => row.code === code)?.label || code
|
||||
}
|
||||
|
||||
function variantStatusClass(status?: number | string) {
|
||||
function taskMeta(item: VariantEntry) {
|
||||
return item.meta || {}
|
||||
}
|
||||
|
||||
function statusClass(status?: number | string) {
|
||||
const numeric = Number(status)
|
||||
if (numeric === 1 || /running|processing/i.test(String(status))) return 'running'
|
||||
if (numeric === 2 || /success|completed|done/i.test(String(status))) return 'success'
|
||||
@@ -242,29 +224,6 @@ function variantStatusClass(status?: number | string) {
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function toVariantTaskView(item: VariantEntry): TaskItemView {
|
||||
const extraLines: string[] = []
|
||||
const source = item.meta?.source || ''
|
||||
if (source) extraLines.push(`来源:${source}`)
|
||||
extraLines.push(`采集国家:${countryText(item) || '-'}`)
|
||||
const updateText = item.update_time || formatDateTime(item.updated_at)
|
||||
if (updateText) extraLines.push(`更新时间:${updateText}`)
|
||||
extraLines.push(latestMessage(item))
|
||||
return {
|
||||
key: `vc-${item.task_id}`,
|
||||
title: `任务 ${item.task_id}`,
|
||||
taskId: item.task_id,
|
||||
// 桥接列表未提供任务开始时间,开始时间占位为 '-'
|
||||
startedAt: '-',
|
||||
// 桥接仅提供最近更新时间,作为结束时间的近似展示
|
||||
finishedAt: formatDateTime(item.update_time),
|
||||
statusText: statusText(item.status),
|
||||
statusClass: variantStatusClass(item.status),
|
||||
extraLines,
|
||||
source: item,
|
||||
}
|
||||
}
|
||||
|
||||
let autoTimer: number | null = null
|
||||
let refreshTimer: number | null = null
|
||||
|
||||
@@ -369,8 +328,8 @@ async function submitTask() {
|
||||
ElMessage.warning('请在桌面客户端中打开本页面执行变体采集')
|
||||
return
|
||||
}
|
||||
if (!selectedCountries.value.length) {
|
||||
ElMessage.warning('请至少选择一个采集国家')
|
||||
if (!selectedCountryCode.value) {
|
||||
ElMessage.warning('请选择采集国家')
|
||||
return
|
||||
}
|
||||
if (!uid()) {
|
||||
@@ -386,7 +345,7 @@ async function submitTask() {
|
||||
files?: string[]
|
||||
} = {
|
||||
mode: inputMode.value,
|
||||
countries: selectedCountries.value,
|
||||
countries: selectedCountryCode.value ? [selectedCountryCode.value] : [],
|
||||
uid: uid(),
|
||||
}
|
||||
if (inputMode.value === 'asin') {
|
||||
@@ -473,6 +432,7 @@ async function loadTasks(page?: number) {
|
||||
const entry = tasks[item.task_id]
|
||||
entry.status = item.status
|
||||
if (item.update_time) entry.update_time = item.update_time
|
||||
if (item.create_time) entry.create_time = item.create_time
|
||||
entry.res_type = item.res_type
|
||||
entry.res_mes = item.res_mes
|
||||
entry.file_url = item.file_url
|
||||
@@ -542,6 +502,26 @@ async function downloadSource(item: VariantTaskListItem) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务:当前客户端/后端未提供删除接口(batch 仅 add/query/list/export),
|
||||
* 先提示用户操作不可用,避免出现"已删除"假象
|
||||
*/
|
||||
async function deleteTask(item: VariantTaskListItem) {
|
||||
const api = getPywebviewApi()
|
||||
const hasVcDelete = Boolean((api as Record<string, unknown> | null)?.['vc_delete_task'])
|
||||
if (!hasVcDelete) {
|
||||
ElMessage.warning('当前客户端不支持删除任务,可在后台保存或稍后清理')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await (api as { vc_delete_task: (taskId: string) => Promise<{ success?: boolean; error?: string }> })
|
||||
.vc_delete_task(item.task_id)
|
||||
void loadTasks(currentPage.value || 1)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function reexportTask(item: VariantTaskListItem) {
|
||||
const bridge = getPywebviewApi()
|
||||
if (!bridge?.vc_export_task) {
|
||||
@@ -657,48 +637,66 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.module-page { min-height: 100vh; background: #151a25; }
|
||||
.module-page { min-height: 100vh; background: #242424; }
|
||||
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
|
||||
.left-panel { width: 420px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
|
||||
.right-panel { flex: 1; min-width: 0; background: #151a25; display: flex; flex-direction: column; }
|
||||
.left-panel { width: 420px; background: #242424; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
|
||||
.right-panel { flex: 1; min-width: 0; background: #242424; display: flex; flex-direction: column; }
|
||||
.section-title { font-size: 13px; color: #a0acbe; margin: 14px 0 10px; }
|
||||
.hint, .loading-msg, .files, .country-hint { color: #5e6878; font-size: 12px; line-height: 1.5; }
|
||||
.hint { margin-bottom: 8px; }
|
||||
.input-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||
.input-tab { flex: 1; padding: 9px 0; border: 1px solid #3e4a62; border-radius: 8px; background: #2e3a52; color: #a0acbe; font-size: 13px; cursor: pointer; }
|
||||
.input-tab.active { background: rgba(64, 158, 255, 0.15); color: #409eff; border-color: #409eff; }
|
||||
.input-tab { flex: 1; padding: 9px 0; border: 1px solid #3e4a62; border-radius: 8px; background: #242424; color: #a0acbe; font-size: 13px; cursor: pointer; }
|
||||
.input-tab.active { background: #409eff; color: #ffffff; border-color: #409eff; }
|
||||
.input-panel { margin-bottom: 4px; }
|
||||
.asin-textarea { width: 100%; box-sizing: border-box; min-height: 150px; padding: 10px; background: #151a25; color: #c8d2e2; border: 1px solid #3e4a62; border-radius: 8px; font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
|
||||
.asin-textarea { width: 100%; box-sizing: border-box; min-height: 150px; padding: 10px; background: #242424; color: #c8d2e2; border: 1px solid #3e4a62; border-radius: 8px; font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
|
||||
.asin-textarea:focus { border-color: #409eff; }
|
||||
.asin-count { margin-top: 8px; color: #5e6878; font-size: 12px; }
|
||||
.asin-count b { color: #c8d2e2; }
|
||||
.asin-count b { color: #f5f8fc; }
|
||||
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.opt-btn { padding: 8px 14px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 7px; cursor: pointer; font-size: 12px; }
|
||||
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
|
||||
.opt-btn { padding: 8px 14px; color: #f5f8fc; background: #409eff; border: none; border-radius: 7px; cursor: pointer; font-size: 12px; }
|
||||
.opt-btn:hover:not(:disabled) { background: #66b1ff; }
|
||||
.opt-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.selected-files { margin-top: 10px; color: #a0acbe; font-size: 12px; word-break: break-all; }
|
||||
.selected-files span { display: block; margin: 3px 0; }
|
||||
.country-group { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.country-chip { display: inline-flex; align-items: center; gap: 7px; padding: 9px 11px; border: 1px solid #333; border-radius: 8px; background: #2e3a52; color: #c8d2e2; font-size: 12px; cursor: pointer; user-select: none; }
|
||||
.country-chip.selected { border-color: #409eff; background: rgba(64, 158, 255, 0.12); color: #e6f2ff; }
|
||||
.country-chip input { width: 15px; height: 15px; accent-color: #409eff; cursor: pointer; }
|
||||
.country-chip { display: inline-flex; align-items: center; gap: 7px; padding: 9px 11px; border: 1px solid #3e4a62; border-radius: 8px; background: #242424; color: #f5f8fc; font-size: 12px; cursor: pointer; user-select: none; }
|
||||
.country-chip.selected { border-color: #409eff; background: #409eff; color: #ffffff; }
|
||||
.country-chip input { display: none; }
|
||||
.country-hint { margin: 10px 0 4px; }
|
||||
.country-hint.error { color: #e6a23c; }
|
||||
.country-hint b { color: #c8d2e2; }
|
||||
.bridge-tip { margin: 12px 0; padding: 9px 12px; border-radius: 8px; background: rgba(230, 162, 60, 0.1); color: #e6a23c; font-size: 11px; line-height: 1.6; }
|
||||
.btn-run { padding: 10px 18px; color: #f5f8fc; background: #27ae60; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
|
||||
.btn-run { padding: 10px 18px; color: #ffffff; background: #409eff; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; width: 100%; }
|
||||
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.loading-msg { margin-top: 10px; }
|
||||
.panel-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 20px 0; }
|
||||
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
||||
.toolbar-pagination { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.act-btn { padding: 4px 9px; border-radius: 6px; font-size: 12px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; }
|
||||
.act-btn.ok { background: rgba(52, 152, 219, 0.18); color: #69b6ff; border-color: transparent; }
|
||||
.act-btn.warn { background: rgba(230, 162, 60, 0.14); color: #e6a23c; border-color: transparent; }
|
||||
.page-info { color: #5e6878; font-size: 12px; }
|
||||
.page-btns { display: flex; gap: 8px; }
|
||||
.page-btn { padding: 5px 12px; border-radius: 6px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; font-size: 12px; }
|
||||
.queue-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 20px 0; }
|
||||
.queue-title { font-size: 15px; font-weight: 700; color: #f5f8fc; }
|
||||
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #242424; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
||||
.queue-body { flex: 1; min-height: 0; overflow: auto; padding: 16px 20px; }
|
||||
.queue-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.task-card { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; background: #232a3b; border: 1px solid #333f55; border-radius: 10px; }
|
||||
.task-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.task-card-id { color: #f5f8fc; font-size: 14px; font-weight: 700; word-break: break-all; }
|
||||
.task-card-meta { display: flex; flex-direction: column; gap: 2px; }
|
||||
.task-card-meta .files { color: #a0acbe; font-size: 12px; line-height: 1.5; }
|
||||
.task-card-msg { padding: 8px 10px; background: #2b3447; border-radius: 6px; color: #c8d2e2; font-size: 12px; line-height: 1.5; min-height: 20px; }
|
||||
.task-card-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; }
|
||||
.act-btn { padding: 5px 12px; border: none; border-radius: 6px; font-size: 12px; cursor: pointer; background: #242424; color: #c8d2e2; }
|
||||
.act-btn:hover { filter: brightness(1.2); }
|
||||
.act-btn.ok { background: #2496e0; color: #ffffff; }
|
||||
.act-btn.warn { background: #f59e0b; color: #ffffff; }
|
||||
.act-btn.danger { background: rgba(231, 76, 60, 0.16); color: #ff6b6b; }
|
||||
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
||||
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.status.pending { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
|
||||
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; }
|
||||
.page-info { color: #a0acbe; font-size: 12px; }
|
||||
.page-btns { display: flex; align-items: center; gap: 8px; }
|
||||
.page-btn { padding: 5px 12px; border-radius: 6px; background: #242424; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; font-size: 12px; }
|
||||
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.page-now { min-width: 22px; text-align: center; color: #c8d2e2; font-size: 12px; }
|
||||
.empty-tasks { color: #5e6878; font-size: 13px; padding: 12px 0; text-align: center; }
|
||||
.detail-body { max-height: 60vh; overflow: auto; }
|
||||
.kv-row { display: flex; gap: 12px; padding: 6px 0; font-size: 13px; }
|
||||
.kv-key { flex-shrink: 0; width: 110px; color: #5e6878; }
|
||||
|
||||
Reference in New Issue
Block a user