最新修改
This commit is contained in:
@@ -1,131 +1,137 @@
|
||||
<template>
|
||||
<div class="client-view-container">
|
||||
<header class="client-header">
|
||||
<div class="brand">
|
||||
<div class="logo-box">
|
||||
<el-icon>
|
||||
<DataLine />
|
||||
</el-icon>
|
||||
<div class="cv-wrap">
|
||||
<!-- Header -->
|
||||
<header class="cv-header">
|
||||
<div class="cv-brand">
|
||||
<div class="cv-logo">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<span class="title">{{ currentUser.company_name || "数据交付监控台" }}</span>
|
||||
<span class="version">Real-time Monitor v2.0</span>
|
||||
<div>
|
||||
<div class="cv-title">方舟星河</div>
|
||||
<div class="cv-subtitle">采集数据平台 · Monitor v2.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-space :size="20">
|
||||
<div class="user-badge">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>{{ currentUser.username }}</span>
|
||||
</div>
|
||||
<el-button type="danger" plain size="small" @click="handleLogout" :icon="SwitchButton">退出</el-button>
|
||||
</el-space>
|
||||
<div class="cv-user">
|
||||
<div class="cv-avatar">{{ currentUser.username?.charAt(0) || 'U' }}</div>
|
||||
<span class="cv-username">{{ currentUser.username }}</span>
|
||||
<el-button class="cv-logout-btn" @click="handleLogout" :icon="SwitchButton" size="small">退出</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="client-stats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="stat in summaryStats" :key="stat.label">
|
||||
<div class="stat-card-premium" :class="{ 'is-active': activeStatus === stat.status }"
|
||||
:style="{ '--theme-color': stat.color }" @click="handleStatFilter(stat.status)">
|
||||
<div class="stat-info">
|
||||
<span class="s-label">{{ stat.label }}</span>
|
||||
<span class="s-value">{{ stat.value }}</span>
|
||||
</div>
|
||||
<div class="stat-icon-wrapper">
|
||||
<el-icon>
|
||||
<component :is="stat.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="activeStatus === stat.status" class="active-dot"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- Stats -->
|
||||
<section class="cv-stats">
|
||||
<div
|
||||
v-for="stat in summaryStats"
|
||||
:key="stat.label"
|
||||
class="cv-stat-card"
|
||||
:class="{ 'is-active': activeStatus === stat.status }"
|
||||
:style="{ '--c': stat.color }"
|
||||
@click="handleStatFilter(stat.status)"
|
||||
>
|
||||
<div class="cv-stat-icon">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="cv-stat-body">
|
||||
<div class="cv-stat-val">{{ stat.value }}</div>
|
||||
<div class="cv-stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
<div class="cv-stat-bar"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="data-section">
|
||||
<div class="section-header">
|
||||
<div class="header-left">
|
||||
<span class="indicator"></span>
|
||||
<span class="section-title">实时交付看板</span>
|
||||
<el-tag v-if="activeStatus" closable size="small" type="info" class="filter-tag" @close="activeStatus = null">
|
||||
当前筛选: {{ activeStatus }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" link @click="loadData" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon> 同步最新数据
|
||||
</el-button>
|
||||
<!-- Table Panel -->
|
||||
<main class="cv-panel">
|
||||
<div class="cv-panel-header">
|
||||
<div class="cv-panel-title">
|
||||
<span class="cv-title-dot"></span>
|
||||
实时交付看板
|
||||
<el-tag
|
||||
v-if="activeStatus"
|
||||
closable
|
||||
size="small"
|
||||
class="cv-filter-tag"
|
||||
@close="activeStatus = null"
|
||||
>{{ activeStatus }}</el-tag>
|
||||
</div>
|
||||
<el-button type="primary" link :loading="loading" @click="loadData" :icon="Refresh" size="small">
|
||||
同步数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
|
||||
<el-table-column prop="id" label="单号" width="90" align="center">
|
||||
<el-table
|
||||
:data="paginatedTickets"
|
||||
v-loading="loading"
|
||||
class="cv-table"
|
||||
:header-cell-style="{ background: 'var(--cv-th-bg)', color: 'var(--text-muted)', fontWeight: '500', fontSize: '12px' }"
|
||||
:row-style="{ height: '52px' }"
|
||||
>
|
||||
<el-table-column label="单号" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="id-tag">#{{ row.id }}</span>
|
||||
<span class="cv-id">#{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="website" label="网站地址" min-width="260">
|
||||
<el-table-column label="网站地址" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="project-cell">
|
||||
<el-icon class="project-icon">
|
||||
<Monitor />
|
||||
</el-icon>
|
||||
<span class="project-name" :title="row.website">{{ row.website }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交付进度" width="260">
|
||||
<template #default="scope">
|
||||
<div class="progress-box">
|
||||
<div class="progress-labels">
|
||||
<span class="percent-val">{{ scope.row.progress || 0 }}%</span>
|
||||
<div class="cv-site-cell">
|
||||
<div class="cv-site-favicon">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</div>
|
||||
<el-progress :percentage="Number(scope.row.progress || 0)"
|
||||
:status="Number(scope.row.progress) === 100 ? 'success' : ''" :stroke-width="10" :show-text="false"
|
||||
striped striped-flow />
|
||||
<span class="cv-site-name" :title="row.website">{{ row.website }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="datanum" label="总数据量" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<div class="data-val-box">
|
||||
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span>
|
||||
<el-table-column label="交付进度" width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cv-progress-cell">
|
||||
<el-progress
|
||||
:percentage="Number(row.progress || 0)"
|
||||
:status="Number(row.progress) === 100 ? 'success' : ''"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
striped
|
||||
striped-flow
|
||||
/>
|
||||
<span class="cv-progress-pct" :class="{ 'is-done': Number(row.progress) === 100 }">
|
||||
{{ row.progress || 0 }}%
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交付状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]">
|
||||
<span class="dot"></span>
|
||||
<span class="text">{{ scope.row.is_complete }}</span>
|
||||
</div>
|
||||
<el-table-column label="数据量" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="cv-datanum">{{ formatNumber(row.datanum) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新日期" width="140" align="center">
|
||||
<template #default="scope">
|
||||
<span class="date-text">{{
|
||||
formatDate(
|
||||
scope.row.lock_time || scope.row.update_time || scope.row.create_time
|
||||
)
|
||||
}}</span>
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['cv-status-badge', getStatusClass(row.status)]">
|
||||
<i class="cv-status-dot"></i>{{ statusLabel(row.status) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新时间" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="cv-date">{{ formatDate(row.lock_time || row.update_time || row.create_time) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<footer class="pagination-footer">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="limit" :total="total"
|
||||
layout="total, prev, pager, next" background @current-change="handlePageChange" />
|
||||
</footer>
|
||||
<div class="cv-footer">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="limit"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
small
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -133,17 +139,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { resetRouter } from "@/router";
|
||||
import {
|
||||
DataLine,
|
||||
SwitchButton,
|
||||
User,
|
||||
Refresh,
|
||||
Files,
|
||||
Timer,
|
||||
CircleCheck,
|
||||
Monitor,
|
||||
DataLine, SwitchButton, User, Refresh,
|
||||
Files, Timer, CircleCheck, Monitor,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ticketApi, userApi } from "@/api/ticket";
|
||||
import { clientApi, userApi } from "@/api/ticket";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
@@ -152,387 +153,217 @@ const loading = ref(false);
|
||||
const tickets = ref([]);
|
||||
const page = ref(1);
|
||||
const limit = ref(15);
|
||||
const activeStatus = ref(null);
|
||||
|
||||
// --- 筛选状态控制 ---
|
||||
const activeStatus = ref(null); // null 代表显示全部,其余为具体状态字符串
|
||||
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
|
||||
const statusLabel = (s) => statusMap[Number(s)] || "未知";
|
||||
|
||||
// --- 1. 用户信息 ---
|
||||
const currentUser = computed(() => {
|
||||
const info = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
|
||||
return info ? JSON.parse(info) : { username: "外部客户", customer_id: null };
|
||||
return info ? JSON.parse(info) : { username: "访客" };
|
||||
});
|
||||
|
||||
// --- 2. 数据加载 ---
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const customerId = currentUser.value.customer_id;
|
||||
const res = await ticketApi.getTickets({
|
||||
page: 1,
|
||||
limit: 1000, // 客户侧通常数据量不大,取1000条前端分页
|
||||
customer_id: customerId,
|
||||
});
|
||||
|
||||
if (res.code === 200 || res.code === true) {
|
||||
tickets.value = res.data.data;
|
||||
}
|
||||
} catch (e) {
|
||||
const res = await clientApi.getTickets();
|
||||
if (res.code === 200) tickets.value = res.data || [];
|
||||
} catch {
|
||||
ElMessage.error("数据同步失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 3. 核心:双重过滤逻辑 (权限过滤 + 卡片点击过滤) ---
|
||||
const baseTickets = computed(() => tickets.value.filter((t) => Number(t.status) !== 3));
|
||||
const filteredTickets = computed(() => {
|
||||
const allowed = ["未分配", "进行中", "已完成"];
|
||||
// 第一步:过滤掉“已完结”等不需展示给客户的状态
|
||||
let result = tickets.value.filter((t) => allowed.includes(t.is_complete));
|
||||
|
||||
// 第二步:根据卡片点击的状态进行筛选
|
||||
if (activeStatus.value) {
|
||||
result = result.filter((t) => t.is_complete === activeStatus.value);
|
||||
}
|
||||
|
||||
return result;
|
||||
if (!activeStatus.value) return baseTickets.value;
|
||||
return baseTickets.value.filter((t) => statusLabel(t.status) === activeStatus.value);
|
||||
});
|
||||
|
||||
const paginatedTickets = computed(() => {
|
||||
const start = (page.value - 1) * limit.value;
|
||||
return filteredTickets.value.slice(start, start + limit.value);
|
||||
});
|
||||
|
||||
const total = computed(() => filteredTickets.value.length);
|
||||
|
||||
// --- 4. 统计卡片数据定义 ---
|
||||
const summaryStats = computed(() => {
|
||||
// 基础权限过滤后的原始数组
|
||||
const baseData = tickets.value.filter((t) =>
|
||||
["未分配", "进行中", "已完成"].includes(t.is_complete)
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
label: "项目总数",
|
||||
value: baseData.length,
|
||||
color: "#409EFF",
|
||||
icon: Monitor,
|
||||
status: null,
|
||||
},
|
||||
{
|
||||
label: "等待排期",
|
||||
value: baseData.filter((t) => t.is_complete === "未分配").length,
|
||||
color: "#A8ABB2",
|
||||
icon: Files,
|
||||
status: "未分配",
|
||||
},
|
||||
{
|
||||
label: "正在采集",
|
||||
value: baseData.filter((t) => t.is_complete === "进行中").length,
|
||||
color: "#E6A23C",
|
||||
icon: Timer,
|
||||
status: "进行中",
|
||||
},
|
||||
{
|
||||
label: "已就绪",
|
||||
value: baseData.filter((t) => t.is_complete === "已完成").length,
|
||||
color: "#67C23A",
|
||||
icon: CircleCheck,
|
||||
status: "已完成",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// --- 5. 交互方法 ---
|
||||
const handleStatFilter = (status) => {
|
||||
activeStatus.value = status;
|
||||
page.value = 1; // 切换筛选时重置到第一页
|
||||
};
|
||||
const summaryStats = computed(() => [
|
||||
{ label: "采集项目总数", value: baseTickets.value.length, color: "#6366f1", icon: Monitor, status: null },
|
||||
{ label: "等待排期", value: baseTickets.value.filter((t) => Number(t.status) === 0).length, color: "#94a3b8", icon: Files, status: "未分配" },
|
||||
{ label: "正在采集", value: baseTickets.value.filter((t) => Number(t.status) === 1).length, color: "#f59e0b", icon: Timer, status: "进行中" },
|
||||
{ label: "已就绪", value: baseTickets.value.filter((t) => Number(t.status) === 2).length, color: "#10b981", icon: CircleCheck, status: "已完成" },
|
||||
]);
|
||||
|
||||
const handleStatFilter = (status) => { activeStatus.value = status; page.value = 1; };
|
||||
const formatNumber = (val) => {
|
||||
if (!val || val === "0") return "0";
|
||||
const num = Number(val);
|
||||
return isNaN(num) ? val : num.toLocaleString();
|
||||
if (!val || val === "0") return "—";
|
||||
const n = Number(val);
|
||||
return isNaN(n) ? val : n.toLocaleString();
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const map = { 未分配: "is-waiting", 进行中: "is-running", 已完成: "is-success" };
|
||||
return map[status] || "";
|
||||
};
|
||||
|
||||
const handlePageChange = (p) => {
|
||||
page.value = p;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const getStatusClass = (s) => ({ 0: "s-wait", 1: "s-run", 2: "s-done" })[Number(s)] || "";
|
||||
const handlePageChange = (p) => { page.value = p; window.scrollTo({ top: 0, behavior: "smooth" }); };
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await userApi.logout();
|
||||
} catch (e) {}
|
||||
|
||||
localStorage.removeItem("user_info");
|
||||
sessionStorage.removeItem("user_info");
|
||||
router.push("/login");
|
||||
try { await userApi.logout(); } catch {}
|
||||
resetRouter();
|
||||
router.push({ name: "login" });
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.client-view-container {
|
||||
padding: 24px 40px;
|
||||
background-color: #f6f8fb;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
/* ── tokens ── */
|
||||
.cv-wrap {
|
||||
--cv-th-bg: #f8f9fb;
|
||||
--cv-radius: 14px;
|
||||
padding: 8px 4px 24px;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.client-header {
|
||||
/* ── Header ── */
|
||||
.cv-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.cv-brand { display: flex; align-items: center; gap: 14px; }
|
||||
.cv-logo {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #6366f1, #818cf8);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-size: 20px;
|
||||
box-shadow: 0 4px 14px rgba(99,102,241,.32);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cv-title { font-size: 17px; font-weight: 700; color: var(--text-primary); line-height: 1.3; letter-spacing: -0.2px; }
|
||||
.cv-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 2px; letter-spacing: 0.2px; }
|
||||
|
||||
.cv-user { display: flex; align-items: center; gap: 12px; }
|
||||
.cv-avatar {
|
||||
width: 32px; height: 32px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6366f1, #a78bfa);
|
||||
color: #fff; font-size: 13px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(99,102,241,.25);
|
||||
}
|
||||
.cv-username { font-size: 13px; color: var(--text-secondary); }
|
||||
.cv-logout-btn { border-radius: 8px !important; }
|
||||
|
||||
/* ── Stats ── */
|
||||
.cv-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
.cv-stat-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--cv-radius);
|
||||
padding: 20px 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
background: linear-gradient(135deg, #409eff, #007aff);
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 10px;
|
||||
color: #a1a1a6;
|
||||
margin-left: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #fff;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 统计卡片交互样式 */
|
||||
.stat-card-premium {
|
||||
background: #fff;
|
||||
padding: 22px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
|
||||
border-left: 4px solid var(--theme-color);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s;
|
||||
}
|
||||
|
||||
.stat-card-premium:hover {
|
||||
.cv-stat-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.stat-card-premium.is-active {
|
||||
background: #f0f7ff;
|
||||
border-left-width: 8px;
|
||||
box-shadow: inset 0 0 0 1px rgba(64, 158, 255, 0.1), 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
.cv-stat-card.is-active {
|
||||
border-color: var(--c);
|
||||
background: color-mix(in srgb, var(--c) 5%, #fff);
|
||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--c) 18%, transparent);
|
||||
}
|
||||
|
||||
.active-dot {
|
||||
.cv-stat-bar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--theme-color);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--theme-color);
|
||||
animation: pulse 2s infinite;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
background: var(--c);
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s;
|
||||
border-radius: 0 0 var(--cv-radius) var(--cv-radius);
|
||||
}
|
||||
.cv-stat-card.is-active .cv-stat-bar,
|
||||
.cv-stat-card:hover .cv-stat-bar { opacity: 1; }
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
.cv-stat-icon {
|
||||
width: 46px; height: 46px; border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||
color: var(--c);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cv-stat-val { font-size: 28px; font-weight: 700; color: var(--text-primary); line-height: 1.1; letter-spacing: -0.5px; }
|
||||
.cv-stat-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; letter-spacing: 0.1px; }
|
||||
|
||||
.s-label {
|
||||
font-size: 13px;
|
||||
color: #86868b;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
/* ── Panel ── */
|
||||
.cv-panel {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--cv-radius);
|
||||
padding: 24px 28px 20px;
|
||||
}
|
||||
|
||||
.s-value {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
font-size: 36px;
|
||||
color: var(--theme-color);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.data-section {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
.cv-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.cv-panel-title {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 14px; font-weight: 600; color: var(--text-primary);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: #409eff;
|
||||
.cv-title-dot {
|
||||
width: 3px; height: 16px;
|
||||
background: linear-gradient(180deg, #6366f1, #a78bfa);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.cv-filter-tag { border-radius: 6px; }
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
/* ── Table cells ── */
|
||||
.cv-table { width: 100%; }
|
||||
.cv-id { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
.cv-site-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.cv-site-favicon {
|
||||
width: 28px; height: 28px; border-radius: 7px;
|
||||
background: var(--cv-th-bg);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text-muted); font-size: 13px; flex-shrink: 0;
|
||||
}
|
||||
.cv-site-name {
|
||||
font-size: 13px; font-weight: 500; color: var(--text-primary);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
margin-left: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cv-progress-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.cv-progress-cell .el-progress { flex: 1; }
|
||||
.cv-progress-pct { font-size: 11px; font-weight: 600; color: var(--text-muted); width: 32px; text-align: right; flex-shrink: 0; }
|
||||
.cv-progress-pct.is-done { color: #10b981; }
|
||||
|
||||
.id-tag {
|
||||
color: #909399;
|
||||
font-family: "Consolas", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cv-datanum { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--text-primary); }
|
||||
.cv-date { font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
.project-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* ── Status badge ── */
|
||||
.cv-status-badge {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 4px 11px; border-radius: 20px;
|
||||
font-size: 11px; font-weight: 500;
|
||||
}
|
||||
.cv-status-dot {
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
background: currentColor; display: inline-block;
|
||||
}
|
||||
.s-wait { background: #f1f5f9; color: #94a3b8; }
|
||||
.s-run { background: #fffbeb; color: #f59e0b; }
|
||||
.s-done { background: #ecfdf5; color: #10b981; }
|
||||
|
||||
.project-icon {
|
||||
color: #c0c4cc;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.percent-val {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.data-num {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.status-dot-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.is-waiting {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.is-waiting .dot {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
.is-running {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.is-running .dot {
|
||||
background: #e6a23c;
|
||||
box-shadow: 0 0 6px rgba(230, 162, 60, 0.4);
|
||||
}
|
||||
|
||||
.is-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.is-success .dot {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
.pagination-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: #f8f9fb;
|
||||
}
|
||||
/* ── Footer ── */
|
||||
.cv-footer { margin-top: 24px; padding-bottom: 4px; display: flex; justify-content: center; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user