Files
cz-app/src/pages/project-center/detail/index.vue
2026-09-14 10:32:21 +08:00

704 lines
16 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 { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getProjectDetail } from '@/constants/project-detail'
import cardBg from '@/static/icons/new-card.png'
import deleteIcon from '@/static/icons/X.png'
import type { ProjectDetail, ProjectDetailTab } from '@/types/project-detail'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const detail = ref<ProjectDetail | null>(null)
const activeTab = ref<ProjectDetailTab>('progress')
const tabs: Array<{ key: ProjectDetailTab, label: string }> = [
{ key: 'progress', label: '项目进度' },
{ key: 'files', label: '项目文件' },
{ key: 'finance', label: '合同财务' },
{ key: 'aftersales', label: '售后信息' },
]
onLoad((options) => {
const rawId = options?.id
const id = rawId ? decodeURIComponent(String(rawId)) : '1'
detail.value = getProjectDetail(id)
if (!detail.value) {
uni.showToast({ title: '项目不存在', icon: 'none' })
setTimeout(() => uni.navigateBack(), 1500)
}
})
function handleBack() {
navigateBackWithFallback('/pages/project-center/index')
}
function handleSelectTab(key: ProjectDetailTab) {
activeTab.value = key
}
</script>
<template>
<view
v-if="detail"
class="project-detail"
>
<view class="project-detail__navbar">
<view
class="project-detail__back"
@tap="handleBack"
>
<image
class="project-detail__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="project-detail__navbar-title">
项目详情
</text>
<view class="project-detail__navbar-placeholder" />
</view>
<scroll-view
class="project-detail__scroll"
scroll-y
:show-scrollbar="false"
>
<view class="project-detail__content">
<view class="project-detail__hero">
<image
class="project-detail__hero-bg"
:src="cardBg"
mode="scaleToFill"
/>
<view class="project-detail__hero-inner">
<view class="project-detail__hero-top">
<text class="project-detail__tag">
{{ detail.category }}
</text>
<view class="project-detail__hero-status">
<text class="project-detail__status">
{{ detail.status }}
</text>
<text
v-if="detail.progress !== undefined"
class="project-detail__progress-text"
>
{{ detail.progress }}%
</text>
</view>
</view>
<text class="project-detail__title">
{{ detail.title }}
</text>
<text class="project-detail__no">
项目编号:{{ detail.projectNo }}
</text>
</view>
</view>
<scroll-view
class="project-detail__tabs"
scroll-x
:show-scrollbar="false"
>
<view class="project-detail__tabs-inner">
<text
v-for="tab in tabs"
:key="tab.key"
class="project-detail__tab"
:class="{ 'project-detail__tab--active': activeTab === tab.key }"
@tap="handleSelectTab(tab.key)"
>
{{ tab.label }}
</text>
</view>
</scroll-view>
<view
v-if="activeTab === 'progress'"
class="project-detail__panel"
>
<view class="project-detail__dev-progress">
<text class="project-detail__panel-title">
开发进度
</text>
<view class="project-detail__overall">
<text class="project-detail__overall-label">
总体进度
</text>
<text class="project-detail__overall-value">
{{ detail.progress ?? 0 }}%
</text>
</view>
<view class="project-detail__slider">
<view class="project-detail__slider-track">
<view
class="project-detail__slider-fill"
:style="{ width: `${detail.progress ?? 0}%` }"
/>
<view
class="project-detail__slider-thumb"
:style="{ left: `${detail.progress ?? 0}%` }"
/>
</view>
</view>
<view class="project-detail__dates">
<text>开始时间:{{ detail.progressStartTime }}</text>
<text>结束时间:{{ detail.progressEndTime }}</text>
</view>
</view>
<view class="project-detail__timeline">
<text class="project-detail__panel-title project-detail__timeline-heading">
项目进度(里程碑)
</text>
<view
v-for="(step, index) in detail.steps"
:key="step.id"
class="project-detail__timeline-item"
>
<view class="project-detail__timeline-left">
<view
class="project-detail__timeline-dot"
:class="{
'project-detail__timeline-dot--done': step.status === 'done',
'project-detail__timeline-dot--active': step.status === 'active',
'project-detail__timeline-dot--pending': step.status === 'pending',
}"
>
<text
v-if="step.status === 'done'"
class="project-detail__timeline-check"
>
✓
</text>
<text
v-else
class="project-detail__timeline-index"
>
{{ index + 1 }}
</text>
</view>
<view
v-if="index < detail.steps.length - 1"
class="project-detail__timeline-line"
:class="{ 'project-detail__timeline-line--done': step.status === 'done' }"
/>
</view>
<view class="project-detail__timeline-body">
<text class="project-detail__timeline-title">
{{ step.title }}
</text>
<text
v-if="step.time"
class="project-detail__timeline-time"
>
{{ step.time }}
</text>
<text
v-else-if="step.statusText"
class="project-detail__timeline-status"
:class="{ 'project-detail__timeline-status--active': step.status === 'active' }"
>
{{ step.statusText }}
</text>
</view>
</view>
</view>
<view class="project-detail__team">
<text class="project-detail__panel-title">
项目团队
</text>
<scroll-view
class="project-detail__team-scroll"
scroll-x
:show-scrollbar="false"
>
<view class="project-detail__team-inner">
<view
v-for="member in detail.team"
:key="member.id"
class="project-detail__member"
>
<view class="project-detail__avatar">
<text>{{ member.name.slice(0, 2) }}</text>
</view>
<text class="project-detail__member-name">
{{ member.name }}
</text>
<text class="project-detail__member-role">
{{ member.role }}
</text>
</view>
</view>
</scroll-view>
</view>
</view>
<view
v-else-if="activeTab === 'files'"
class="project-detail__panel"
>
<view class="project-detail__files">
<view
v-for="file in detail.files"
:key="file.id"
class="project-detail__file"
>
<view class="project-detail__file-remove">
<image
class="project-detail__file-remove-icon"
:src="deleteIcon"
mode="aspectFit"
/>
</view>
<image
class="project-detail__file-icon"
:src="file.icon"
mode="aspectFit"
/>
<text class="project-detail__file-name">
{{ file.name }}
</text>
</view>
</view>
</view>
<view
v-else-if="activeTab === 'finance'"
class="project-detail__panel"
>
<text class="project-detail__paragraph">
{{ detail.financeSummary }}
</text>
</view>
<view
v-else
class="project-detail__panel"
>
<text class="project-detail__paragraph">
{{ detail.afterSalesSummary }}
</text>
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.project-detail {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-section;
}
.project-detail__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 12rpx) 24rpx 20rpx;
background: $color-bg-page;
}
.project-detail__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.project-detail__back-icon {
width: 40rpx;
height: 40rpx;
}
.project-detail__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.project-detail__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.project-detail__scroll {
flex: 1;
height: 0;
}
.project-detail__content {
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
}
.project-detail__hero {
position: relative;
overflow: hidden;
border-radius: 20rpx;
}
.project-detail__hero-bg {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
}
.project-detail__hero-inner {
position: relative;
z-index: 1;
padding: 28rpx 24rpx;
}
.project-detail__hero-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.project-detail__tag {
padding: 4rpx 12rpx;
font-size: 20rpx;
color: $color-primary;
background: $color-primary-light;
border-radius: 6rpx;
}
.project-detail__hero-status {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.project-detail__status {
font-size: 24rpx;
color: $color-text-secondary;
}
.project-detail__progress-text {
margin-top: 4rpx;
font-size: 24rpx;
color: $color-text-secondary;
}
.project-detail__title {
display: block;
margin-top: 20rpx;
font-size: 32rpx;
font-weight: 600;
color: $color-text-primary;
}
.project-detail__no {
display: block;
margin-top: 16rpx;
font-size: 22rpx;
color: #666666;
}
.project-detail__tabs {
margin-top: 24rpx;
white-space: nowrap;
}
.project-detail__tabs-inner {
display: inline-flex;
gap: 32rpx;
padding: 0 4rpx;
}
.project-detail__tab {
flex-shrink: 0;
padding-bottom: 12rpx;
font-size: 28rpx;
color: $color-text-secondary;
&--active {
font-weight: 600;
color: $color-text-primary;
border-bottom: 4rpx solid $color-primary;
}
}
.project-detail__panel {
margin-top: 24rpx;
padding: 28rpx 24rpx;
background: $color-bg-page;
border-radius: 20rpx;
}
.project-detail__panel-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: $color-text-primary;
}
.project-detail__dev-progress {
padding-bottom: 8rpx;
}
.project-detail__overall {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
}
.project-detail__overall-label {
font-size: 26rpx;
color: $color-text-secondary;
}
.project-detail__overall-value {
font-size: 28rpx;
font-weight: 600;
color: $color-primary;
}
.project-detail__slider {
margin-top: 20rpx;
}
.project-detail__slider-track {
position: relative;
height: 8rpx;
background: #e8eaef;
border-radius: 4rpx;
}
.project-detail__slider-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: $color-primary;
border-radius: 4rpx;
}
.project-detail__slider-thumb {
position: absolute;
top: 50%;
width: 24rpx;
height: 24rpx;
margin-left: -12rpx;
background: $color-bg-page;
border: 4rpx solid $color-primary;
border-radius: 50%;
transform: translateY(-50%);
}
.project-detail__dates {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
font-size: 22rpx;
color: $color-text-secondary;
}
.project-detail__timeline {
margin-top: 32rpx;
padding-top: 8rpx;
}
.project-detail__timeline-heading {
display: block;
margin-bottom: 24rpx;
}
.project-detail__timeline-item {
display: flex;
min-height: 96rpx;
}
.project-detail__timeline-left {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 48rpx;
margin-right: 20rpx;
}
.project-detail__timeline-dot {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
&--done {
background: $color-primary;
}
&--active {
background: $color-primary;
}
&--pending {
background: $color-bg-page;
border: 2rpx solid #d8dde6;
}
}
.project-detail__timeline-check {
font-size: 22rpx;
color: #ffffff;
}
.project-detail__timeline-index {
font-size: 22rpx;
color: #ffffff;
.project-detail__timeline-dot--pending & {
color: $color-text-secondary;
}
}
.project-detail__timeline-line {
flex: 1;
width: 2rpx;
min-height: 40rpx;
margin-top: 8rpx;
background: #e8eaef;
&--done {
background: $color-primary;
}
}
.project-detail__timeline-body {
flex: 1;
padding-bottom: 28rpx;
}
.project-detail__timeline-title {
display: block;
font-size: 28rpx;
color: $color-text-primary;
}
.project-detail__timeline-time,
.project-detail__timeline-status {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: $color-text-secondary;
}
.project-detail__timeline-status--active {
color: $color-primary;
}
.project-detail__team {
margin-top: 32rpx;
padding-top: 28rpx;
border-top: 1rpx solid #f0f0f0;
}
.project-detail__team-scroll {
margin-top: 24rpx;
white-space: nowrap;
}
.project-detail__team-inner {
display: inline-flex;
gap: 32rpx;
}
.project-detail__member {
display: flex;
flex-direction: column;
align-items: center;
width: 140rpx;
}
.project-detail__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
font-size: 28rpx;
color: #ffffff;
background: $color-primary;
border-radius: 50%;
}
.project-detail__member-name {
margin-top: 12rpx;
font-size: 26rpx;
color: $color-text-primary;
}
.project-detail__member-role {
margin-top: 6rpx;
font-size: 22rpx;
color: $color-text-secondary;
}
.project-detail__files {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
padding: 10rpx 10rpx 0 0;
}
.project-detail__file {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 160rpx;
padding: 20rpx 16rpx;
background: $color-bg-section;
border-radius: 12rpx;
}
.project-detail__file-remove {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 32rpx;
height: 32rpx;
}
.project-detail__file-remove-icon {
width: 32rpx;
height: 32rpx;
}
.project-detail__file-icon {
width: 64rpx;
height: 64rpx;
}
.project-detail__file-name {
margin-top: 12rpx;
font-size: 22rpx;
color: $color-text-primary;
text-align: center;
word-break: break-all;
}
.project-detail__paragraph {
display: block;
font-size: 28rpx;
line-height: 1.8;
color: $color-text-primary;
}
</style>