Files
cz-web/src/views/project/detail.vue
2026-10-09 14:20:20 +08:00

252 lines
6.4 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.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ConfirmDialog } from '@/components'
import type { FileInfo } from '@/types/api'
import { useProjectStore } from '@/stores/modules/project'
import ProjectInfoCard from './cpns/ProjectInfoCard.vue'
import ProjectProgress from './cpns/ProjectProgress.vue'
import ProjectMilestones from './cpns/ProjectMilestones.vue'
import ProjectTeam from './cpns/ProjectTeam.vue'
import ProjectFiles from './cpns/ProjectFiles.vue'
import ProjectFinance from './cpns/ProjectFinance.vue'
import ProjectAfterSales from './cpns/ProjectAfterSales.vue'
type DetailTab = 'progress' | 'files' | 'finance' | 'afterSales'
const route = useRoute()
const router = useRouter()
const projectStore = useProjectStore()
const { current, loadingTab } = storeToRefs(projectStore)
const projectId = computed(() => String(route.params.id))
const activeTab = ref<DetailTab>('progress')
const cancelVisible = ref(false)
const tabs: Array<{ label: string; value: DetailTab }> = [
{ label: '项目进度', value: 'progress' },
{ label: '项目文件', value: 'files' },
{ label: '合同财务', value: 'finance' },
{ label: '售后信息', value: 'afterSales' },
]
function load(): void {
void projectStore.fetchDetail(projectId.value)
}
function onTabClick(tab: DetailTab): void {
activeTab.value = tab
void projectStore.refreshTab(tab)
}
function onDeleteFile(index: number): void {
void index
ElMessage.info('附件删除接口待接入')
}
function onPreviewFile(): void {
// TODO(api): 文件预览接口
ElMessage.info('文件预览待接口接入')
}
function onDownloadFile(): void {
// TODO(api): 文件下载接口
ElMessage.info('文件下载待接口接入')
}
function onPay(billId: string): void {
const id = String(billId ?? '').trim()
if (!/^\d+$/.test(id)) {
ElMessage.warning('该期账单尚未生成,暂时无法支付')
return
}
void router.push({
path: '/finance/checkout',
query: { requirementId: projectId.value, ids: id },
})
}
function onFinancePreview(file: FileInfo): void {
// TODO(api): 发票/合同文件预览接口
ElMessage.info(`「${file.name}」预览待接口接入`)
}
function onFinanceDownload(file: FileInfo): void {
// TODO(api): 发票/合同文件下载接口
ElMessage.info(`「${file.name}」下载待接口接入`)
}
function onApplyAfterSales(): void {
void router.push(`/project/${projectId.value}/after-sales`)
}
function onConfirmCancel(): void {
void projectStore.cancelAfterSales(projectId.value)
}
watch(projectId, load)
onMounted(load)
</script>
<template>
<div class="container project-detail">
<nav class="project-detail__breadcrumb text-secondary">
<RouterLink to="/project" class="link">
项目中心
</RouterLink>
<span> &gt; </span>
<span>项目详情</span>
</nav>
<template v-if="current">
<div class="project-detail__info">
<ProjectInfoCard :project="current" />
</div>
<div class="project-detail__tabs">
<button
v-for="tab in tabs"
:key="tab.value"
class="project-detail__tab"
:class="{
'is-active': activeTab === tab.value,
'is-loading': loadingTab === tab.value,
}"
@click="onTabClick(tab.value)"
>
{{ tab.label }}
</button>
</div>
<template v-if="activeTab === 'progress'">
<ProjectProgress
:progress="current.progress"
:start-time="current.startTime"
:end-time="current.endTime"
/>
<ProjectMilestones :milestones="current.milestones" />
<ProjectTeam :team="current.team" />
</template>
<ProjectFiles
v-if="activeTab === 'files'"
:files="current.files"
@delete="onDeleteFile"
@preview="onPreviewFile"
@download="onDownloadFile"
/>
<ProjectFinance
v-if="activeTab === 'finance'"
:payment-plans="current.paymentPlans"
:invoice-records="current.invoiceRecords"
:contract-file="current.contractFile"
@pay="onPay"
@preview="onFinancePreview"
@download="onFinanceDownload"
/>
<ProjectAfterSales
v-if="activeTab === 'afterSales'"
:after-sales="current.afterSales"
:project-status="current.status"
@apply="onApplyAfterSales"
@cancel="cancelVisible = true"
/>
</template>
<p v-else class="project-detail__empty text-secondary">
项目不存在或已取消
</p>
<ConfirmDialog
v-model:visible="cancelVisible"
title="取消售后"
message="确定取消当前售后工单吗?"
confirm-text="确定取消"
danger
@confirm="onConfirmCancel"
/>
</div>
</template>
<style lang="less" scoped>
.project-detail {
.flex(column, flex-start, stretch);
gap: 0;
padding-bottom: @spacing-page-bottom;
background: @white;
// 设计稿 el55 面包屑 y=116,header 底 72 → padding-top 44px。
&__breadcrumb {
padding-top: 44px;
font-size: 24px;
font-weight: 500;
line-height: 27px;
color: @project-breadcrumb;
}
// 设计稿 el51 logo 盒 y=195(面包屑盒底 = 72+44+27 = 143)→ 间距 52px。
&__info {
margin-top: 52px;
}
// 设计稿 el42 tab 文字 y=264,meta 文字底 231+15=246。
&__tabs {
.flex(row, flex-start, center);
gap: 16.45px;
height: 41px;
// 信息卡底 195+67=262,tab 文字顶 264 → tabs 盒顶 264,margin 2px。
margin-top: 2px;
box-sizing: border-box;
border-bottom: 1px solid @project-divider;
}
&__tab {
position: relative;
padding: 0 0 24px;
border: none;
background: transparent;
color: @project-cell;
font-size: 13px;
font-weight: 400;
line-height: 17px;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: @project-accent;
}
&.is-loading {
opacity: 0.6;
cursor: progress;
}
&.is-active {
color: @project-accent;
font-weight: 600;
&::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: @project-accent;
}
}
}
&__empty {
padding: @spacing-xxl 0;
text-align: center;
font-size: @font-size-body;
}
}
</style>