针对于不同客户,展示对应的数据,以及增加通过卡片筛选数据
This commit is contained in:
@@ -1,293 +1,545 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="client-view-container">
|
<div class="client-view-container">
|
||||||
<header class="client-header">
|
<header class="client-header">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="logo-box">
|
<div class="logo-box">
|
||||||
<el-icon><DataLine /></el-icon>
|
<el-icon>
|
||||||
</div>
|
<DataLine />
|
||||||
<div class="brand-text">
|
</el-icon>
|
||||||
<span class="title">数据交付监控台</span>
|
|
||||||
<span class="version">Real-time Monitor v2.0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info">
|
<div class="brand-text">
|
||||||
<el-space :size="20">
|
<span class="title">{{ currentUser.company_name || "数据交付监控台" }}</span>
|
||||||
<div class="user-badge">
|
<span class="version">Real-time Monitor v2.0</span>
|
||||||
<el-icon><User /></el-icon>
|
</div>
|
||||||
<span>{{ currentUser.username }}</span>
|
</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>
|
||||||
|
</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>
|
||||||
<el-button type="danger" plain size="small" @click="handleLogout" :icon="SwitchButton">退出</el-button>
|
<div class="stat-icon-wrapper">
|
||||||
</el-space>
|
<el-icon>
|
||||||
</div>
|
<component :is="stat.icon" />
|
||||||
</header>
|
</el-icon>
|
||||||
|
|
||||||
<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" :style="{ '--theme-color': stat.color }">
|
|
||||||
<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>
|
</div>
|
||||||
</el-col>
|
<div v-if="activeStatus === stat.status" class="active-dot"></div>
|
||||||
</el-row>
|
</div>
|
||||||
</section>
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</section>
|
||||||
|
|
||||||
<main class="data-section">
|
<main class="data-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="indicator"></span>
|
<span class="indicator"></span>
|
||||||
<span class="section-title">实时交付看板</span>
|
<span class="section-title">实时交付看板</span>
|
||||||
</div>
|
<el-tag v-if="activeStatus" closable size="small" type="info" class="filter-tag" @close="activeStatus = null">
|
||||||
<div class="header-right">
|
当前筛选: {{ activeStatus }}
|
||||||
<el-button type="primary" link @click="loadData" :loading="loading">
|
</el-tag>
|
||||||
<el-icon><Refresh /></el-icon> 同步最新数据
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-button type="primary" link @click="loadData" :loading="loading">
|
||||||
|
<el-icon>
|
||||||
|
<Refresh />
|
||||||
|
</el-icon> 同步最新数据
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
|
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
|
||||||
<el-table-column prop="id" label="单号" width="90" align="center">
|
<el-table-column prop="id" label="单号" width="90" align="center">
|
||||||
<template #default="{row}">
|
<template #default="{ row }">
|
||||||
<span class="id-tag">#{{ row.id }}</span>
|
<span class="id-tag">#{{ row.id }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="website" label="网站地址" min-width="260">
|
<el-table-column prop="website" label="网站地址" min-width="260">
|
||||||
<template #default="{row}">
|
<template #default="{ row }">
|
||||||
<div class="project-cell">
|
<div class="project-cell">
|
||||||
<el-icon class="project-icon"><Monitor /></el-icon>
|
<el-icon class="project-icon">
|
||||||
<span class="project-name" :title="row.website">{{ row.website }}</span>
|
<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>
|
</div>
|
||||||
</template>
|
<el-progress :percentage="Number(scope.row.progress || 0)"
|
||||||
</el-table-column>
|
:status="Number(scope.row.progress) === 100 ? 'success' : ''" :stroke-width="10" :show-text="false"
|
||||||
|
striped striped-flow />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="交付进度" width="260">
|
<el-table-column prop="datanum" label="总数据量" width="160" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="progress-box">
|
<div class="data-val-box">
|
||||||
<div class="progress-labels">
|
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span>
|
||||||
<span class="percent-val">{{ scope.row.progress || 0 }}%</span>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<el-progress
|
</el-table-column>
|
||||||
:percentage="Number(scope.row.progress || 0)"
|
|
||||||
:status="Number(scope.row.progress) === 100 ? 'success' : ''"
|
|
||||||
:stroke-width="10"
|
|
||||||
:show-text="false"
|
|
||||||
striped
|
|
||||||
striped-flow
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column prop="datanum" label="总数据量" width="160" align="center">
|
<el-table-column label="交付状态" width="120" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="data-val-box">
|
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]">
|
||||||
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span>
|
<span class="dot"></span>
|
||||||
</div>
|
<span class="text">{{ scope.row.is_complete }}</span>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="交付状态" width="120" align="center">
|
<el-table-column label="更新日期" width="140" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]">
|
<span class="date-text">{{
|
||||||
<span class="dot"></span>
|
formatDateOnly(
|
||||||
<span class="text">{{ scope.row.is_complete }}</span>
|
scope.row.lock_time || scope.row.update_time || scope.row.create_time
|
||||||
</div>
|
)
|
||||||
</template>
|
}}</span>
|
||||||
</el-table-column>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
<el-table-column label="更新日期" width="140" align="center">
|
<footer class="pagination-footer">
|
||||||
<template #default="scope">
|
<el-pagination v-model:current-page="page" v-model:page-size="limit" :total="total"
|
||||||
<span class="date-text">{{ formatDateOnly(scope.row.lock_time || scope.row.update_time || scope.row.create_time) }}</span>
|
layout="total, prev, pager, next" background @current-change="handlePageChange" />
|
||||||
</template>
|
</footer>
|
||||||
</el-table-column>
|
</main>
|
||||||
</el-table>
|
</div>
|
||||||
|
|
||||||
<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>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from "vue-router";
|
||||||
import { DataLine, SwitchButton, User, Refresh, Files, Timer, CircleCheck, Monitor } from '@element-plus/icons-vue'
|
import {
|
||||||
import { ticketApi } from '@/api/ticket'
|
DataLine,
|
||||||
import { ElMessage } from 'element-plus'
|
SwitchButton,
|
||||||
|
User,
|
||||||
|
Refresh,
|
||||||
|
Files,
|
||||||
|
Timer,
|
||||||
|
CircleCheck,
|
||||||
|
Monitor,
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import { ticketApi } from "@/api/ticket";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
const tickets = ref([])
|
const tickets = ref([]);
|
||||||
const page = ref(1)
|
const page = ref(1);
|
||||||
const limit = ref(15)
|
const limit = ref(15);
|
||||||
|
|
||||||
const currentUser = computed(() => {
|
// --- 筛选状态控制 ---
|
||||||
const info = localStorage.getItem('user_info') || sessionStorage.getItem('user_info')
|
const activeStatus = ref(null); // null 代表显示全部,其余为具体状态字符串
|
||||||
return info ? JSON.parse(info) : { username: '外部客户' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredTickets = computed(() => {
|
// --- 1. 用户信息 ---
|
||||||
const allowed = ['未分配', '进行中', '已完成']
|
const currentUser = computed(() => {
|
||||||
return tickets.value.filter(t => allowed.includes(t.is_complete))
|
const info = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
|
||||||
})
|
return info ? JSON.parse(info) : { username: "外部客户", customer_id: null };
|
||||||
|
});
|
||||||
|
|
||||||
const paginatedTickets = computed(() => {
|
// --- 2. 数据加载 ---
|
||||||
const start = (page.value - 1) * limit.value
|
const loadData = async () => {
|
||||||
return filteredTickets.value.slice(start, start + limit.value)
|
loading.value = true;
|
||||||
})
|
try {
|
||||||
|
const customerId = currentUser.value.customer_id;
|
||||||
|
const res = await ticketApi.getTickets({
|
||||||
|
page: 1,
|
||||||
|
limit: 1000, // 客户侧通常数据量不大,取1000条前端分页
|
||||||
|
customer_id: customerId,
|
||||||
|
});
|
||||||
|
|
||||||
const total = computed(() => filteredTickets.value.length)
|
if (res.code === 200 || res.code === true) {
|
||||||
|
tickets.value = res.data.data;
|
||||||
const summaryStats = computed(() => [
|
|
||||||
{ label: '项目总数', value: total.value, color: '#409EFF', icon: Monitor },
|
|
||||||
{ label: '等待排期', value: filteredTickets.value.filter(t => t.is_complete === '未分配').length, color: '#A8ABB2', icon: Files },
|
|
||||||
{ label: '正在采集', value: filteredTickets.value.filter(t => t.is_complete === '进行中').length, color: '#E6A23C', icon: Timer },
|
|
||||||
{ label: '已就绪', value: filteredTickets.value.filter(t => t.is_complete === '已完成').length, color: '#67C23A', icon: CircleCheck }
|
|
||||||
])
|
|
||||||
|
|
||||||
const formatNumber = (val) => {
|
|
||||||
if (!val || val === '0') return '0';
|
|
||||||
const num = Number(val);
|
|
||||||
return isNaN(num) ? val : num.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusClass = (status) => {
|
|
||||||
const map = { '未分配': 'is-waiting', '进行中': 'is-running', '已完成': 'is-success' }
|
|
||||||
return map[status] || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await ticketApi.getTickets({ page: 1, limit: 1000 })
|
|
||||||
if (res.code === 200 || res.code === true) {
|
|
||||||
tickets.value = res.data.data
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error('同步失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("数据同步失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDateOnly = (ts) => {
|
return result;
|
||||||
if (!ts || ts === '0' || ts === '') return '-'
|
});
|
||||||
const date = !isNaN(ts) ? new Date(String(ts).length === 10 ? ts * 1000 : parseInt(ts)) : new Date(ts)
|
|
||||||
return isNaN(date.getTime()) ? '-' : `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
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 formatNumber = (val) => {
|
||||||
|
if (!val || val === "0") return "0";
|
||||||
|
const num = Number(val);
|
||||||
|
return isNaN(num) ? val : num.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const map = { 未分配: "is-waiting", 进行中: "is-running", 已完成: "is-success" };
|
||||||
|
return map[status] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateOnly = (ts) => {
|
||||||
|
if (!ts || ts === "0" || ts === "") return "-";
|
||||||
|
const date = !isNaN(ts)
|
||||||
|
? new Date(String(ts).length === 10 ? ts * 1000 : parseInt(ts))
|
||||||
|
: new Date(ts);
|
||||||
|
return isNaN(date.getTime())
|
||||||
|
? "-"
|
||||||
|
: `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (p) => {
|
||||||
|
page.value = p;
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
router.push("/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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.client-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-premium:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-dot {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (p) => {
|
50% {
|
||||||
page.value = p
|
opacity: 0.3;
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
100% {
|
||||||
localStorage.clear(); sessionStorage.clear(); router.push('/login')
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadData)
|
.s-label {
|
||||||
</script>
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
<style scoped>
|
.s-value {
|
||||||
.client-view-container {
|
font-size: 28px;
|
||||||
padding: 24px 40px;
|
font-weight: 800;
|
||||||
background-color: #f6f8fb;
|
color: #1d1d1f;
|
||||||
min-height: 100vh;
|
}
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部导航 */
|
.stat-icon-wrapper {
|
||||||
.client-header {
|
font-size: 36px;
|
||||||
display: flex;
|
color: var(--theme-color);
|
||||||
justify-content: space-between;
|
opacity: 0.1;
|
||||||
align-items: center;
|
}
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.brand { 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;
|
.data-section {
|
||||||
background: #fff; padding: 6px 14px;
|
background: #fff;
|
||||||
border-radius: 20px; font-size: 13px; color: #606266;
|
padding: 24px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
border-radius: 12px;
|
||||||
}
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
/* 指标卡片 */
|
.section-header {
|
||||||
.stat-card-premium {
|
display: flex;
|
||||||
background: #fff;
|
justify-content: space-between;
|
||||||
padding: 22px;
|
align-items: center;
|
||||||
border-radius: 12px;
|
margin-bottom: 20px;
|
||||||
display: flex;
|
}
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
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 ease;
|
|
||||||
}
|
|
||||||
.stat-card-premium:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
|
||||||
.s-label { font-size: 13px; color: #86868b; display: block; margin-bottom: 6px; }
|
|
||||||
.s-value { font-size: 28px; font-weight: 800; color: #1d1d1f; }
|
|
||||||
.stat-icon-wrapper { font-size: 36px; color: var(--theme-color); opacity: 0.1; }
|
|
||||||
|
|
||||||
/* 列表容器 */
|
.header-left {
|
||||||
.data-section {
|
display: flex;
|
||||||
background: #fff; padding: 24px; border-radius: 12px;
|
align-items: center;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
||||||
.header-left { display: flex; align-items: center; gap: 10px; }
|
|
||||||
.indicator { width: 3px; height: 16px; background: #409EFF; border-radius: 2px; }
|
|
||||||
.section-title { font-size: 16px; font-weight: 600; color: #303133; }
|
|
||||||
|
|
||||||
/* 表格样式微调 */
|
.indicator {
|
||||||
.id-tag { color: #909399; font-family: 'Consolas', monospace; font-size: 12px; }
|
width: 3px;
|
||||||
.project-cell { display: flex; align-items: center; gap: 8px; }
|
height: 16px;
|
||||||
.project-icon { color: #c0c4cc; font-size: 16px; }
|
background: #409eff;
|
||||||
.project-name { font-weight: 500; color: #2c3e50; }
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-labels { display: flex; justify-content: flex-end; margin-bottom: 4px; }
|
.section-title {
|
||||||
.percent-val { font-size: 11px; font-weight: 700; color: #409eff; }
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
.data-val-box { display: flex; flex-direction: column; align-items: center; }
|
.filter-tag {
|
||||||
.data-num { font-size: 16px; font-weight: 700; color: #2c3e50; font-family: 'Helvetica Neue', sans-serif; }
|
margin-left: 10px;
|
||||||
.unit { font-size: 9px; color: #a8abb2; transform: scale(0.9); }
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 状态指示器 */
|
.id-tag {
|
||||||
.status-dot-box { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; }
|
color: #909399;
|
||||||
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
font-family: "Consolas", monospace;
|
||||||
.is-waiting { color: #909399; } .is-waiting .dot { background: #909399; }
|
font-size: 12px;
|
||||||
.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; }
|
|
||||||
|
|
||||||
.date-text { font-size: 12px; color: #909399; }
|
.project-cell {
|
||||||
.pagination-footer { margin-top: 24px; display: flex; justify-content: center; }
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table) { --el-table-header-bg-color: #f8f9fb; }
|
.project-icon {
|
||||||
:deep(.el-table__row:hover) { background-color: #faffff !important; }
|
color: #c0c4cc;
|
||||||
</style>
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user