- 工作类型新增「修改」(返工)和「QC」(质量审核),支持组长多角色工作记录 - 提交表单支持按集数(EP)归类,项目详情展示各集进度 - 热力图增加修改(橙色)、QC(绿色)颜色区分和图例 - 效率算法优化:产出按小时计算,损耗统计更精准 - 数据库自动迁移:episode_number字段 + work_type ENUM扩展 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
9.0 KiB
Vue
180 lines
9.0 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 && authStore.hasPermission('project:view_contract')" 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-row :gutter="16" class="waste-phase-row">
|
||
<el-col :span="8">
|
||
<div class="waste-phase-card">
|
||
<div class="waste-phase-title">前期(工时制)</div>
|
||
<div class="waste-phase-value" :class="{ danger: preWasteHours > 0 }">{{ preWasteHours }}h</div>
|
||
<div class="waste-phase-detail" v-for="d in (data.pre_waste?.details || [])" :key="d.milestone">
|
||
{{ d.milestone }}:预估{{ d.estimated_days }}天 / 实际{{ d.actual_days }}天,超{{ d.overrun_days }}天 = {{ d.waste_hours }}h
|
||
</div>
|
||
<div v-if="!(data.pre_waste?.details || []).length" class="waste-phase-empty">无超期</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="waste-phase-card">
|
||
<div class="waste-phase-title">制作(秒数制)</div>
|
||
<div class="waste-phase-sub">
|
||
<span>测试损耗</span>
|
||
<span :class="{ danger: data.test_waste_seconds > 0 }">{{ formatSecs(data.test_waste_seconds) }}</span>
|
||
</div>
|
||
<div class="waste-phase-sub">
|
||
<span>超产损耗</span>
|
||
<span :class="{ danger: data.overproduction_waste_seconds > 0 }">{{ formatSecs(data.overproduction_waste_seconds) }}</span>
|
||
</div>
|
||
<div class="waste-phase-sub" style="font-weight:600; border-top:1px solid #eee; padding-top:6px; margin-top:4px">
|
||
<span>秒数损耗率</span>
|
||
<span :class="{ danger: data.waste_rate > 30 }">{{ data.waste_rate }}%</span>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="waste-phase-card">
|
||
<div class="waste-phase-title">后期(工时制)</div>
|
||
<div class="waste-phase-value" :class="{ danger: postWasteHours > 0 }">{{ postWasteHours }}h</div>
|
||
<div class="waste-phase-detail" v-for="d in (data.post_waste?.details || [])" :key="d.milestone">
|
||
{{ d.milestone }}:预估{{ d.estimated_days }}天 / 实际{{ d.actual_days }}天,超{{ d.overrun_days }}天 = {{ d.waste_hours }}h
|
||
</div>
|
||
<div v-if="!(data.post_waste?.details || []).length" class="waste-phase-empty">无超期</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</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" width="70"><template #default="{row}">{{ row.total_hours }}h</template></el-table-column>
|
||
<el-table-column label="日均产出" align="right"><template #default="{row}"><strong>{{ formatSecs(row.daily_avg) }}</strong></template></el-table-column>
|
||
<el-table-column label="效率" align="right" width="100">
|
||
<template #default="{row}">
|
||
<span :style="{color: row.efficiency_rate >= 0 ? '#34C759' : '#FF3B30', fontWeight: 600}">
|
||
{{ row.efficiency_rate > 0 ? '+' : '' }}{{ row.efficiency_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, computed, onMounted } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { projectApi } from '../api'
|
||
import { useAuthStore } from '../stores/auth'
|
||
|
||
const authStore = useAuthStore()
|
||
|
||
const route = useRoute()
|
||
const loading = ref(false)
|
||
const data = ref({})
|
||
|
||
const preWasteHours = computed(() => data.value.pre_waste?.waste_hours || 0)
|
||
const postWasteHours = computed(() => data.value.post_waste?.days_waste_hours || 0)
|
||
|
||
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; }
|
||
|
||
/* 分阶段损耗 */
|
||
.waste-phase-row { margin-top: 4px; }
|
||
.waste-phase-card { background: #F7F8FA; border-radius: 8px; padding: 14px 16px; min-height: 120px; }
|
||
.waste-phase-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
|
||
.waste-phase-value { font-size: 20px; font-weight: 700; color: var(--text-primary); margin-bottom: 6px; }
|
||
.waste-phase-value.danger { color: #f56c6c; }
|
||
.waste-phase-sub { display: flex; justify-content: space-between; font-size: 13px; color: var(--text-primary); padding: 3px 0; }
|
||
.waste-phase-sub .danger { color: #f56c6c; font-weight: 600; }
|
||
.waste-phase-detail { font-size: 12px; color: var(--text-secondary); line-height: 1.6; }
|
||
.waste-phase-empty { font-size: 12px; color: #C0C4CC; }
|
||
</style>
|