项目中心

This commit is contained in:
wangzzpeng
2026-09-28 17:47:07 +08:00
parent 43c2dc0d8d
commit b014fffacf
23 changed files with 1630 additions and 1577 deletions

View File

@@ -8,8 +8,9 @@ import type { FileInfo } from '@/types/api'
// 页面容器:数据只经 store,不直接 import api
import { useProjectStore } from '@/stores/modules/project'
import ProjectInfoCard from './cpns/ProjectInfoCard.vue'
import ProjectTeam from './cpns/ProjectTeam.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'
@@ -19,12 +20,13 @@ type DetailTab = 'progress' | 'files' | 'finance' | 'afterSales'
const route = useRoute()
const router = useRouter()
const projectStore = useProjectStore()
const { current } = storeToRefs(projectStore)
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' },
@@ -37,16 +39,26 @@ function load(): void {
}
/**
* ⚠️ 上传 / 删除附件、售后提交都还没有后端实现
* (`ProjectRequirementUserController` 只有 create / list / get / serviceTypeList)。
* 所以这里只提示「待接入」,**不往详情里塞本地假数据** —— 详情数据现在来自真实接口,
* 混入前端自造条目会让刷新前后不一致。
* 点击页签:**每次都调一次接口**,取该页签的最新数据。
*
* <p>后端详情本就是「4 个页签 4 个端点」,所以点一次 = 一次请求,且只请求被点的那一支;
* 重复点击当前页签也会重取(用于拿到别处改动后的最新值)。竞态由 store 的请求序号兜住
* ——切项目、连点同一页签都不会把旧响应盖回页面。</p>
*/
function onUpload(file: File): void {
void file
ElMessage.info('附件上传接口待接入')
function onTabClick(tab: DetailTab): void {
activeTab.value = tab
void projectStore.refreshTab(tab)
}
/**
* 写操作的现状(2026-09-28):
* · **售后已接真实接口** —— 申请跳 `/project/:id/after-sales`(`/afterSales/request`),
* 取消走 `/afterSales/withdraw`;两者成功后都会重拉售后页签;
* · **需求附件的删除 / 预览 / 下载仍无用户端接口**(上传入口在需求提报页),
* 所以这里只提示「待接入」,**不往详情里塞本地假数据** —— 详情数据来自真实接口,
* 混入前端自造条目会让刷新前后不一致。
* 另外详情页「只展示文件」:上传入口在需求提报页,这里不提供。
*/
function onDeleteFile(index: number): void {
void index
ElMessage.info('附件删除接口待接入')
@@ -82,7 +94,8 @@ function onApplyAfterSales(): void {
}
function onConfirmCancel(): void {
void projectStore.cancelAfterSales()
// 撤回接口收的是**需求 id**(后端按需求校验归属)
void projectStore.cancelAfterSales(projectId.value)
}
watch(projectId, load)
@@ -102,33 +115,37 @@ onMounted(load)
<template v-if="current">
<ProjectInfoCard :project="current" />
<ProjectTeam :team="current.team" />
<!-- 本地 Tab 切换 -->
<!-- 本地 Tab 切换(设计稿:文字 + 激活项下划线) -->
<div class="project-detail__tabs">
<button
v-for="tab in tabs"
:key="tab.value"
class="project-detail__tab"
:class="{ 'is-active': activeTab === tab.value }"
@click="activeTab = tab.value"
:class="{
'is-active': activeTab === tab.value,
'is-loading': loadingTab === tab.value,
}"
@click="onTabClick(tab.value)"
>
{{ tab.label }}
</button>
</div>
<ProjectMilestones
v-if="activeTab === 'progress'"
:progress="current.progress"
:milestones="current.milestones"
:start-time="current.startTime"
:end-time="current.endTime"
/>
<!-- 「项目进度」页签 = 开发进度 → 里程碑 → 项目团队(顺序按设计稿) -->
<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"
@upload="onUpload"
@delete="onDeleteFile"
@preview="onPreviewFile"
@download="onDownloadFile"
@@ -144,10 +161,11 @@ onMounted(load)
@download="onFinanceDownload"
/>
<!-- 售后页签三形态:项目未完成 / 已完成未提交 / 已完成已提交(详见组件内注释) -->
<ProjectAfterSales
v-if="activeTab === 'afterSales'"
:after-sales="current.afterSales"
:project-id="current.id"
:project-status="current.status"
@apply="onApplyAfterSales"
@cancel="cancelVisible = true"
/>
@@ -178,36 +196,49 @@ onMounted(load)
padding-bottom: @spacing-xxl;
&__breadcrumb {
font-size: @font-size-base;
font-size: @font-size-sm;
}
// 设计稿:页签无白底容器,仅一行文字 + 激活项 2px 下划线;相邻间距实测 ≈50px
&__tabs {
.flex(row, flex-start);
gap: @spacing-xs;
padding: @spacing-xs;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
.flex(row, flex-start, center);
gap: 50px;
padding: 0 @spacing-base;
}
&__tab {
height: 34px;
padding: 0 @spacing-base;
position: relative;
padding: 0 0 @spacing-sm;
border: none;
border-radius: @radius-base;
background: transparent;
color: @text-color;
font-size: @font-size-base;
transition: all 0.2s;
line-height: 20px;
transition: color 0.2s;
&:hover {
color: @primary-color;
color: @project-accent;
}
// 刷新中:轻微变淡 + 进度光标(本地接口很快,做太重的 loading 反而闪屏)
&.is-loading {
opacity: 0.6;
cursor: progress;
}
&.is-active {
background: @primary-light;
color: @primary-color;
font-weight: 600;
color: @project-accent;
&::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
border-radius: 1px;
background: @project-accent;
}
}
}