seaislee1209 bc06725ed1
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m22s
Build and Deploy Web / build-and-deploy (push) Successful in 51s
style: UI鍏ㄩ潰鍗囩骇涓洪涔﹂鏍?- 鐧藉簳钃濊壊涓昏壊璋?娓呯埥涓撲笟
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 15:07:54 +08:00

216 lines
8.1 KiB
Vue

<template>
<div class="dashboard" v-loading="loading">
<!-- 顶部统计卡片 -->
<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>
</div>
<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>
<!-- 项目进度 -->
<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">
<!-- 损耗排行 -->
<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" width="100">
<template #default="{ row }">{{ formatSecs(row.waste_seconds) }}</template>
</el-table-column>
<el-table-column label="损耗率" align="right" width="90">
<template #default="{ row }">
<span class="rate-badge" :class="{ danger: row.waste_rate > 30 }">{{ row.waste_rate }}%</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!data.waste_ranking?.length" description="暂无数据" :image-size="60" />
</div>
</div>
<!-- 已结算项目 -->
<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" width="100">
<template #default="{ row }">¥{{ formatNum(row.total_cost) }}</template>
</el-table-column>
<el-table-column label="盈亏" align="right" width="100">
<template #default="{ row }">
<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 class="text-muted"></span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!data.settled_projects?.length" description="暂无数据" :image-size="60" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { dashboardApi } from '../api'
const loading = ref(false)
const data = ref({})
const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' }
function formatNum(n) { return (n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 }) }
function formatSecs(s) {
if (!s) return '0秒'
const m = Math.floor(s / 60)
const sec = Math.round(s % 60)
return m > 0 ? `${m}${sec > 0 ? sec + '秒' : ''}` : `${sec}`
}
onMounted(async () => {
loading.value = true
try { data.value = await dashboardApi.get() } finally { loading.value = false }
})
</script>
<style scoped>
/* 统计网格 */
.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>