216 lines
8.1 KiB
Vue
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>
|