252 lines
6.4 KiB
Vue
252 lines
6.4 KiB
Vue
<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> > </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>
|