前端展示项目
This commit is contained in:
12
Vue/frontend/package-lock.json
generated
12
Vue/frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
@ -1029,6 +1030,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
"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": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
"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": {
|
"delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
|
|||||||
@ -1,22 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<h3>设备管理系统</h3>
|
<div class="brand">
|
||||||
<nav>
|
<div class="logo-icon">⚡</div>
|
||||||
<router-link to="/dashboard">📊 项目看板</router-link>
|
<h3>设备管理系统</h3>
|
||||||
<router-link to="/project/add">➕ 新建项目</router-link>
|
</div>
|
||||||
<router-link v-if="userStore.role === 'admin'" to="/admin/users">👥 人员权限</router-link>
|
|
||||||
|
<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>
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span>v1.0.0</span>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
<span>当前用户: {{ userStore.username }}</span>
|
<div class="breadcrumb">
|
||||||
<button @click="handleLogout">退出</button>
|
<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>
|
</header>
|
||||||
|
|
||||||
<div class="page-view">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,16 +61,186 @@ const userStore = useUserStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
// 添加一点确认逻辑或直接退出
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.layout-container { display: flex; height: 100vh; }
|
/* --- 全浅色主题变量 --- */
|
||||||
.sidebar { width: 200px; background: #2c3e50; color: white; padding: 20px; }
|
:root {
|
||||||
.sidebar a { display: block; color: white; margin: 10px 0; text-decoration: none; }
|
/* 侧边栏:纯白 */
|
||||||
.main-content { flex: 1; display: flex; flex-direction: column; }
|
--sidebar-bg: #ffffff;
|
||||||
.top-bar { padding: 10px 20px; background: #eee; display: flex; justify-content: space-between; }
|
/* 侧边栏文字:深灰(不仅能看见,而且不刺眼) */
|
||||||
.page-view { padding: 20px; overflow: auto; }
|
--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>
|
</style>
|
||||||
|
|||||||
@ -1,43 +1,562 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="board-container">
|
||||||
<h2>项目进度看板</h2>
|
<div class="header">
|
||||||
<table border="1" cellspacing="0" cellpadding="10" style="width: 100%">
|
<div class="header-decoration left"></div>
|
||||||
<thead>
|
<h2 class="title">项目全生命周期监控看板 (Pro)</h2>
|
||||||
<tr>
|
<div class="header-decoration right"></div>
|
||||||
<th>项目名称</th>
|
</div>
|
||||||
<th>状态</th>
|
|
||||||
<th>成立日期</th>
|
<div class="control-bar">
|
||||||
<th>负责人</th>
|
<div class="filter-tabs">
|
||||||
<th>操作</th>
|
<div
|
||||||
</tr>
|
v-for="tab in tabs"
|
||||||
</thead>
|
:key="tab"
|
||||||
<tbody>
|
class="filter-item"
|
||||||
<tr v-for="item in projects" :key="item.id">
|
:class="{ active: currentTab === tab }"
|
||||||
<td>{{ item.name }}</td>
|
@click="currentTab = tab"
|
||||||
<td>{{ item.status }}</td>
|
>
|
||||||
<td>{{ item.created_at }}</td>
|
{{ tab }}
|
||||||
<td>{{ item.owner }}</td>
|
<div class="active-line" v-if="currentTab === tab"></div>
|
||||||
<td><button>查看详情</button></td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
<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 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, 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import axios from '../api/request'
|
|
||||||
|
|
||||||
|
// === 1. 状态定义 ===
|
||||||
|
const tabs = ['全部', '研发中', '维修中', '待维修', '已完成']
|
||||||
|
const currentTab = ref('全部')
|
||||||
|
const sortMode = ref('delivery') // 默认按照交货期排序
|
||||||
const projects = ref([])
|
const projects = ref([])
|
||||||
|
|
||||||
onMounted(async () => {
|
// === 2. 核心:筛选与排序逻辑 ===
|
||||||
// 真实请求: const res = await axios.get('/projects')
|
const processedProjects = computed(() => {
|
||||||
// projects.value = res.data
|
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 = [
|
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>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user