135 lines
6.4 KiB
Vue
135 lines
6.4 KiB
Vue
<template>
|
|
<div v-loading="loading">
|
|
<div class="page-header">
|
|
<el-button text @click="$router.back()"><el-icon><ArrowLeft /></el-icon> 返回</el-button>
|
|
<h2 style="display:inline;margin-left:8px">项目结算报告</h2>
|
|
</div>
|
|
|
|
<template v-if="data.project_name">
|
|
<!-- 项目基本信息 -->
|
|
<el-card class="section-card">
|
|
<template #header><span class="section-title">{{ data.project_name }}</span></template>
|
|
<el-descriptions :column="3" border size="small">
|
|
<el-descriptions-item label="项目类型">{{ data.project_type }}</el-descriptions-item>
|
|
<el-descriptions-item label="目标时长">{{ formatSecs(data.target_seconds) }}</el-descriptions-item>
|
|
<el-descriptions-item label="实际提交">{{ formatSecs(data.total_submitted_seconds) }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-card>
|
|
|
|
<!-- 成本汇总 -->
|
|
<el-row :gutter="16" class="stat-row">
|
|
<el-col :span="5">
|
|
<el-card shadow="hover"><div class="stat-label">人力成本</div><div class="stat-value">¥{{ fmt(data.labor_cost) }}</div></el-card>
|
|
</el-col>
|
|
<el-col :span="5">
|
|
<el-card shadow="hover"><div class="stat-label">AI 工具成本</div><div class="stat-value">¥{{ fmt(data.ai_tool_cost) }}</div></el-card>
|
|
</el-col>
|
|
<el-col :span="4">
|
|
<el-card shadow="hover"><div class="stat-label">外包成本</div><div class="stat-value">¥{{ fmt(data.outsource_cost) }}</div></el-card>
|
|
</el-col>
|
|
<el-col :span="5">
|
|
<el-card shadow="hover"><div class="stat-label">固定开支</div><div class="stat-value">¥{{ fmt(data.overhead_cost) }}</div></el-card>
|
|
</el-col>
|
|
<el-col :span="5">
|
|
<el-card shadow="hover">
|
|
<div class="stat-label">项目总成本</div>
|
|
<div class="stat-value" style="color:#e6a23c">¥{{ fmt(data.total_cost) }}</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<!-- 盈亏(仅客户正式项目) -->
|
|
<el-card v-if="data.contract_amount != null" class="section-card">
|
|
<template #header><span class="section-title">项目盈亏</span></template>
|
|
<el-row :gutter="16">
|
|
<el-col :span="8">
|
|
<div class="big-stat"><div class="stat-label">合同金额</div><div class="stat-value" style="color:#409eff">¥{{ fmt(data.contract_amount) }}</div></div>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<div class="big-stat"><div class="stat-label">项目总成本</div><div class="stat-value" style="color:#e6a23c">¥{{ fmt(data.total_cost) }}</div></div>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<div class="big-stat">
|
|
<div class="stat-label">盈亏结果</div>
|
|
<div class="stat-value" :style="{color: data.profit_loss >= 0 ? '#67c23a' : '#f56c6c'}">
|
|
{{ data.profit_loss >= 0 ? '+' : '' }}¥{{ fmt(data.profit_loss) }}
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
</el-card>
|
|
|
|
<!-- 损耗分析 -->
|
|
<el-card class="section-card">
|
|
<template #header><span class="section-title">损耗分析</span></template>
|
|
<el-descriptions :column="2" border size="small">
|
|
<el-descriptions-item label="测试损耗">{{ formatSecs(data.test_waste_seconds) }}</el-descriptions-item>
|
|
<el-descriptions-item label="超产损耗">{{ formatSecs(data.overproduction_waste_seconds) }}</el-descriptions-item>
|
|
<el-descriptions-item label="总损耗">{{ formatSecs(data.total_waste_seconds) }}</el-descriptions-item>
|
|
<el-descriptions-item label="损耗率">
|
|
<span :style="{color: data.waste_rate > 30 ? '#f56c6c' : '#333', fontWeight:600}">{{ data.waste_rate }}%</span>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-card>
|
|
|
|
<!-- 团队效率 -->
|
|
<el-card v-if="data.team_efficiency?.length" class="section-card">
|
|
<template #header><span class="section-title">团队效率排行</span></template>
|
|
<el-table :data="data.team_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-column prop="submission_count" label="提交次数" width="90" align="right" />
|
|
</el-table>
|
|
</el-card>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { projectApi } from '../api'
|
|
|
|
const route = useRoute()
|
|
const loading = ref(false)
|
|
const data = ref({})
|
|
|
|
function fmt(n) { return (n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 }) }
|
|
function formatSecs(s) {
|
|
if (!s) return '0秒'
|
|
const abs = Math.abs(s)
|
|
const m = Math.floor(abs / 60)
|
|
const sec = Math.round(abs % 60)
|
|
const sign = s < 0 ? '-' : ''
|
|
return m > 0 ? `${sign}${m}分${sec > 0 ? sec + '秒' : ''}` : `${sign}${sec}秒`
|
|
}
|
|
|
|
onMounted(async () => {
|
|
loading.value = true
|
|
try { data.value = await projectApi.settlement(route.params.id) } finally { loading.value = false }
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page-header { margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
|
|
.section-card { margin-bottom: 16px; }
|
|
.section-title { font-weight: 600; font-size: 14px; }
|
|
.stat-row { margin-bottom: 16px; }
|
|
.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>
|