项目中心
This commit is contained in:
@@ -2,185 +2,168 @@
|
||||
import type { Milestone } from '@/types/business/project'
|
||||
|
||||
interface Props {
|
||||
progress: number
|
||||
milestones: Milestone[]
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
/** 三态 → 修饰类(设计稿:已完成=绿实心+白勾 / 进行中=白底粗蓝环 / 待开始=白底灰环) */
|
||||
function stateClass(status: Milestone['status']): string {
|
||||
if (status === '已完成') return 'is-done'
|
||||
if (status === '进行中') return 'is-active'
|
||||
return 'is-pending'
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接线是否走「已完成」的绿色。
|
||||
*
|
||||
* <p>设计稿实测:只有<b>下一个节点已完成</b>时该段才是绿色(第二个节点已完成 → 第 1 段绿;
|
||||
* 第三个节点进行中 → 第 2 段仍是灰色,不随「进行中」变蓝)。</p>
|
||||
*/
|
||||
function linkDone(index: number): boolean {
|
||||
return props.milestones[index + 1]?.status === '已完成'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="project-milestones">
|
||||
<div class="project-milestones__head">
|
||||
<h2 class="project-milestones__title">
|
||||
总体进度
|
||||
</h2>
|
||||
<span class="project-milestones__percent">{{ progress }}%</span>
|
||||
</div>
|
||||
<div class="project-milestones__track">
|
||||
<div
|
||||
class="project-milestones__bar"
|
||||
:style="{ width: `${progress}%` }"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="project-milestones__title">
|
||||
项目进度(里程碑)
|
||||
</h2>
|
||||
|
||||
<ol class="project-milestones__timeline">
|
||||
<ol v-if="props.milestones.length > 0" class="project-milestones__track">
|
||||
<li
|
||||
v-for="node in milestones"
|
||||
:key="node.name"
|
||||
v-for="(node, index) in props.milestones"
|
||||
:key="`${node.name}-${index}`"
|
||||
class="project-milestones__node"
|
||||
:class="`is-${node.status === '已完成' ? 'done' : node.status === '进行中' ? 'active' : 'pending'}`"
|
||||
:class="[stateClass(node.status), { 'is-link-done': linkDone(index) }]"
|
||||
>
|
||||
<span class="project-milestones__dot" />
|
||||
<div class="project-milestones__body">
|
||||
<div class="project-milestones__row">
|
||||
<span class="project-milestones__name">{{ node.name }}</span>
|
||||
<span class="project-milestones__status">{{ node.status }}</span>
|
||||
</div>
|
||||
<span v-if="node.time" class="project-milestones__time text-secondary">{{ node.time }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="milestones.length === 0" class="project-milestones__empty text-secondary">
|
||||
暂无里程碑数据
|
||||
<span class="project-milestones__dot">
|
||||
<svg
|
||||
v-if="node.status === '已完成'"
|
||||
class="project-milestones__check"
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3.5 8.4 6.6 11.5 12.5 4.8" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="project-milestones__name">{{ node.name }}</span>
|
||||
<!-- 设计稿:已完成节点显示完成时间,其余显示状态文案 -->
|
||||
<span class="project-milestones__sub text-secondary">{{ node.time || node.status }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="project-milestones__range">
|
||||
<span v-if="startTime" class="text-secondary">开始时间:{{ startTime }}</span>
|
||||
<span v-if="endTime" class="text-secondary">结束时间:{{ endTime }}</span>
|
||||
</div>
|
||||
<p v-else class="project-milestones__empty text-secondary">
|
||||
暂无里程碑数据
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 节点列宽 = 设计稿相邻节点中心距(237 设计px × 0.77 ≈ 182px)
|
||||
@milestone-node-width: 182px;
|
||||
@milestone-dot-size: 32px;
|
||||
|
||||
.project-milestones {
|
||||
padding: @spacing-lg @spacing-xl;
|
||||
.flex(column, flex-start, stretch);
|
||||
padding: @spacing-lg;
|
||||
border-radius: @radius-lg;
|
||||
background: @white;
|
||||
box-shadow: @shadow-base;
|
||||
|
||||
&__head {
|
||||
.flex(row, space-between, center);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: 600;
|
||||
color: @heading-color;
|
||||
}
|
||||
|
||||
&__percent {
|
||||
font-size: @font-size-xl;
|
||||
font-weight: 600;
|
||||
color: @primary-color;
|
||||
.section-title();
|
||||
}
|
||||
|
||||
// 设计稿:轨迹从卡片左侧正文位起、**固定节距**左对齐(不铺满整宽)
|
||||
&__track {
|
||||
height: 8px;
|
||||
margin-top: @spacing-base;
|
||||
border-radius: @radius-round;
|
||||
background: @bg-color;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
height: 100%;
|
||||
border-radius: @radius-round;
|
||||
background: @primary-color;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
&__timeline {
|
||||
.flex(row, flex-start, flex-start);
|
||||
margin-top: @spacing-lg;
|
||||
padding-left: @spacing-xs;
|
||||
padding-bottom: @spacing-xs;
|
||||
overflow-x: auto;
|
||||
.scrollbar();
|
||||
}
|
||||
|
||||
&__node {
|
||||
position: relative;
|
||||
padding: 0 0 @spacing-base @spacing-lg;
|
||||
border-left: 2px solid @border-color;
|
||||
.flex(column, flex-start, center);
|
||||
flex: 0 0 @milestone-node-width;
|
||||
min-width: 0;
|
||||
|
||||
&:last-of-type {
|
||||
border-left-color: transparent;
|
||||
padding-bottom: 0;
|
||||
// 圆点之间的连接线:从本节点圆点右缘连到下一节点圆点左缘
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: (@milestone-dot-size / 2 - 1px);
|
||||
left: calc(50% + @milestone-dot-size / 2);
|
||||
width: calc(100% - @milestone-dot-size);
|
||||
height: 2px;
|
||||
background: @milestone-idle;
|
||||
}
|
||||
|
||||
&.is-done {
|
||||
border-left-color: @success-color;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-left-color: @primary-color;
|
||||
&.is-link-done:not(:last-child)::after {
|
||||
background: @milestone-done;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: -7px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid @border-color-dark;
|
||||
.flex(row, center, center);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: @milestone-dot-size;
|
||||
height: @milestone-dot-size;
|
||||
border: 2px solid @milestone-idle;
|
||||
border-radius: @radius-round;
|
||||
background: @white;
|
||||
box-sizing: border-box;
|
||||
|
||||
.is-done & {
|
||||
border-color: @success-color;
|
||||
background: @success-color;
|
||||
border-color: @milestone-done;
|
||||
background: @milestone-done;
|
||||
}
|
||||
|
||||
.is-active & {
|
||||
border-color: @primary-color;
|
||||
background: @primary-color;
|
||||
border-width: 3px;
|
||||
border-color: @project-accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
.flex(column, flex-start, stretch);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
.flex(row, center);
|
||||
gap: @spacing-sm;
|
||||
&__check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-top: @spacing-sm;
|
||||
max-width: 100%;
|
||||
font-size: @font-size-base;
|
||||
color: @heading-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: @font-size-sm;
|
||||
text-align: center;
|
||||
color: @text-color-secondary;
|
||||
.multi-ellipsis(2);
|
||||
|
||||
.is-done & {
|
||||
color: @success-color;
|
||||
color: @milestone-done;
|
||||
}
|
||||
|
||||
.is-active & {
|
||||
color: @primary-color;
|
||||
color: @project-accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
&__sub {
|
||||
margin-top: @spacing-xs;
|
||||
max-width: 100%;
|
||||
font-size: @font-size-sm;
|
||||
text-align: center;
|
||||
.multi-ellipsis(2);
|
||||
}
|
||||
|
||||
&__empty {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
&__range {
|
||||
.flex(row, flex-start, center);
|
||||
gap: @spacing-lg;
|
||||
margin-top: @spacing-base;
|
||||
padding-top: @spacing-base;
|
||||
border-top: 1px solid @border-color;
|
||||
font-size: @font-size-sm;
|
||||
padding-left: 21px;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user