airlabs-manage/frontend/src/views/Settlement.vue
seaislee1209 087d4e1a6b
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 5m44s
Build and Deploy Web / build-and-deploy (push) Successful in 3m24s
feat: 新增「修改」「QC」工作类型 + EP集数追踪
- 工作类型新增「修改」(返工)和「QC」(质量审核),支持组长多角色工作记录
- 提交表单支持按集数(EP)归类,项目详情展示各集进度
- 热力图增加修改(橙色)、QC(绿色)颜色区分和图例
- 效率算法优化:产出按小时计算,损耗统计更精准
- 数据库自动迁移:episode_number字段 + work_type ENUM扩展

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:18:47 +08:00

180 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>