最新修改,初版

This commit is contained in:
2026-02-25 09:38:25 +08:00
parent 1f5bfba3b3
commit 0378198f55
15 changed files with 1691 additions and 824 deletions

293
src/views/ClientView.vue Normal file
View File

@@ -0,0 +1,293 @@
<template>
<div class="client-view-container">
<header class="client-header">
<div class="brand">
<div class="logo-box">
<el-icon><DataLine /></el-icon>
</div>
<div class="brand-text">
<span class="title">数据交付监控台</span>
<span class="version">Real-time Monitor v2.0</span>
</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>
</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" :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>
</el-col>
</el-row>
</section>
<main class="data-section">
<div class="section-header">
<div class="header-left">
<span class="indicator"></span>
<span class="section-title">实时交付看板</span>
</div>
<div class="header-right">
<el-button type="primary" link @click="loadData" :loading="loading">
<el-icon><Refresh /></el-icon>&nbsp;同步最新数据
</el-button>
</div>
</div>
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
<el-table-column prop="id" label="单号" width="90" align="center">
<template #default="{row}">
<span class="id-tag">#{{ row.id }}</span>
</template>
</el-table-column>
<el-table-column prop="website" label="网站地址" min-width="260">
<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>
<el-progress
: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">
<template #default="scope">
<div class="data-val-box">
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</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>
</template>
</el-table-column>
<el-table-column label="更新日期" width="140" align="center">
<template #default="scope">
<span class="date-text">{{ formatDateOnly(scope.row.lock_time || scope.row.update_time || scope.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>
</main>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DataLine, 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 loading = ref(false)
const tickets = ref([])
const page = ref(1)
const limit = ref(15)
const currentUser = computed(() => {
const info = localStorage.getItem('user_info') || sessionStorage.getItem('user_info')
return info ? JSON.parse(info) : { username: '外部客户' }
})
const filteredTickets = computed(() => {
const allowed = ['未分配', '进行中', '已完成']
return tickets.value.filter(t => allowed.includes(t.is_complete))
})
const paginatedTickets = computed(() => {
const start = (page.value - 1) * limit.value
return filteredTickets.value.slice(start, start + limit.value)
})
const total = computed(() => filteredTickets.value.length)
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
}
}
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 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; }
/* 列表容器 */
.data-section {
background: #fff; padding: 24px; border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
}
.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; }
/* 表格样式微调 */
.id-tag { color: #909399; font-family: 'Consolas', monospace; font-size: 12px; }
.project-cell { display: flex; align-items: center; gap: 8px; }
.project-icon { color: #c0c4cc; font-size: 16px; }
.project-name { font-weight: 500; color: #2c3e50; }
.progress-labels { display: flex; justify-content: flex-end; margin-bottom: 4px; }
.percent-val { font-size: 11px; font-weight: 700; color: #409eff; }
.data-val-box { display: flex; flex-direction: column; align-items: center; }
.data-num { font-size: 16px; font-weight: 700; color: #2c3e50; font-family: 'Helvetica Neue', sans-serif; }
.unit { font-size: 9px; color: #a8abb2; transform: scale(0.9); }
/* 状态指示器 */
.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; }
.date-text { font-size: 12px; color: #909399; }
.pagination-footer { margin-top: 24px; display: flex; justify-content: center; }
:deep(.el-table) { --el-table-header-bg-color: #f8f9fb; }
:deep(.el-table__row:hover) { background-color: #faffff !important; }
</style>