|
@@ -2,7 +2,86 @@
|
|
|
<div class="store-page">
|
|
<div class="store-page">
|
|
|
<Breadcrumb />
|
|
<Breadcrumb />
|
|
|
<a-card title="抢课任务监控">
|
|
<a-card title="抢课任务监控">
|
|
|
- <AppQueryFilter>
|
|
|
|
|
|
|
+ <div class="stats-section">
|
|
|
|
|
+ <a-row :gutter="[16, 16]" class="stats-row" align="stretch">
|
|
|
|
|
+ <a-col :xs="24" :md="10" :lg="8" class="stats-col">
|
|
|
|
|
+ <div class="stats-panel stats-highlight">
|
|
|
|
|
+ <a-progress
|
|
|
|
|
+ type="circle"
|
|
|
|
|
+ :percent="summary.activeRate / 100"
|
|
|
|
|
+ :stroke-width="8"
|
|
|
|
|
+ :width="isMobile ? 96 : 108"
|
|
|
|
|
+ :color="activeRateColor"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #text>
|
|
|
|
|
+ <div class="progress-text">
|
|
|
|
|
+ <div class="progress-value">{{ summary.activeRate }}%</div>
|
|
|
|
|
+ <div class="progress-label">活跃占比</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-progress>
|
|
|
|
|
+ <div class="stats-highlight-meta">
|
|
|
|
|
+ <div class="highlight-title">任务执行概况</div>
|
|
|
|
|
+ <div class="highlight-desc">
|
|
|
|
|
+ 抢课中 <strong>{{ summary.grabbing }}</strong> · 待分配 <strong>{{ summary.pending }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="highlight-desc">
|
|
|
|
|
+ 任务总数 <strong>{{ summary.total }}</strong> · 未开始 <strong>{{ summary.paused }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="24" :md="14" :lg="16" class="stats-col">
|
|
|
|
|
+ <div class="stats-panel stats-grid-panel">
|
|
|
|
|
+ <a-row :gutter="[12, 12]" class="stats-grid">
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="任务总数" :value="summary.total" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="抢课中" :value="summary.grabbing" value-style="color: rgb(var(--orange-6))" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="待分配" :value="summary.pending" value-style="color: rgb(var(--arcoblue-6))" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="未开始" :value="summary.paused" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="成功" :value="summary.success" value-style="color: rgb(var(--green-6))" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="失败" :value="summary.failed" value-style="color: rgb(var(--red-6))" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="已取消" :value="summary.cancelled" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :xs="12" :sm="8" :md="6">
|
|
|
|
|
+ <div class="stat-card">
|
|
|
|
|
+ <a-statistic title="剩余槽位" :value="summary.freeSlots" value-style="color: rgb(var(--green-6))" />
|
|
|
|
|
+ <div class="stat-sub">占用 {{ summary.usedSlots }} / {{ summary.totalSlots }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ </a-row>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ </a-row>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <AppQueryFilter class="query-filter">
|
|
|
<a-row class="query-row">
|
|
<a-row class="query-row">
|
|
|
<a-col :flex="'1000px'" class="query-main">
|
|
<a-col :flex="'1000px'" class="query-main">
|
|
|
<a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
|
|
<a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
|
|
@@ -55,12 +134,13 @@
|
|
|
</AppQueryFilter>
|
|
</AppQueryFilter>
|
|
|
|
|
|
|
|
<a-table
|
|
<a-table
|
|
|
|
|
+ row-key="id"
|
|
|
:data="data"
|
|
:data="data"
|
|
|
:bordered="false"
|
|
:bordered="false"
|
|
|
hoverable
|
|
hoverable
|
|
|
class="table"
|
|
class="table"
|
|
|
:loading="loading"
|
|
:loading="loading"
|
|
|
- :scroll="{ x: 1880 }"
|
|
|
|
|
|
|
+ :scroll="{ x: isMobile ? 1200 : 1880 }"
|
|
|
:pagination="{
|
|
:pagination="{
|
|
|
showPageSize: true,
|
|
showPageSize: true,
|
|
|
showJumper: true,
|
|
showJumper: true,
|
|
@@ -93,9 +173,15 @@
|
|
|
</template>
|
|
</template>
|
|
|
</a-table-column>
|
|
</a-table-column>
|
|
|
<a-table-column title="课程" :width="260">
|
|
<a-table-column title="课程" :width="260">
|
|
|
- <template #cell="{ record }">{{ [...(record.courses || []), ...(record.course_groups || [])].join('、') || '-' }}</template>
|
|
|
|
|
|
|
+ <template #cell="{ record }">
|
|
|
|
|
+ <AppTableEllipsis>{{ formatCourseList(record) }}</AppTableEllipsis>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table-column>
|
|
|
|
|
+ <a-table-column title="错误" :width="220">
|
|
|
|
|
+ <template #cell="{ record }">
|
|
|
|
|
+ <AppTableEllipsis>{{ record.error_msg || '-' }}</AppTableEllipsis>
|
|
|
|
|
+ </template>
|
|
|
</a-table-column>
|
|
</a-table-column>
|
|
|
- <a-table-column title="错误" data-index="error_msg" :width="220" ellipsis tooltip />
|
|
|
|
|
<a-table-column title="备注" :width="140" ellipsis tooltip>
|
|
<a-table-column title="备注" :width="140" ellipsis tooltip>
|
|
|
<template #cell="{ record }">{{ record.name || '-' }}</template>
|
|
<template #cell="{ record }">{{ record.name || '-' }}</template>
|
|
|
</a-table-column>
|
|
</a-table-column>
|
|
@@ -110,6 +196,7 @@
|
|
|
<a-space wrap>
|
|
<a-space wrap>
|
|
|
<a-button
|
|
<a-button
|
|
|
v-if="canStart(record)"
|
|
v-if="canStart(record)"
|
|
|
|
|
+ :key="`${record.id}-start`"
|
|
|
type="text"
|
|
type="text"
|
|
|
size="small"
|
|
size="small"
|
|
|
status="success"
|
|
status="success"
|
|
@@ -119,6 +206,7 @@
|
|
|
</a-button>
|
|
</a-button>
|
|
|
<a-button
|
|
<a-button
|
|
|
v-if="canPause(record)"
|
|
v-if="canPause(record)"
|
|
|
|
|
+ :key="`${record.id}-pause`"
|
|
|
type="text"
|
|
type="text"
|
|
|
size="small"
|
|
size="small"
|
|
|
status="warning"
|
|
status="warning"
|
|
@@ -126,12 +214,30 @@
|
|
|
>
|
|
>
|
|
|
暂停
|
|
暂停
|
|
|
</a-button>
|
|
</a-button>
|
|
|
- <a-button type="text" size="small" :disabled="!canEdit(record)" @click="openModal(record)">编辑</a-button>
|
|
|
|
|
- <a-popconfirm content="确认重试该任务?将重新进入待分配队列。" @ok="retryTask(record)">
|
|
|
|
|
- <a-button type="text" size="small" :disabled="!canRetry(record)">重试</a-button>
|
|
|
|
|
|
|
+ <a-button
|
|
|
|
|
+ v-if="canEdit(record)"
|
|
|
|
|
+ :key="`${record.id}-edit`"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @click="openModal(record)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 编辑
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ <a-popconfirm
|
|
|
|
|
+ v-if="canRetry(record)"
|
|
|
|
|
+ :key="`${record.id}-retry`"
|
|
|
|
|
+ content="确认重试该任务?将重新进入待分配队列。"
|
|
|
|
|
+ @ok="retryTask(record)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <a-button type="text" size="small">重试</a-button>
|
|
|
</a-popconfirm>
|
|
</a-popconfirm>
|
|
|
- <a-popconfirm :content="cancelConfirmText(record)" @ok="cancelTask(record)">
|
|
|
|
|
- <a-button type="text" status="danger" size="small" :disabled="!canCancel(record)">取消</a-button>
|
|
|
|
|
|
|
+ <a-popconfirm
|
|
|
|
|
+ v-if="canCancel(record)"
|
|
|
|
|
+ :key="`${record.id}-cancel`"
|
|
|
|
|
+ :content="cancelConfirmText(record)"
|
|
|
|
|
+ @ok="cancelTask(record)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <a-button type="text" status="danger" size="small">取消</a-button>
|
|
|
</a-popconfirm>
|
|
</a-popconfirm>
|
|
|
</a-space>
|
|
</a-space>
|
|
|
</template>
|
|
</template>
|
|
@@ -221,6 +327,7 @@
|
|
|
import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
|
|
import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
|
|
|
import { Notification } from '@arco-design/web-vue'
|
|
import { Notification } from '@arco-design/web-vue'
|
|
|
import {
|
|
import {
|
|
|
|
|
+ adminQkClientList,
|
|
|
adminQkTaskList,
|
|
adminQkTaskList,
|
|
|
adminQkTaskDetail,
|
|
adminQkTaskDetail,
|
|
|
adminQkTaskUpdate,
|
|
adminQkTaskUpdate,
|
|
@@ -260,6 +367,47 @@ const pagination = reactive({
|
|
|
current: 1,
|
|
current: 1,
|
|
|
pagesize: 20
|
|
pagesize: 20
|
|
|
})
|
|
})
|
|
|
|
|
+const statusStats = reactive({
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ paused: 0,
|
|
|
|
|
+ pending: 0,
|
|
|
|
|
+ assigned: 0,
|
|
|
|
|
+ running: 0,
|
|
|
|
|
+ success: 0,
|
|
|
|
|
+ failed: 0,
|
|
|
|
|
+ cancelled: 0,
|
|
|
|
|
+ usedSlots: 0,
|
|
|
|
|
+ totalSlots: 0,
|
|
|
|
|
+ freeSlots: 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const summary = computed(() => {
|
|
|
|
|
+ const grabbing = statusStats.running + statusStats.assigned
|
|
|
|
|
+ const active = grabbing + statusStats.pending
|
|
|
|
|
+ const total = statusStats.total || 0
|
|
|
|
|
+ const activeRate = total > 0 ? Math.min(100, Math.round((active / total) * 100)) : 0
|
|
|
|
|
+ return {
|
|
|
|
|
+ total,
|
|
|
|
|
+ paused: statusStats.paused,
|
|
|
|
|
+ pending: statusStats.pending,
|
|
|
|
|
+ grabbing,
|
|
|
|
|
+ success: statusStats.success,
|
|
|
|
|
+ failed: statusStats.failed,
|
|
|
|
|
+ cancelled: statusStats.cancelled,
|
|
|
|
|
+ activeRate,
|
|
|
|
|
+ usedSlots: statusStats.usedSlots,
|
|
|
|
|
+ totalSlots: statusStats.totalSlots,
|
|
|
|
|
+ freeSlots: statusStats.freeSlots
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const activeRateColor = computed(() => {
|
|
|
|
|
+ const rate = summary.value.activeRate
|
|
|
|
|
+ if (rate >= 80) return '#F53F3F'
|
|
|
|
|
+ if (rate >= 50) return '#FF7D00'
|
|
|
|
|
+ return '#165DFF'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const form = reactive({
|
|
const form = reactive({
|
|
|
id: '',
|
|
id: '',
|
|
|
name: '',
|
|
name: '',
|
|
@@ -281,6 +429,11 @@ const targetHint = computed(() => getQkTargetHint(form.coursesText, form.courseG
|
|
|
|
|
|
|
|
const splitLines = splitQkLines
|
|
const splitLines = splitQkLines
|
|
|
|
|
|
|
|
|
|
+const formatCourseList = (record) => {
|
|
|
|
|
+ const list = [...(record.courses || []), ...(record.course_groups || [])]
|
|
|
|
|
+ return list.length ? list.join('、') : '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const formatTime = (time) => {
|
|
const formatTime = (time) => {
|
|
|
if (!time) return '-'
|
|
if (!time) return '-'
|
|
|
return new Date(Number(time)).toLocaleString('zh-CN', {
|
|
return new Date(Number(time)).toLocaleString('zh-CN', {
|
|
@@ -393,6 +546,48 @@ const getList = async (silent = false) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const fetchStats = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [
|
|
|
|
|
+ allRes,
|
|
|
|
|
+ pausedRes,
|
|
|
|
|
+ pendingRes,
|
|
|
|
|
+ assignedRes,
|
|
|
|
|
+ runningRes,
|
|
|
|
|
+ successRes,
|
|
|
|
|
+ failedRes,
|
|
|
|
|
+ cancelledRes,
|
|
|
|
|
+ clientRes
|
|
|
|
|
+ ] = await Promise.all([
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1 }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'paused' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'pending' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'assigned' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'running' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'success' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'failed' }),
|
|
|
|
|
+ adminQkTaskList({ current: 1, pagesize: 1, status: 'cancelled' }),
|
|
|
|
|
+ adminQkClientList()
|
|
|
|
|
+ ])
|
|
|
|
|
+ if (allRes?.code === 0) statusStats.total = allRes.data?.total || 0
|
|
|
|
|
+ if (pausedRes?.code === 0) statusStats.paused = pausedRes.data?.total || 0
|
|
|
|
|
+ if (pendingRes?.code === 0) statusStats.pending = pendingRes.data?.total || 0
|
|
|
|
|
+ if (assignedRes?.code === 0) statusStats.assigned = assignedRes.data?.total || 0
|
|
|
|
|
+ if (runningRes?.code === 0) statusStats.running = runningRes.data?.total || 0
|
|
|
|
|
+ if (successRes?.code === 0) statusStats.success = successRes.data?.total || 0
|
|
|
|
|
+ if (failedRes?.code === 0) statusStats.failed = failedRes.data?.total || 0
|
|
|
|
|
+ if (cancelledRes?.code === 0) statusStats.cancelled = cancelledRes.data?.total || 0
|
|
|
|
|
+
|
|
|
|
|
+ const clients = clientRes?.code === 0 ? (clientRes.data || []) : []
|
|
|
|
|
+ const online = clients.filter(item => Number(item.enabled) === 1 && Number(item.online) === 1)
|
|
|
|
|
+ const totalSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.max_slots) || 0), 0)
|
|
|
|
|
+ const usedSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.current_slots) || 0), 0)
|
|
|
|
|
+ statusStats.totalSlots = totalSlots
|
|
|
|
|
+ statusStats.usedSlots = usedSlots
|
|
|
|
|
+ statusStats.freeSlots = Math.max(0, totalSlots - usedSlots)
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const submitTask = async () => {
|
|
const submitTask = async () => {
|
|
|
const validated = validateQkTaskTargets(form.coursesText, form.courseGroupsText, form.interval_ms)
|
|
const validated = validateQkTaskTargets(form.coursesText, form.courseGroupsText, form.interval_ms)
|
|
|
if (!validated.ok) {
|
|
if (!validated.ok) {
|
|
@@ -418,6 +613,7 @@ const submitTask = async () => {
|
|
|
Notification.success({ title: '保存成功' })
|
|
Notification.success({ title: '保存成功' })
|
|
|
visible.value = false
|
|
visible.value = false
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
Notification.error({ title: '保存失败', content: error.message || '请稍后再试' })
|
|
Notification.error({ title: '保存失败', content: error.message || '请稍后再试' })
|
|
|
} finally {
|
|
} finally {
|
|
@@ -432,6 +628,7 @@ const cancelTask = async (record) => {
|
|
|
}
|
|
}
|
|
|
Notification.success({ title: '已取消' })
|
|
Notification.success({ title: '已取消' })
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const startTask = async (record) => {
|
|
const startTask = async (record) => {
|
|
@@ -441,6 +638,7 @@ const startTask = async (record) => {
|
|
|
}
|
|
}
|
|
|
Notification.success({ title: '已开启', content: '任务已进入待分配队列' })
|
|
Notification.success({ title: '已开启', content: '任务已进入待分配队列' })
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const pauseTask = async (record) => {
|
|
const pauseTask = async (record) => {
|
|
@@ -450,6 +648,7 @@ const pauseTask = async (record) => {
|
|
|
}
|
|
}
|
|
|
Notification.success({ title: '已暂停' })
|
|
Notification.success({ title: '已暂停' })
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const retryTask = async (record) => {
|
|
const retryTask = async (record) => {
|
|
@@ -459,6 +658,7 @@ const retryTask = async (record) => {
|
|
|
}
|
|
}
|
|
|
Notification.success({ title: '已重新加入待分配队列' })
|
|
Notification.success({ title: '已重新加入待分配队列' })
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const search = () => {
|
|
const search = () => {
|
|
@@ -486,11 +686,15 @@ const onPageSizeChange = (pageSize) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let pollTimer = null
|
|
let pollTimer = null
|
|
|
|
|
+let statsTimer = null
|
|
|
|
|
|
|
|
const startPolling = () => {
|
|
const startPolling = () => {
|
|
|
if (!pollTimer) {
|
|
if (!pollTimer) {
|
|
|
pollTimer = setInterval(() => getList(true), 5000)
|
|
pollTimer = setInterval(() => getList(true), 5000)
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!statsTimer) {
|
|
|
|
|
+ statsTimer = setInterval(fetchStats, 30000)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const stopPolling = () => {
|
|
const stopPolling = () => {
|
|
@@ -498,6 +702,10 @@ const stopPolling = () => {
|
|
|
clearInterval(pollTimer)
|
|
clearInterval(pollTimer)
|
|
|
pollTimer = null
|
|
pollTimer = null
|
|
|
}
|
|
}
|
|
|
|
|
+ if (statsTimer) {
|
|
|
|
|
+ clearInterval(statsTimer)
|
|
|
|
|
+ statsTimer = null
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const loadBatches = async () => {
|
|
const loadBatches = async () => {
|
|
@@ -512,6 +720,7 @@ const loadBatches = async () => {
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
loadBatches()
|
|
loadBatches()
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ fetchStats()
|
|
|
startPolling()
|
|
startPolling()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -523,8 +732,123 @@ onUnmounted(() => {
|
|
|
<style scoped lang="less">
|
|
<style scoped lang="less">
|
|
|
@import '@/styles/store-theme.less';
|
|
@import '@/styles/store-theme.less';
|
|
|
|
|
|
|
|
|
|
+.stats-section {
|
|
|
|
|
+ padding-bottom: 40px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-row {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.query-filter.app-query-filter) {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-col {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-panel {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 148px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-highlight {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border: 1px solid var(--color-border-2);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: linear-gradient(135deg, rgba(var(--primary-1), 0.35), rgba(var(--primary-1), 0.08));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-grid-panel {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid var(--color-border-2);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: var(--color-bg-1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-grid {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ align-content: stretch;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-text {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-value {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-label {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--color-text-3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-highlight-meta {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.highlight-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.highlight-desc {
|
|
|
|
|
+ color: var(--color-text-2);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+
|
|
|
|
|
+ strong {
|
|
|
|
|
+ color: var(--color-text-1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 72px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid var(--color-border-2);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ background: var(--color-bg-2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.stat-card .arco-statistic-title) {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.stat-card .arco-statistic-value) {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-sub {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--color-text-3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.query-row {
|
|
.query-row {
|
|
|
- margin-bottom: 10px;
|
|
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.query-main {
|
|
.query-main {
|
|
@@ -536,7 +860,7 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.table {
|
|
.table {
|
|
|
- margin-top: 16px;
|
|
|
|
|
|
|
+ margin-top: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.edit-alert {
|
|
.edit-alert {
|
|
@@ -544,6 +868,24 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
@media (max-width: 768px) {
|
|
|
|
|
+ .stats-panel {
|
|
|
|
|
+ min-height: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .stats-highlight {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .stats-highlight-meta {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .stat-card {
|
|
|
|
|
+ min-height: 64px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.query-row {
|
|
.query-row {
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|