style: UI鍏ㄩ潰鍗囩骇涓洪涔﹂鏍?- 鐧藉簳钃濊壊涓昏壊璋?娓呯埥涓撲笟
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
df9147a554
commit
bc06725ed1
@ -1,8 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; }
|
||||
</style>
|
||||
|
||||
@ -1,57 +1,53 @@
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapsed ? '64px' : '220px'" class="aside">
|
||||
<el-aside :width="isCollapsed ? '64px' : '240px'" class="aside">
|
||||
<div class="logo" @click="isCollapsed = !isCollapsed">
|
||||
<el-icon :size="24"><DataAnalysis /></el-icon>
|
||||
<span v-show="!isCollapsed" class="logo-text">AirLabs</span>
|
||||
<div class="logo-icon">A</div>
|
||||
<transition name="fade">
|
||||
<div v-show="!isCollapsed" class="logo-info">
|
||||
<span class="logo-title">AirLabs</span>
|
||||
<span class="logo-sub">项目管理系统</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<router-link
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="nav-item"
|
||||
:class="{ active: isActive(item.path) }"
|
||||
v-show="!item.role || hasRole(item.role)"
|
||||
>
|
||||
<el-icon :size="18"><component :is="item.icon" /></el-icon>
|
||||
<span v-show="!isCollapsed" class="nav-label">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- 底部用户信息 -->
|
||||
<div class="sidebar-footer" v-show="!isCollapsed">
|
||||
<div class="user-brief">
|
||||
<div class="user-avatar">{{ authStore.user?.name?.[0] || '?' }}</div>
|
||||
<div class="user-meta">
|
||||
<div class="user-name">{{ authStore.user?.name }}</div>
|
||||
<div class="user-role">{{ authStore.user?.role }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
:collapse="isCollapsed"
|
||||
router
|
||||
background-color="#1d1e2c"
|
||||
text-color="#a0a3bd"
|
||||
active-text-color="#ffffff"
|
||||
class="side-menu"
|
||||
>
|
||||
<el-menu-item v-if="authStore.isOwner()" index="/dashboard">
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/projects">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span>项目管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/submissions">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
<span>内容提交</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="authStore.isLeaderOrAbove()" index="/costs">
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>成本管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="authStore.isOwner()" index="/users">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<el-container class="main-container">
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ $route.name }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<h3 class="page-route-title">{{ currentTitle }}</h3>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-tag :type="roleTagType" size="small" class="role-tag">{{ authStore.user?.role }}</el-tag>
|
||||
<span class="user-name">{{ authStore.user?.name }}</span>
|
||||
<el-button text @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<el-button text class="logout-btn" @click="handleLogout">
|
||||
<el-icon :size="16"><SwitchButton /></el-icon>
|
||||
<span>退出</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
@ -64,18 +60,46 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const isCollapsed = ref(false)
|
||||
|
||||
const roleTagType = computed(() => {
|
||||
const map = { 'Owner': 'danger', '主管': 'warning', '组长': '', '成员': 'info' }
|
||||
return map[authStore.user?.role] || 'info'
|
||||
const menuItems = [
|
||||
{ path: '/dashboard', label: '仪表盘', icon: 'Odometer', role: 'Owner' },
|
||||
{ path: '/projects', label: '项目管理', icon: 'FolderOpened' },
|
||||
{ path: '/submissions', label: '内容提交', icon: 'EditPen' },
|
||||
{ path: '/costs', label: '成本管理', icon: 'Money', role: 'leader+' },
|
||||
{ path: '/users', label: '用户管理', icon: 'User', role: 'Owner' },
|
||||
]
|
||||
|
||||
const titleMap = {
|
||||
'/dashboard': '仪表盘',
|
||||
'/projects': '项目管理',
|
||||
'/submissions': '内容提交',
|
||||
'/costs': '成本管理',
|
||||
'/users': '用户管理',
|
||||
}
|
||||
|
||||
const currentTitle = computed(() => {
|
||||
if (route.path.startsWith('/projects/')) return '项目详情'
|
||||
if (route.path.startsWith('/settlement/')) return '项目结算'
|
||||
return titleMap[route.path] || ''
|
||||
})
|
||||
|
||||
function isActive(path) {
|
||||
return route.path === path || route.path.startsWith(path + '/')
|
||||
}
|
||||
|
||||
function hasRole(role) {
|
||||
if (role === 'Owner') return authStore.isOwner()
|
||||
if (role === 'leader+') return authStore.isLeaderOrAbove()
|
||||
return true
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.token && !authStore.user) {
|
||||
await authStore.fetchUser()
|
||||
@ -90,32 +114,124 @@ function handleLogout() {
|
||||
|
||||
<style scoped>
|
||||
.layout { height: 100vh; }
|
||||
|
||||
/* ── 侧边栏 ── */
|
||||
.aside {
|
||||
background: #1d1e2c;
|
||||
transition: width 0.3s;
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
padding: 0 16px;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #2d2e3e;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.logo-text { font-size: 18px; font-weight: 700; letter-spacing: 2px; }
|
||||
.side-menu { border-right: none; }
|
||||
.logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo-info { display: flex; flex-direction: column; }
|
||||
.logo-title { font-size: 15px; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||
.logo-sub { font-size: 11px; color: var(--text-secondary); }
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-regular);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.nav-item.active {
|
||||
background: var(--bg-active);
|
||||
color: var(--primary);
|
||||
}
|
||||
.nav-label { line-height: 1; }
|
||||
|
||||
/* 底部用户 */
|
||||
.sidebar-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
.user-brief { display: flex; align-items: center; gap: 10px; }
|
||||
.user-avatar {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; font-size: 13px; flex-shrink: 0;
|
||||
}
|
||||
.user-meta { display: flex; flex-direction: column; }
|
||||
.user-name { font-size: 13px; font-weight: 600; color: var(--text-primary); line-height: 1.3; }
|
||||
.user-role { font-size: 11px; color: var(--text-secondary); }
|
||||
|
||||
/* ── 顶栏 ── */
|
||||
.main-container { background: var(--bg-page); }
|
||||
.header {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.header-right { display: flex; align-items: center; gap: 12px; }
|
||||
.user-name { font-size: 14px; color: #333; }
|
||||
.role-tag { margin-right: 4px; }
|
||||
.main { background: #f5f7fa; min-height: 0; }
|
||||
.page-route-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.logout-btn {
|
||||
color: var(--text-secondary) !important;
|
||||
font-size: 13px !important;
|
||||
gap: 4px;
|
||||
}
|
||||
.logout-btn:hover { color: var(--danger) !important; }
|
||||
|
||||
/* ── 主内容 ── */
|
||||
.main {
|
||||
padding: 24px;
|
||||
background: var(--bg-page);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@ -4,6 +4,7 @@ import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
|
||||
@ -1,79 +1,174 @@
|
||||
/* ══════════════════════════════════════════════
|
||||
AirLabs Project · 飞书风格全局主题
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
/* 主色 */
|
||||
--primary: #3370FF;
|
||||
--primary-light: #E8F0FE;
|
||||
--primary-hover: #2860E1;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
/* 功能色 */
|
||||
--success: #34C759;
|
||||
--warning: #FF9500;
|
||||
--danger: #FF3B30;
|
||||
--info: #8E8E93;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* 文字 */
|
||||
--text-primary: #1F2329;
|
||||
--text-regular: #3B3F46;
|
||||
--text-secondary: #8F959E;
|
||||
--text-placeholder: #BFC3C9;
|
||||
|
||||
/* 背景 */
|
||||
--bg-page: #F5F6F7;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-sidebar: #FFFFFF;
|
||||
--bg-hover: #F0F1F2;
|
||||
--bg-active: #E8F0FE;
|
||||
|
||||
/* 边框 */
|
||||
--border-color: #E5E6EB;
|
||||
--border-light: #F0F1F2;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
/* 字体 */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-page);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
/* ── Element Plus 主题覆盖 ── */
|
||||
|
||||
/* 按钮 */
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: var(--primary) !important;
|
||||
--el-button-border-color: var(--primary) !important;
|
||||
--el-button-hover-bg-color: var(--primary-hover) !important;
|
||||
--el-button-hover-border-color: var(--primary-hover) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
.el-button {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
/* 卡片 */
|
||||
.el-card {
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
.el-card__header {
|
||||
border-bottom: 1px solid var(--border-light) !important;
|
||||
padding: 16px 20px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
.el-card__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
/* 表格 */
|
||||
.el-table {
|
||||
--el-table-border-color: var(--border-light) !important;
|
||||
--el-table-header-bg-color: #FAFBFC !important;
|
||||
--el-table-row-hover-bg-color: #F7F8FA !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
overflow: hidden;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
font-weight: 600 !important;
|
||||
color: var(--text-secondary) !important;
|
||||
font-size: 12px !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
/* 标签 */
|
||||
.el-tag {
|
||||
border-radius: 4px !important;
|
||||
font-size: 12px !important;
|
||||
border: none !important;
|
||||
}
|
||||
.el-tag--success { background: #E8F8EE !important; color: #1A9E3F !important; }
|
||||
.el-tag--warning { background: #FFF3E0 !important; color: #D47E00 !important; }
|
||||
.el-tag--danger { background: #FFE8E7 !important; color: #D4380D !important; }
|
||||
.el-tag--info { background: #F0F1F2 !important; color: #646A73 !important; }
|
||||
.el-tag--primary, .el-tag:not([class*="--"]) { background: var(--primary-light) !important; color: var(--primary) !important; }
|
||||
|
||||
/* 输入框 */
|
||||
.el-input__wrapper {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: 0 0 0 1px var(--border-color) inset !important;
|
||||
}
|
||||
.el-input__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px #C0C4CC inset !important;
|
||||
}
|
||||
.el-input__wrapper.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--primary) inset !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
/* 对话框 */
|
||||
.el-dialog {
|
||||
border-radius: var(--radius-lg) !important;
|
||||
}
|
||||
.el-dialog__header {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-right: 0 !important;
|
||||
padding: 16px 24px !important;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px !important;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
border-top: 1px solid var(--border-light);
|
||||
padding: 12px 24px !important;
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.el-progress-bar__outer {
|
||||
border-radius: 4px !important;
|
||||
background: #F0F1F2 !important;
|
||||
}
|
||||
.el-progress-bar__inner {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* 选项卡 */
|
||||
.el-tabs__item {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
.el-tabs__active-bar {
|
||||
background-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
/* 描述列表 */
|
||||
.el-descriptions__label {
|
||||
color: var(--text-secondary) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
@ -151,6 +151,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; }
|
||||
.page-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
|
||||
.tab-header { margin-bottom: 12px; }
|
||||
</style>
|
||||
|
||||
@ -1,100 +1,110 @@
|
||||
<template>
|
||||
<div class="dashboard" v-loading="loading">
|
||||
<h2 class="page-title">仪表盘</h2>
|
||||
|
||||
<!-- 顶部统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon blue"><el-icon :size="20"><FolderOpened /></el-icon></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ data.active_projects || 0 }}</div>
|
||||
<div class="stat-label">进行中项目</div>
|
||||
<div class="stat-value blue">{{ data.active_projects }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-label">本月人力成本</div>
|
||||
<div class="stat-value orange">¥{{ formatNum(data.monthly_labor_cost) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-label">本月产出</div>
|
||||
<div class="stat-value green">{{ formatSecs(data.monthly_total_seconds) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-label">人均日产出</div>
|
||||
<div class="stat-value purple">{{ formatSecs(data.avg_daily_seconds_per_person) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 项目进度 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="section-title">项目进度一览</span>
|
||||
</template>
|
||||
<div v-for="p in data.projects" :key="p.id" class="project-row" @click="$router.push(`/projects/${p.id}`)">
|
||||
<div class="project-info">
|
||||
<span class="project-name">{{ p.name }}</span>
|
||||
<el-tag size="small" :type="typeTagMap[p.project_type]">{{ p.project_type }}</el-tag>
|
||||
<el-tag v-if="p.is_overdue" size="small" type="danger">超期</el-tag>
|
||||
</div>
|
||||
<div class="project-progress">
|
||||
<el-progress
|
||||
:percentage="Math.min(p.progress_percent, 100)"
|
||||
:color="p.is_overdue ? '#f56c6c' : '#67c23a'"
|
||||
:stroke-width="14"
|
||||
:text-inside="true"
|
||||
:format="() => p.progress_percent + '%'"
|
||||
/>
|
||||
<span class="progress-detail">{{ formatSecs(p.submitted_seconds) }} / {{ formatSecs(p.target_seconds) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!data.projects?.length" description="暂无进行中的项目" />
|
||||
</el-card>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon orange"><el-icon :size="20"><Money /></el-icon></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">¥{{ formatNum(data.monthly_labor_cost) }}</div>
|
||||
<div class="stat-label">本月人力成本</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon green"><el-icon :size="20"><VideoCamera /></el-icon></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ formatSecs(data.monthly_total_seconds) }}</div>
|
||||
<div class="stat-label">本月总产出</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon purple"><el-icon :size="20"><TrendCharts /></el-icon></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ formatSecs(data.avg_daily_seconds_per_person) }}</div>
|
||||
<div class="stat-label">人均日产出</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<!-- 项目进度 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">项目进度</span>
|
||||
<span class="card-count">{{ data.projects?.length || 0 }} 个进行中</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="p in data.projects" :key="p.id" class="progress-item" @click="$router.push(`/projects/${p.id}`)">
|
||||
<div class="progress-top">
|
||||
<div class="progress-info">
|
||||
<span class="progress-name">{{ p.name }}</span>
|
||||
<el-tag size="small" :type="typeTagMap[p.project_type]">{{ p.project_type }}</el-tag>
|
||||
<el-tag v-if="p.is_overdue" size="small" type="danger">超期</el-tag>
|
||||
</div>
|
||||
<span class="progress-pct">{{ p.progress_percent }}%</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="Math.min(p.progress_percent, 100)"
|
||||
:color="p.is_overdue ? '#FF3B30' : '#3370FF'"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
/>
|
||||
<div class="progress-meta">
|
||||
<span>{{ formatSecs(p.submitted_seconds) }} / {{ formatSecs(p.target_seconds) }}</span>
|
||||
<span v-if="p.waste_rate > 0" :style="{color: p.waste_rate > 30 ? '#FF3B30' : '#8F959E'}">损耗 {{ p.waste_rate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!data.projects?.length" description="暂无进行中的项目" :image-size="80" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two-col">
|
||||
<!-- 损耗排行 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="section-card">
|
||||
<template #header><span class="section-title">损耗排行</span></template>
|
||||
<el-table :data="data.waste_ranking" size="small" stripe>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">损耗排行</span></div>
|
||||
<div class="card-body">
|
||||
<el-table :data="data.waste_ranking" size="small">
|
||||
<el-table-column prop="project_name" label="项目" />
|
||||
<el-table-column label="损耗秒数" align="right">
|
||||
<el-table-column label="损耗" align="right" width="100">
|
||||
<template #default="{ row }">{{ formatSecs(row.waste_seconds) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="损耗率" align="right" width="100">
|
||||
<el-table-column label="损耗率" align="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.waste_rate > 30 ? '#f56c6c' : '#333' }">{{ row.waste_rate }}%</span>
|
||||
<span class="rate-badge" :class="{ danger: row.waste_rate > 30 }">{{ row.waste_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-empty v-if="!data.waste_ranking?.length" description="暂无数据" :image-size="60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已结算项目 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="section-card">
|
||||
<template #header><span class="section-title">已结算项目</span></template>
|
||||
<el-table :data="data.settled_projects" size="small" stripe>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">已结算项目</span></div>
|
||||
<div class="card-body">
|
||||
<el-table :data="data.settled_projects" size="small">
|
||||
<el-table-column prop="project_name" label="项目" />
|
||||
<el-table-column label="总成本" align="right">
|
||||
<el-table-column label="总成本" align="right" width="100">
|
||||
<template #default="{ row }">¥{{ formatNum(row.total_cost) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="盈亏" align="right">
|
||||
<el-table-column label="盈亏" align="right" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.profit_loss != null" :style="{ color: row.profit_loss >= 0 ? '#67c23a' : '#f56c6c', fontWeight: 600 }">
|
||||
<span v-if="row.profit_loss != null" class="profit" :class="{ loss: row.profit_loss < 0 }">
|
||||
{{ row.profit_loss >= 0 ? '+' : '' }}¥{{ formatNum(row.profit_loss) }}
|
||||
</span>
|
||||
<span v-else style="color:#999">—</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-if="!data.settled_projects?.length" description="暂无数据" :image-size="60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -104,12 +114,7 @@ import { dashboardApi } from '../api'
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref({})
|
||||
const typeTagMap = {
|
||||
'客户正式项目': 'success',
|
||||
'客户测试项目': 'warning',
|
||||
'内部原创项目': '',
|
||||
'内部测试项目': 'info',
|
||||
}
|
||||
const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' }
|
||||
|
||||
function formatNum(n) { return (n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 }) }
|
||||
function formatSecs(s) {
|
||||
@ -126,22 +131,85 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; }
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-card { text-align: center; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
|
||||
.stat-value { font-size: 28px; font-weight: 700; }
|
||||
.stat-value.blue { color: #409eff; }
|
||||
.stat-value.orange { color: #e6a23c; }
|
||||
.stat-value.green { color: #67c23a; }
|
||||
.stat-value.purple { color: #9b59b6; }
|
||||
.section-card { margin-bottom: 16px; }
|
||||
.section-title { font-weight: 600; }
|
||||
.project-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
|
||||
.project-row:hover { background: #fafafa; }
|
||||
.project-info { display: flex; align-items: center; gap: 8px; min-width: 260px; }
|
||||
.project-name { font-weight: 500; }
|
||||
.project-progress { flex: 1; display: flex; align-items: center; gap: 12px; }
|
||||
.project-progress .el-progress { flex: 1; }
|
||||
.progress-detail { font-size: 12px; color: #909399; white-space: nowrap; }
|
||||
/* 统计网格 */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.stat-icon {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stat-icon.blue { background: #E8F0FE; color: #3370FF; }
|
||||
.stat-icon.orange { background: #FFF3E0; color: #FF9500; }
|
||||
.stat-icon.green { background: #E8F8EE; color: #34C759; }
|
||||
.stat-icon.purple { background: #F0E8FE; color: #9B59B6; }
|
||||
.stat-body { flex: 1; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||
.stat-label { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
|
||||
.card-count { font-size: 12px; color: var(--text-secondary); }
|
||||
.card-body { padding: 16px 20px; }
|
||||
|
||||
/* 项目进度 */
|
||||
.progress-item {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin: 0 -20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.progress-item:last-child { border-bottom: none; }
|
||||
.progress-item:hover { background: #FAFBFC; }
|
||||
.progress-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.progress-info { display: flex; align-items: center; gap: 8px; }
|
||||
.progress-name { font-size: 14px; font-weight: 500; color: var(--text-primary); }
|
||||
.progress-pct { font-size: 14px; font-weight: 600; color: var(--primary); }
|
||||
.progress-meta {
|
||||
display: flex; justify-content: space-between;
|
||||
font-size: 12px; color: var(--text-secondary); margin-top: 6px;
|
||||
}
|
||||
|
||||
/* 两列布局 */
|
||||
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
|
||||
/* 标签 */
|
||||
.rate-badge {
|
||||
font-size: 12px; font-weight: 600; color: var(--text-secondary);
|
||||
background: var(--bg-hover); padding: 2px 8px; border-radius: 4px;
|
||||
}
|
||||
.rate-badge.danger { background: #FFE8E7; color: #FF3B30; }
|
||||
.profit { font-weight: 600; color: #34C759; }
|
||||
.profit.loss { color: #FF3B30; }
|
||||
.text-muted { color: var(--text-secondary); }
|
||||
</style>
|
||||
|
||||
@ -1,22 +1,33 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<el-icon :size="36" color="#409eff"><DataAnalysis /></el-icon>
|
||||
<div class="login-left">
|
||||
<div class="brand">
|
||||
<div class="brand-icon">A</div>
|
||||
<h1>AirLabs Project</h1>
|
||||
<p>内容组项目管理系统</p>
|
||||
<p>内容组 · 项目管理系统</p>
|
||||
</div>
|
||||
<div class="brand-features">
|
||||
<div class="feature"><span class="dot blue"></span>项目进度实时追踪</div>
|
||||
<div class="feature"><span class="dot green"></span>成本自动核算分摊</div>
|
||||
<div class="feature"><span class="dot orange"></span>损耗与效率分析</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-right">
|
||||
<div class="login-card">
|
||||
<h2>登录</h2>
|
||||
<p class="login-desc">使用你的账号登录系统</p>
|
||||
<el-form :model="form" @submit.prevent="handleLogin" class="login-form">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.username" placeholder="用户名" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" placeholder="密码" type="password" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="handleLogin" class="login-btn">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-form :model="form" @submit.prevent="handleLogin" class="login-form">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" placeholder="密码" prefix-icon="Lock" type="password" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="handleLogin" style="width:100%">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -40,7 +51,7 @@ async function handleLogin() {
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.login(form.username, form.password)
|
||||
ElMessage.success(`欢迎,${authStore.user?.name}`)
|
||||
ElMessage.success(`欢迎回来,${authStore.user?.name || ''}`)
|
||||
router.push('/')
|
||||
} catch {
|
||||
// 错误已在拦截器处理
|
||||
@ -54,22 +65,69 @@ async function handleLogin() {
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 左侧品牌区 */
|
||||
.login-left {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #3370FF 0%, #2451B8 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 80px;
|
||||
color: #fff;
|
||||
}
|
||||
.brand { margin-bottom: 48px; }
|
||||
.brand-icon {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px; font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.brand h1 { font-size: 32px; font-weight: 700; margin-bottom: 8px; }
|
||||
.brand p { font-size: 16px; opacity: 0.8; }
|
||||
.brand-features { display: flex; flex-direction: column; gap: 16px; }
|
||||
.feature {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
font-size: 15px; opacity: 0.9;
|
||||
}
|
||||
.dot {
|
||||
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.dot.blue { background: #7EB8FF; }
|
||||
.dot.green { background: #7DECA2; }
|
||||
.dot.orange { background: #FFB967; }
|
||||
|
||||
/* 右侧登录区 */
|
||||
.login-right {
|
||||
width: 480px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1d1e2c 0%, #2d3a5c 100%);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
.login-card {
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
width: 360px;
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
.login-card h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.login-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.login-header h1 { font-size: 24px; margin: 12px 0 4px; color: #1d1e2c; }
|
||||
.login-header p { color: #999; font-size: 14px; }
|
||||
.login-form { margin-top: 8px; }
|
||||
.login-form .el-form-item { margin-bottom: 20px; }
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 44px !important;
|
||||
font-size: 15px !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,91 +1,99 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<el-button text @click="$router.push('/projects')"><el-icon><ArrowLeft /></el-icon> 返回</el-button>
|
||||
<h2 style="display:inline; margin-left:8px">{{ project.name }}</h2>
|
||||
<el-tag :type="typeTagMap[project.project_type]" style="margin-left:8px">{{ project.project_type }}</el-tag>
|
||||
<el-tag :type="project.status === '已完成' ? 'success' : ''" style="margin-left:4px">{{ project.status }}</el-tag>
|
||||
<div class="page-header-left">
|
||||
<el-button text @click="$router.push('/projects')" class="back-btn"><el-icon><ArrowLeft /></el-icon></el-button>
|
||||
<h2>{{ project.name }}</h2>
|
||||
<el-tag :type="typeTagMap[project.project_type]" size="small">{{ project.project_type }}</el-tag>
|
||||
<el-tag :type="project.status === '已完成' ? 'success' : 'info'" size="small">{{ project.status }}</el-tag>
|
||||
</div>
|
||||
<el-space>
|
||||
<el-button v-if="authStore.isOwner() && project.status === '制作中'" type="danger" @click="handleComplete">
|
||||
确认完成结算
|
||||
</el-button>
|
||||
<el-button v-if="authStore.isOwner() && project.status === '已完成'" type="primary" @click="$router.push(`/settlement/${project.id}`)">
|
||||
查看结算报告
|
||||
</el-button>
|
||||
<el-button v-if="authStore.isOwner() && project.status === '制作中'" type="danger" plain @click="handleComplete">确认完成</el-button>
|
||||
<el-button v-if="authStore.isOwner() && project.status === '已完成'" type="primary" @click="$router.push(`/settlement/${project.id}`)">查看结算</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
|
||||
<!-- 项目概览 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover"><div class="stat-label">目标时长</div><div class="stat-value">{{ formatSecs(project.target_total_seconds) }}</div></el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover"><div class="stat-label">已提交</div><div class="stat-value">{{ formatSecs(project.total_submitted_seconds) }}</div></el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover"><div class="stat-label">完成进度</div><div class="stat-value" :style="{color: project.progress_percent > 100 ? '#e6a23c' : '#409eff'}">{{ project.progress_percent }}%</div></el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover"><div class="stat-label">损耗率</div><div class="stat-value" :style="{color: project.waste_rate > 30 ? '#f56c6c' : '#67c23a'}">{{ project.waste_rate }}%</div></el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 概览卡片 -->
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">目标时长</div>
|
||||
<div class="stat-value">{{ formatSecs(project.target_total_seconds) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">已提交</div>
|
||||
<div class="stat-value">{{ formatSecs(project.total_submitted_seconds) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">完成进度</div>
|
||||
<div class="stat-value" :style="{color: project.progress_percent > 100 ? '#FF9500' : '#3370FF'}">{{ project.progress_percent }}%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">损耗率</div>
|
||||
<div class="stat-value" :style="{color: project.waste_rate > 30 ? '#FF3B30' : '#34C759'}">{{ project.waste_rate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<el-card class="section-card">
|
||||
<template #header><span class="section-title">项目进度</span></template>
|
||||
<el-progress :percentage="Math.min(project.progress_percent, 100)" :stroke-width="20" :text-inside="true"
|
||||
:format="() => project.progress_percent + '%'"
|
||||
:color="progressColor" style="margin-bottom:12px" />
|
||||
<div class="meta-row">
|
||||
<span>目标:{{ project.episode_count }}集 × {{ project.episode_duration_minutes }}分 = {{ formatSecs(project.target_total_seconds) }}</span>
|
||||
<span v-if="project.estimated_completion_date">预估完成:{{ project.estimated_completion_date }}</span>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">项目进度</span></div>
|
||||
<div class="card-body">
|
||||
<el-progress :percentage="Math.min(project.progress_percent, 100)" :stroke-width="8" :show-text="false"
|
||||
:color="progressColor" style="margin-bottom:12px" />
|
||||
<div class="meta-row">
|
||||
<span>目标:{{ project.episode_count }}集 × {{ project.episode_duration_minutes }}分 = {{ formatSecs(project.target_total_seconds) }}</span>
|
||||
<span v-if="project.estimated_completion_date">预估完成:{{ project.estimated_completion_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 团队效率 -->
|
||||
<el-card v-if="authStore.isLeaderOrAbove() && efficiency.length" class="section-card">
|
||||
<template #header><span class="section-title">团队效率(人均基准对比)</span></template>
|
||||
<el-table :data="efficiency" size="small" stripe>
|
||||
<el-table-column prop="user_name" label="成员" width="100" />
|
||||
<el-table-column label="提交总秒数" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
|
||||
<el-table-column label="人均基准" align="right"><template #default="{row}">{{ formatSecs(row.baseline) }}</template></el-table-column>
|
||||
<el-table-column label="超出基准" align="right">
|
||||
<template #default="{row}">
|
||||
<span :style="{color: row.excess_seconds > 0 ? '#f56c6c' : '#67c23a'}">
|
||||
{{ row.excess_seconds > 0 ? '+' : '' }}{{ formatSecs(row.excess_seconds) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="超出比例" align="right" width="100">
|
||||
<template #default="{row}">
|
||||
<span :style="{color: row.excess_rate > 20 ? '#f56c6c' : '#333'}">{{ row.excess_rate > 0 ? '+' : '' }}{{ row.excess_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<div v-if="authStore.isLeaderOrAbove() && efficiency.length" class="card">
|
||||
<div class="card-header"><span class="card-title">团队效率</span></div>
|
||||
<div class="card-body">
|
||||
<el-table :data="efficiency" size="small">
|
||||
<el-table-column prop="user_name" label="成员" width="100" />
|
||||
<el-table-column label="提交总量" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
|
||||
<el-table-column label="人均基准" align="right"><template #default="{row}">{{ formatSecs(row.baseline) }}</template></el-table-column>
|
||||
<el-table-column label="超出基准" align="right">
|
||||
<template #default="{row}">
|
||||
<span :style="{color: row.excess_seconds > 0 ? '#FF3B30' : '#34C759', fontWeight: 600}">
|
||||
{{ row.excess_seconds > 0 ? '+' : '' }}{{ formatSecs(row.excess_seconds) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="超出比例" align="right" width="100">
|
||||
<template #default="{row}">
|
||||
<span class="rate-badge" :class="{danger: row.excess_rate > 20}">{{ row.excess_rate > 0 ? '+' : '' }}{{ row.excess_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交记录 -->
|
||||
<el-card class="section-card">
|
||||
<template #header><span class="section-title">提交记录</span></template>
|
||||
<el-table :data="submissions" size="small" stripe>
|
||||
<el-table-column prop="submit_date" label="日期" width="110" />
|
||||
<el-table-column prop="user_name" label="提交人" width="80" />
|
||||
<el-table-column prop="project_phase" label="阶段" width="70" />
|
||||
<el-table-column prop="work_type" label="工作类型" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.work_type === '测试' ? 'warning' : ''" size="small">{{ row.work_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_type" label="内容类型" width="90" />
|
||||
<el-table-column label="产出时长" width="90" align="right">
|
||||
<template #default="{row}">{{ row.total_seconds > 0 ? formatSecs(row.total_seconds) : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">提交记录</span>
|
||||
<span class="card-count">{{ submissions.length }} 条</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-table :data="submissions" size="small">
|
||||
<el-table-column prop="submit_date" label="日期" width="110" />
|
||||
<el-table-column prop="user_name" label="提交人" width="80" />
|
||||
<el-table-column prop="project_phase" label="阶段" width="70" />
|
||||
<el-table-column label="工作类型" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : ''" size="small">{{ row.work_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_type" label="内容类型" width="90" />
|
||||
<el-table-column label="产出时长" width="90" align="right">
|
||||
<template #default="{row}">{{ row.total_seconds > 0 ? formatSecs(row.total_seconds) : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -105,10 +113,7 @@ const submissions = ref([])
|
||||
const efficiency = ref([])
|
||||
const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' }
|
||||
|
||||
const progressColor = computed(() => {
|
||||
if (project.value.progress_percent > 100) return '#e6a23c'
|
||||
return '#67c23a'
|
||||
})
|
||||
const progressColor = computed(() => project.value.progress_percent > 100 ? '#FF9500' : '#3370FF')
|
||||
|
||||
function formatSecs(s) {
|
||||
if (!s) return '0秒'
|
||||
@ -144,11 +149,34 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-bottom: 4px; }
|
||||
.stat-value { font-size: 24px; font-weight: 700; }
|
||||
.section-card { margin-bottom: 16px; }
|
||||
.section-title { font-weight: 600; }
|
||||
.meta-row { display: flex; justify-content: space-between; font-size: 13px; color: #909399; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-header-left { display: flex; align-items: center; gap: 8px; }
|
||||
.page-header-left h2 { font-size: 18px; font-weight: 600; }
|
||||
.back-btn { font-size: 16px !important; padding: 4px !important; }
|
||||
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px; }
|
||||
.stat-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md); padding: 20px; text-align: center;
|
||||
}
|
||||
.stat-label { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: var(--text-primary); }
|
||||
|
||||
.card {
|
||||
background: var(--bg-card); border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md); margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
padding: 16px 20px; border-bottom: 1px solid var(--border-light);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 600; }
|
||||
.card-count { font-size: 12px; color: var(--text-secondary); }
|
||||
.card-body { padding: 20px; }
|
||||
.meta-row { display: flex; justify-content: space-between; font-size: 13px; color: var(--text-secondary); }
|
||||
.rate-badge {
|
||||
font-size: 12px; font-weight: 600; color: var(--text-secondary);
|
||||
background: var(--bg-hover); padding: 2px 8px; border-radius: 4px; display: inline-block;
|
||||
}
|
||||
.rate-badge.danger { background: #FFE8E7; color: #FF3B30; }
|
||||
</style>
|
||||
|
||||
@ -1,59 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>项目管理</h2>
|
||||
<div></div>
|
||||
<el-button v-if="authStore.isSupervisor()" type="primary" @click="showCreate = true">
|
||||
<el-icon><Plus /></el-icon> 新建项目
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<el-card class="filter-card">
|
||||
<el-space>
|
||||
<el-select v-model="filter.status" placeholder="状态" clearable style="width:130px" @change="load">
|
||||
<el-option label="制作中" value="制作中" />
|
||||
<el-option label="已完成" value="已完成" />
|
||||
</el-select>
|
||||
<el-select v-model="filter.project_type" placeholder="项目类型" clearable style="width:160px" @change="load">
|
||||
<el-option v-for="t in projectTypes" :key="t" :label="t" :value="t" />
|
||||
</el-select>
|
||||
</el-space>
|
||||
</el-card>
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="filter.status" placeholder="状态" clearable style="width:120px" @change="load">
|
||||
<el-option label="制作中" value="制作中" />
|
||||
<el-option label="已完成" value="已完成" />
|
||||
</el-select>
|
||||
<el-select v-model="filter.project_type" placeholder="项目类型" clearable style="width:150px" @change="load">
|
||||
<el-option v-for="t in projectTypes" :key="t" :label="t" :value="t" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<el-table :data="projects" v-loading="loading" stripe @row-click="row => $router.push(`/projects/${row.id}`)">
|
||||
<el-table-column prop="name" label="项目名称" min-width="160" />
|
||||
<el-table-column label="类型" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="typeTagMap[row.project_type]">{{ row.project_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.status === '已完成' ? 'success' : ''">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="leader_name" label="负责人" width="100" />
|
||||
<el-table-column label="目标" width="140">
|
||||
<template #default="{ row }">{{ row.episode_count }}集 × {{ row.episode_duration_minutes }}分</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Math.min(row.progress_percent, 100)" :stroke-width="10" :text-inside="true"
|
||||
:format="() => row.progress_percent + '%'"
|
||||
:color="row.progress_percent > 100 ? '#e6a23c' : '#409eff'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="损耗率" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.waste_rate > 30 ? '#f56c6c' : '#333' }">{{ row.waste_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<el-table :data="projects" v-loading="loading" @row-click="row => $router.push(`/projects/${row.id}`)" style="cursor:pointer">
|
||||
<el-table-column prop="name" label="项目名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="cell-bold">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="typeTagMap[row.project_type]">{{ row.project_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.status === '已完成' ? 'success' : 'info'">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="leader_name" label="负责人" width="90" />
|
||||
<el-table-column label="目标" width="130">
|
||||
<template #default="{ row }">{{ row.episode_count }}集 × {{ row.episode_duration_minutes }}分</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-progress">
|
||||
<el-progress :percentage="Math.min(row.progress_percent, 100)" :stroke-width="6" :show-text="false"
|
||||
:color="row.progress_percent > 100 ? '#FF9500' : '#3370FF'" />
|
||||
<span class="progress-text">{{ row.progress_percent }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="损耗率" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="rate-badge" :class="{ danger: row.waste_rate > 30 }">{{ row.waste_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建项目对话框 -->
|
||||
<el-dialog v-model="showCreate" title="新建项目" width="560px" destroy-on-close>
|
||||
<el-form :model="form" label-width="120px" label-position="left">
|
||||
<el-form :model="form" label-width="110px" label-position="left">
|
||||
<el-form-item label="项目名称" required>
|
||||
<el-input v-model="form.name" placeholder="输入项目名称" />
|
||||
</el-form-item>
|
||||
@ -141,6 +149,19 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.page-header h2 { font-size: 20px; }
|
||||
.filter-card { margin-bottom: 16px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.card {
|
||||
background: var(--bg-card); border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.card-body { padding: 4px 0; }
|
||||
.cell-bold { font-weight: 500; color: var(--text-primary); }
|
||||
.cell-progress { display: flex; align-items: center; gap: 8px; }
|
||||
.cell-progress .el-progress { flex: 1; }
|
||||
.progress-text { font-size: 12px; font-weight: 600; color: var(--text-secondary); min-width: 36px; text-align: right; }
|
||||
.rate-badge {
|
||||
font-size: 12px; font-weight: 600; color: var(--text-secondary);
|
||||
background: var(--bg-hover); padding: 2px 8px; border-radius: 4px; display: inline-block;
|
||||
}
|
||||
.rate-badge.danger { background: #FFE8E7; color: #FF3B30; }
|
||||
</style>
|
||||
|
||||
@ -121,11 +121,11 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header { margin-bottom: 16px; }
|
||||
.page-header { margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
|
||||
.section-card { margin-bottom: 16px; }
|
||||
.section-title { font-weight: 600; }
|
||||
.section-title { font-weight: 600; font-size: 14px; }
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-bottom: 4px; }
|
||||
.stat-value { font-size: 24px; font-weight: 700; }
|
||||
.big-stat { text-align: center; padding: 12px 0; }
|
||||
.stat-label { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: var(--text-primary); }
|
||||
.big-stat { text-align: center; padding: 16px 0; }
|
||||
</style>
|
||||
|
||||
@ -175,6 +175,6 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.page-header h2 { font-size: 20px; }
|
||||
.filter-card { margin-bottom: 16px; }
|
||||
.page-header h2 { font-size: 18px; font-weight: 600; }
|
||||
.filter-card { margin-bottom: 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 16px; }
|
||||
</style>
|
||||
|
||||
@ -97,5 +97,5 @@ onMounted(load)
|
||||
|
||||
<style scoped>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.page-header h2 { font-size: 20px; }
|
||||
.page-header h2 { font-size: 18px; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user