airlabs-manage/frontend/src/views/ProjectDetail.vue
2026-02-12 14:24:05 +08:00

155 lines
7.1 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">
<div>
<el-button text @click="$router.push('/projects')"><el-icon><ArrowLeft /></el-icon> 返回</el-button>
<h2 style="display:inline; margin-left:8px">{{ project.name }}</h2>
<el-tag :type="typeTagMap[project.project_type]" style="margin-left:8px">{{ project.project_type }}</el-tag>
<el-tag :type="project.status === '已完成' ? 'success' : ''" style="margin-left:4px">{{ project.status }}</el-tag>
</div>
<el-space>
<el-button v-if="authStore.isOwner() && project.status === '制作中'" type="danger" @click="handleComplete">
确认完成结算
</el-button>
<el-button v-if="authStore.isOwner() && project.status === '已完成'" type="primary" @click="$router.push(`/settlement/${project.id}`)">
查看结算报告
</el-button>
</el-space>
</div>
<!-- 项目概览 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="6">
<el-card shadow="hover"><div class="stat-label">目标时长</div><div class="stat-value">{{ formatSecs(project.target_total_seconds) }}</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-label">已提交</div><div class="stat-value">{{ formatSecs(project.total_submitted_seconds) }}</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-label">完成进度</div><div class="stat-value" :style="{color: project.progress_percent > 100 ? '#e6a23c' : '#409eff'}">{{ project.progress_percent }}%</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-label">损耗率</div><div class="stat-value" :style="{color: project.waste_rate > 30 ? '#f56c6c' : '#67c23a'}">{{ project.waste_rate }}%</div></el-card>
</el-col>
</el-row>
<!-- 进度条 -->
<el-card class="section-card">
<template #header><span class="section-title">项目进度</span></template>
<el-progress :percentage="Math.min(project.progress_percent, 100)" :stroke-width="20" :text-inside="true"
:format="() => project.progress_percent + '%'"
:color="progressColor" style="margin-bottom:12px" />
<div class="meta-row">
<span>目标{{ project.episode_count }} × {{ project.episode_duration_minutes }} = {{ formatSecs(project.target_total_seconds) }}</span>
<span v-if="project.estimated_completion_date">预估完成{{ project.estimated_completion_date }}</span>
</div>
</el-card>
<!-- 团队效率 -->
<el-card v-if="authStore.isLeaderOrAbove() && efficiency.length" class="section-card">
<template #header><span class="section-title">团队效率人均基准对比</span></template>
<el-table :data="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>
</el-card>
<!-- 提交记录 -->
<el-card class="section-card">
<template #header><span class="section-title">提交记录</span></template>
<el-table :data="submissions" size="small" stripe>
<el-table-column prop="submit_date" label="日期" width="110" />
<el-table-column prop="user_name" label="提交人" width="80" />
<el-table-column prop="project_phase" label="阶段" width="70" />
<el-table-column prop="work_type" label="工作类型" width="80">
<template #default="{row}">
<el-tag :type="row.work_type === '测试' ? 'warning' : ''" size="small">{{ row.work_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="content_type" label="内容类型" width="90" />
<el-table-column label="产出时长" width="90" align="right">
<template #default="{row}">{{ row.total_seconds > 0 ? formatSecs(row.total_seconds) : '—' }}</template>
</el-table-column>
<el-table-column prop="description" label="描述" show-overflow-tooltip />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { projectApi, submissionApi } from '../api'
import { useAuthStore } from '../stores/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
const project = ref({})
const submissions = ref([])
const efficiency = ref([])
const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' }
const progressColor = computed(() => {
if (project.value.progress_percent > 100) return '#e6a23c'
return '#67c23a'
})
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}`
}
async function load() {
loading.value = true
try {
const id = route.params.id
project.value = await projectApi.get(id)
submissions.value = await submissionApi.list({ project_id: id })
if (authStore.isLeaderOrAbove()) {
try { efficiency.value = await projectApi.efficiency(id) } catch {}
}
} finally { loading.value = false }
}
async function handleComplete() {
try {
await ElMessageBox.confirm('确认将此项目标记为完成并进行结算?此操作不可撤销。', '确认完成', { type: 'warning' })
await projectApi.complete(route.params.id)
ElMessage.success('项目已完成')
load()
} catch {}
}
onMounted(load)
</script>
<style scoped>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.stat-row { margin-bottom: 16px; }
.stat-label { font-size: 13px; color: #909399; margin-bottom: 4px; }
.stat-value { font-size: 24px; font-weight: 700; }
.section-card { margin-bottom: 16px; }
.section-title { font-weight: 600; }
.meta-row { display: flex; justify-content: space-between; font-size: 13px; color: #909399; }
</style>