feat 搭建架构骨架

This commit is contained in:
Ironsp
2026-09-24 09:09:47 +08:00
parent dd9f6b77a4
commit 7a2efa3fea
180 changed files with 18967 additions and 9 deletions

View File

@@ -0,0 +1,214 @@
<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'
// 页面容器:数据只经 store,不直接 import api
import { useProjectStore } from '@/stores/modules/project'
import ProjectInfoCard from './cpns/ProjectInfoCard.vue'
import ProjectTeam from './cpns/ProjectTeam.vue'
import ProjectMilestones from './cpns/ProjectMilestones.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 } = 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 onUpload(file: File): void {
void projectStore.uploadFile(file).then(() => {
ElMessage.success('文件已上传')
})
}
function onDeleteFile(index: number): void {
void projectStore.deleteFile(index).then(() => {
ElMessage.success('文件已删除')
})
}
function onPreviewFile(): void {
// TODO(api): 文件预览接口
ElMessage.info('文件预览待接口接入')
}
function onDownloadFile(): void {
// TODO(api): 文件下载接口
ElMessage.info('文件下载待接口接入')
}
function onPay(): void {
// TODO: 跳财务中心 `/finance/bills`
ElMessage.info('请前往财务中心完成支付')
}
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()
}
watch(projectId, load)
onMounted(load)
</script>
<template>
<div class="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">
<ProjectInfoCard :project="current" />
<ProjectTeam :team="current.team" />
<!-- 本地 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"
>
{{ tab.label }}
</button>
</div>
<ProjectMilestones
v-if="activeTab === 'progress'"
:progress="current.progress"
:milestones="current.milestones"
:start-time="current.startTime"
:end-time="current.endTime"
/>
<ProjectFiles
v-if="activeTab === 'files'"
:files="current.files"
@upload="onUpload"
@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-id="current.id"
@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);
gap: @spacing-base;
padding-top: @spacing-lg;
padding-bottom: @spacing-xxl;
&__breadcrumb {
font-size: @font-size-base;
}
&__tabs {
.flex(row, flex-start);
gap: @spacing-xs;
padding: @spacing-xs;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
}
&__tab {
height: 34px;
padding: 0 @spacing-base;
border: none;
border-radius: @radius-base;
background: transparent;
color: @text-color;
font-size: @font-size-base;
transition: all 0.2s;
&:hover {
color: @primary-color;
}
&.is-active {
background: @primary-light;
color: @primary-color;
font-weight: 600;
}
}
&__empty {
padding: @spacing-xxl 0;
text-align: center;
font-size: @font-size-base;
}
}
</style>