前端展示项目

This commit is contained in:
YueL1331
2026-01-19 19:34:34 +08:00
parent 03ffb8ddbc
commit 77ad0bd22a
4 changed files with 777 additions and 44 deletions

View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"pinia": "^3.0.4",
"vue": "^3.5.24",
"vue-router": "^4.6.4"
@ -1029,6 +1030,12 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -2264,6 +2271,11 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
},
"dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"pinia": "^3.0.4",
"vue": "^3.5.24",
"vue-router": "^4.6.4"

View File

@ -1,22 +1,53 @@
<template>
<div class="layout-container">
<aside class="sidebar">
<div class="brand">
<div class="logo-icon"></div>
<h3>设备管理系统</h3>
<nav>
<router-link to="/dashboard">📊 项目看板</router-link>
<router-link to="/project/add"> 新建项目</router-link>
<router-link v-if="userStore.role === 'admin'" to="/admin/users">👥 人员权限</router-link>
</div>
<nav class="nav-menu">
<router-link to="/dashboard" class="nav-item">
<span class="icon">📊</span>
<span class="text">项目看板</span>
</router-link>
<router-link to="/project/add" class="nav-item">
<span class="icon"></span>
<span class="text">新建项目</span>
</router-link>
<router-link v-if="userStore.role === 'admin'" to="/admin/users" class="nav-item">
<span class="icon">👥</span>
<span class="text">人员权限</span>
</router-link>
</nav>
<div class="sidebar-footer">
<span>v1.0.0</span>
</div>
</aside>
<div class="main-content">
<header class="top-bar">
<span>当前用户: {{ userStore.username }}</span>
<button @click="handleLogout">退出</button>
<div class="breadcrumb">
<span class="current-date">{{ new Date().toLocaleDateString() }}</span>
</div>
<div class="user-actions">
<span class="welcome-text">Hi, <strong>{{ userStore.username }}</strong></span>
<button class="logout-btn" @click="handleLogout">
<i>🚪</i> 退出
</button>
</div>
</header>
<div class="page-view">
<router-view></router-view>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</div>
@ -30,16 +61,186 @@ const userStore = useUserStore()
const router = useRouter()
const handleLogout = () => {
// 添加一点确认逻辑或直接退出
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.layout-container { display: flex; height: 100vh; }
.sidebar { width: 200px; background: #2c3e50; color: white; padding: 20px; }
.sidebar a { display: block; color: white; margin: 10px 0; text-decoration: none; }
.main-content { flex: 1; display: flex; flex-direction: column; }
.top-bar { padding: 10px 20px; background: #eee; display: flex; justify-content: space-between; }
.page-view { padding: 20px; overflow: auto; }
/* --- 全浅色主题变量 --- */
:root {
/* 侧边栏:纯白 */
--sidebar-bg: #ffffff;
/* 侧边栏文字:深灰(不仅能看见,而且不刺眼) */
--sidebar-text: #5c6b7f;
/* 选中状态:清爽的淡蓝色 */
--active-bg: #e6f7ff;
--active-text: #1890ff; /* 阿里蓝/蚂蚁金服蓝 */
/* 右侧内容区背景:极淡的灰白,用于区分侧边栏和卡片 */
--main-bg: #f5f7fa;
--header-height: 64px;
}
.layout-container {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
/* 全局字体设为深色,防止任何地方出现白字看不见的情况 */
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* --- 1. 左侧侧边栏 --- */
.sidebar {
width: 240px;
height: 100%;
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
display: flex;
flex-direction: column;
/* 右侧加一条极细的分隔线,而不是阴影,更扁平化 */
border-right: 1px solid #e8e8e8;
flex-shrink: 0;
z-index: 10;
}
/* Logo 区域 */
.brand {
height: var(--header-height);
display: flex;
align-items: center;
padding: 0 24px;
/* Logo 下方也加一条线,与顶栏对齐 */
border-bottom: 1px solid #e8e8e8;
}
.logo-icon {
font-size: 20px;
margin-right: 10px;
color: #1890ff; /* Logo 图标颜色 */
}
.brand h3 {
font-size: 16px;
font-weight: 700;
color: #1f2937; /* Logo 文字深黑色 */
margin: 0;
}
/* 导航菜单 */
.nav-menu {
flex: 1;
padding: 16px 8px; /* 左右留一点空隙给胶囊按钮 */
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin-bottom: 4px;
color: var(--sidebar-text);
text-decoration: none;
border-radius: 6px;
transition: all 0.2s;
font-size: 14px;
}
.nav-item .icon {
margin-right: 10px;
font-size: 16px;
width: 20px;
text-align: center;
}
/* 悬停效果 */
.nav-item:hover {
background-color: #fafafa; /* 极淡的灰 */
color: #1f2937;
}
/* 选中状态 (Active) */
.nav-item.router-link-active {
background-color: var(--active-bg);
color: var(--active-text);
font-weight: 500;
}
/* 侧边栏底部 */
.sidebar-footer {
padding: 16px;
text-align: center;
font-size: 12px;
color: #9ca3af;
border-top: 1px solid #e8e8e8;
}
/* --- 2. 右侧主体区域 --- */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
/* 关键:使用淡灰色背景,这样你的白色卡片/图表放上去会有“卡片感” */
background-color: var(--main-bg);
}
/* 顶部 Top Bar */
.top-bar {
height: var(--header-height);
background: #ffffff; /* 纯白背景 */
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
/* 底部阴影,让顶栏稍微浮起,产生层次感 */
box-shadow: 0 1px 4px rgba(0,21,41,0.08);
position: relative;
z-index: 5;
}
.welcome-text {
font-size: 14px;
color: #374151;
}
.logout-btn {
padding: 6px 16px;
font-size: 13px;
border: 1px solid #d9d9d9;
background: #fff;
color: #666;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
color: #ff4d4f;
border-color: #ff4d4f;
background-color: #fff1f0;
}
/* --- 3. 页面内容视口 --- */
.page-view {
flex: 1;
padding: 24px; /* 给内容留出呼吸空间 */
overflow-y: auto;
}
/* (可选) 滚动条美化:让它看起来不像老式浏览器 */
.page-view::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.page-view::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
}
.page-view::-webkit-scrollbar-track {
background: transparent;
}
</style>

View File

@ -1,43 +1,562 @@
<template>
<div>
<h2>项目进度看板</h2>
<table border="1" cellspacing="0" cellpadding="10" style="width: 100%">
<div class="board-container">
<div class="header">
<div class="header-decoration left"></div>
<h2 class="title">项目全生命周期监控看板 (Pro)</h2>
<div class="header-decoration right"></div>
</div>
<div class="control-bar">
<div class="filter-tabs">
<div
v-for="tab in tabs"
:key="tab"
class="filter-item"
:class="{ active: currentTab === tab }"
@click="currentTab = tab"
>
{{ tab }}
<div class="active-line" v-if="currentTab === tab"></div>
</div>
</div>
<div class="sort-switch">
<span class="sort-label">排序优先</span>
<button
class="switch-btn"
:class="{ active: sortMode === 'total' }"
@click="sortMode = 'total'"
>
总周期
</button>
<button
class="switch-btn"
:class="{ active: sortMode === 'current' }"
@click="sortMode = 'current'"
>
当前滞留
</button>
<button
class="switch-btn"
:class="{ active: sortMode === 'delivery' }"
@click="sortMode = 'delivery'"
>
交货期/紧急度
</button>
</div>
</div>
<div class="table-wrapper">
<table class="tech-table">
<thead>
<tr>
<th>项目名称</th>
<th>状态</th>
<th>成立日期</th>
<th>负责人</th>
<th>操作</th>
<th width="18%">项目名称</th>
<th width="10%">状态</th>
<th width="12%">总运行时长</th>
<th width="12%">负责人</th>
<th width="12%">当前滞留</th>
<th width="22%">
预计交货 & 绩效
<span class="th-sub">(Deadline & Performance)</span>
</th>
<th width="14%">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in projects" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.status }}</td>
<td>{{ item.created_at }}</td>
<td>{{ item.owner }}</td>
<td><button>查看详情</button></td>
<tr
v-for="(item, index) in processedProjects"
:key="item.id"
class="slide-in"
:style="{'--delay': index * 0.05 + 's'}"
>
<td class="col-name">
<span class="index-badge">{{ String(index + 1).padStart(2, '0') }}</span>
<span class="name-text">{{ item.name }}</span>
</td>
<td>
<span class="status-tag" :class="getStatusClass(item.status)">
{{ item.status }}
</span>
</td>
<td class="col-duration total">
<span class="days-num">
{{ getDaysDiff(item.created_at, item.finished_at) }}
</span>
</td>
<td class="col-owner">
<span class="owner-icon"></span> {{ item.owner }}
</td>
<td class="col-duration">
<template v-if="item.status !== '已完成'">
<span class="days-num" :class="getDwellClass(getDaysDiff(item.current_owner_start_date))">
{{ getDaysDiff(item.current_owner_start_date) }}
</span>
</template>
<template v-else> <span class="text-mute">-</span> </template>
</td>
<td class="col-delivery">
<div v-if="item.status === '已完成'">
<div class="date-text text-mute">{{ item.delivery_date || '无计划' }}</div>
<div class="perf-status">
<span v-if="getPerformance(item) > 0" class="perf-late">
延期 {{ getPerformance(item) }}
</span>
<span v-else-if="getPerformance(item) < 0" class="perf-early">
🚀 提前 {{ Math.abs(getPerformance(item)) }}
</span>
<span v-else class="perf-ontime">
按时交付
</span>
</div>
</div>
<div v-else-if="item.delivery_date">
<div class="date-text" :class="getDeliveryUrgencyClass(item.delivery_date)">
{{ item.delivery_date }}
</div>
<div class="countdown">
<template v-if="getDaysRemaining(item.delivery_date) < 0">
<span class="delivery-overdue">
已逾期 {{ Math.abs(getDaysRemaining(item.delivery_date)) }} !
</span>
</template>
<template v-else>
( {{ getDaysRemaining(item.delivery_date) }} )
</template>
</div>
</div>
<div v-else>
<span class="tag-missing">待定 / 缺排期</span>
</div>
</td>
<td>
<button class="action-btn" @click="handleDetail(item)">查看详情</button>
</td>
</tr>
</tbody>
</table>
<div v-if="processedProjects.length === 0" class="empty-tip">
当前列表中无相关项目
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from '../api/request'
import { ref, onMounted, computed } from 'vue'
// === 1. 状态定义 ===
const tabs = ['全部', '研发中', '维修中', '待维修', '已完成']
const currentTab = ref('全部')
const sortMode = ref('delivery') // 默认按照交货期排序
const projects = ref([])
onMounted(async () => {
// 真实请求: const res = await axios.get('/projects')
// projects.value = res.data
// === 2. 核心:筛选与排序逻辑 ===
const processedProjects = computed(() => {
let list = []
// 模拟数据
// A. 筛选
if (currentTab.value === '已完成') {
list = projects.value.filter(item => item.status === '已完成')
} else if (currentTab.value === '全部') {
list = projects.value.filter(item => item.status !== '已完成')
} else {
list = projects.value.filter(item => item.status === currentTab.value)
}
// B. 排序
return list.sort((a, b) => {
// 模式1: 按总周期 (创建时间早的在前)
if (sortMode.value === 'total') {
return new Date(a.created_at) - new Date(b.created_at)
}
// 模式2: 按当前滞留 (接手时间早的在前,即滞留越久越前)
else if (sortMode.value === 'current') {
const dateA = a.current_owner_start_date ? new Date(a.current_owner_start_date) : new Date()
const dateB = b.current_owner_start_date ? new Date(b.current_owner_start_date) : new Date()
return dateA - dateB
}
// 模式3: 按交货期 (智能排序)
else if (sortMode.value === 'delivery') {
// 权重计算函数
const getRank = (item) => {
// 优先级 1: 未完成且逾期 (日期越小越严重) -> 返回时间戳
// 优先级 2: 未完成且有日期 (日期越小越紧急) -> 返回时间戳
// 优先级 3: 未完成无日期 (排在有日期之后) -> 给个极大值
// 优先级 4: 已完成 (排在最后) -> 给个超大值
if (item.status === '已完成') return 90000000000000
if (!item.delivery_date) return 80000000000000
return new Date(item.delivery_date).getTime()
}
const rankA = getRank(a)
const rankB = getRank(b)
if (rankA !== rankB) {
return rankA - rankB
}
// === 二级排序 (当交货期相同时) ===
// 按滞留时间倒序 (越久越优先)
// start_date 越小,滞留越久。所以用 dateA - dateB
const dwellA = a.current_owner_start_date ? new Date(a.current_owner_start_date) : new Date()
const dwellB = b.current_owner_start_date ? new Date(b.current_owner_start_date) : new Date()
return dwellA - dwellB
}
return 0
})
})
// === 3. 工具函数 ===
// 计算时长 (支持已完成项目锁定时间)
// endStr 传入 finished_at如果为空则默认为 now
const getDaysDiff = (startStr, endStr = null) => {
if (!startStr) return 0
const start = new Date(startStr)
const end = endStr ? new Date(endStr) : new Date()
// 简单容错:不能算出负数
if (start > end) return 0
const diffTime = Math.abs(end - start)
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}
// 计算剩余天数 (正数=剩余,负数=逾期)
const getDaysRemaining = (dateStr) => {
if (!dateStr) return 0
const end = new Date(dateStr)
const now = new Date()
// 设为当天的 00:00:00 以避免小时误差
end.setHours(0,0,0,0)
now.setHours(0,0,0,0)
const diffTime = end - now
return Math.floor(diffTime / (1000 * 60 * 60 * 24))
}
// 计算绩效 (实际完成 - 计划完成)
const getPerformance = (item) => {
if (!item.finished_at || !item.delivery_date) return 0
const actual = new Date(item.finished_at)
const plan = new Date(item.delivery_date)
actual.setHours(0,0,0,0)
plan.setHours(0,0,0,0)
return Math.floor((actual - plan) / (1000 * 60 * 60 * 24))
}
// 样式工厂
const getDwellClass = (days) => {
if (days > 30) return 'text-danger'
if (days > 10) return 'text-warning'
return 'text-normal'
}
const getDeliveryUrgencyClass = (dateStr) => {
const remaining = getDaysRemaining(dateStr)
if (remaining < 0) return 'text-danger' // 逾期
if (remaining < 7) return 'text-danger' // 紧急
if (remaining < 30) return 'text-warning' // 较急
return 'text-normal'
}
const getStatusClass = (status) => {
switch (status) {
case '研发中': return 'status-process'
case '维修中': return 'status-repairing'
case '待维修': return 'status-pending'
case '已完成': return 'status-success'
default: return 'status-default'
}
}
const handleDetail = (item) => {
alert(`查看详情:${item.name}`)
}
// === 4. 模拟数据 (动态生成以保证逻辑演示效果) ===
const getDate = (offsetDays) => {
const d = new Date()
d.setDate(d.getDate() + offsetDays)
// 格式化 YYYY-MM-DD
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2,'0')
const day = String(d.getDate()).padStart(2,'0')
return `${y}-${m}-${day}`
}
onMounted(() => {
projects.value = [
{ id: 1, name: '高精度传感器研发', status: '研发中', created_at: '2023-10-01', owner: 'zhangsan' },
{ id: 2, name: '机械臂维修A01', status: '维修', created_at: '2023-10-05', owner: 'lisi' }
{
id: 1, name: '核心部件修复 (严重逾期)', status: '维修', owner: '王五',
created_at: getDate(-60), current_owner_start_date: getDate(-40), // 滞留40天
delivery_date: getDate(-10), // 逾期10天
finished_at: null
},
{
id: 2, name: '传感器校准 (逾期且滞留)', status: '维修中', owner: '赵六',
created_at: getDate(-30), current_owner_start_date: getDate(-5),
delivery_date: getDate(-2), // 逾期2天
finished_at: null
},
{
id: 3, name: '控制系统升级 (紧急)', status: '研发中', owner: '张三',
created_at: getDate(-20), current_owner_start_date: getDate(-10),
delivery_date: getDate(3), // 剩3天
finished_at: null
},
{
id: 4, name: '外壳材料测试 (正常)', status: '研发中', owner: '工程组',
created_at: getDate(-5), current_owner_start_date: getDate(-5),
delivery_date: getDate(30), // 剩30天
finished_at: null
},
{
id: 5, name: '备件采购 (无排期风险)', status: '待维修', owner: '采购部',
created_at: getDate(-15), current_owner_start_date: getDate(-15),
delivery_date: null, // 无日期,排在进行中的最后
finished_at: null
},
{
id: 6, name: '一期工程 (提前完成)', status: '已完成', owner: '李四',
created_at: getDate(-100), current_owner_start_date: getDate(-50),
delivery_date: getDate(-20),
finished_at: getDate(-25) // 提前5天
},
{
id: 7, name: '二期工程 (延期交付)', status: '已完成', owner: '李四',
created_at: getDate(-100), current_owner_start_date: getDate(-40),
delivery_date: getDate(-30),
finished_at: getDate(-25) // 晚了5天
},
]
})
</script>
<style scoped>
/* 全局容器 */
.board-container {
width: 100%;
min-height: 100vh;
background-color: #050a1f;
padding: 30px;
box-sizing: border-box;
color: #fff;
font-family: 'PingFang SC', sans-serif;
}
/* 顶部 Header */
.header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.title {
font-size: 32px;
margin: 0 20px;
color: #00f2ff;
letter-spacing: 3px;
text-shadow: 0 0 15px rgba(0, 242, 255, 0.4);
}
.header-decoration {
flex: 1;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.6), transparent);
}
/* 控制栏 */
.control-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 10px;
}
.filter-tabs { display: flex; gap: 30px; }
.filter-item {
font-size: 16px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
padding: 8px 0;
position: relative;
transition: all 0.3s;
}
.filter-item:hover, .filter-item.active {
color: #fff;
text-shadow: 0 0 8px #00f2ff;
}
.active-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: #00f2ff;
box-shadow: 0 0 8px #00f2ff;
}
.sort-switch {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.05);
padding: 5px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.sort-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
margin: 0 10px;
}
.switch-btn {
background: transparent;
border: none;
color: #aaa;
padding: 6px 15px;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
margin-left: 2px;
}
.switch-btn:hover { color: #fff; }
.switch-btn.active {
background: rgba(0, 242, 255, 0.2);
color: #00f2ff;
font-weight: bold;
}
/* 表格区域 */
.table-wrapper {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(0, 242, 255, 0.15);
border-radius: 12px;
padding: 20px;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.4);
min-height: 500px;
}
.tech-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 10px;
text-align: left;
}
.tech-table th {
padding: 10px 15px;
color: rgba(255, 255, 255, 0.5);
font-size: 13px;
font-weight: normal;
}
.th-sub { display: block; font-size: 12px; opacity: 0.6; transform: scale(0.9); transform-origin: left top; }
.tech-table tbody tr {
background: rgba(18, 32, 60, 0.6);
transition: transform 0.2s, background 0.2s;
}
.tech-table tbody tr:hover {
background: rgba(0, 242, 255, 0.08);
transform: scale(1.005);
}
.tech-table td {
padding: 15px;
color: #ddd;
vertical-align: middle;
}
/* 列样式细节 */
.col-name { display: flex; align-items: center; }
.index-badge { color: #00f2ff; font-family: monospace; margin-right: 10px; opacity: 0.7; }
.name-text { font-size: 16px; font-weight: 600; color: #fff; }
.status-tag { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.status-process { color: #4da6ff; background: rgba(0, 102, 255, 0.15); border: 1px solid rgba(0, 102, 255, 0.3); }
.status-repairing { color: #ff7043; background: rgba(255, 87, 34, 0.15); border: 1px solid rgba(255, 87, 34, 0.3); }
.status-pending { color: #ffd54f; background: rgba(255, 193, 7, 0.15); border: 1px solid rgba(255, 193, 7, 0.3); }
.status-success { color: #00ff7f; background: rgba(0, 255, 127, 0.15); border: 1px solid rgba(0, 255, 127, 0.3); }
.col-duration { font-family: 'Consolas', monospace; font-size: 16px; }
.col-duration.total { color: #00f2ff; }
.days-num { font-size: 20px; font-weight: bold; margin-right: 2px; }
.text-mute { color: rgba(255,255,255,0.3); }
.col-owner { font-size: 14px; color: #e0e0e0; }
.owner-icon { color: #00f2ff; font-size: 12px; margin-right: 5px; }
/* 文本颜色预警体系 */
.text-normal { color: #fff; opacity: 0.7; }
.text-warning { color: #ffd54f; }
.text-danger { color: #ff4d4d; }
/* 交货期 & 绩效样式 */
.delivery-overdue {
color: #ff3333;
font-weight: 800;
animation: pulseFast 1.5s infinite;
display: block;
font-size: 13px;
margin-top: 4px;
}
.tag-missing {
color: #ffd54f;
border: 1px solid rgba(255, 213, 79, 0.3);
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: rgba(255, 213, 79, 0.05);
}
.perf-late { color: #ff7043; font-weight: bold; font-size: 13px; }
.perf-early { color: #00ff7f; font-weight: bold; font-size: 13px; }
.perf-ontime { color: #fff; opacity: 0.5; font-size: 13px; }
@keyframes pulseFast {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.02); }
100% { opacity: 1; transform: scale(1); }
}
.countdown { font-size: 12px; margin-top: 4px; opacity: 0.8; }
.date-text { font-family: 'Consolas', monospace; font-size: 14px; }
.action-btn {
background: transparent;
border: 1px solid rgba(0, 242, 255, 0.5);
color: #00f2ff;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: 0.3s;
}
.action-btn:hover { background: #00f2ff; color: #000; }
.empty-tip { text-align: center; color: rgba(255,255,255,0.3); padding: 60px; }
.slide-in {
opacity: 0;
animation: slideIn 0.5s ease forwards;
animation-delay: var(--delay);
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>