273 lines
7.3 KiB
Vue
273 lines
7.3 KiB
Vue
<template>
|
||
<div
|
||
class="cross-year-timeline-container"
|
||
:style="{
|
||
position: 'absolute',
|
||
left: '0',
|
||
width: `${yearRange.length * 200}px`,
|
||
height: '40px',
|
||
zIndex: 2,
|
||
}"
|
||
>
|
||
<!-- 年份分隔线 -->
|
||
<template v-for="(year, index) in yearRange" :key="`divider-${year}`">
|
||
<div
|
||
v-if="index < yearRange.length - 1"
|
||
class="year-divider"
|
||
:style="{
|
||
position: 'absolute',
|
||
left: `${(index + 1) * 200}px`,
|
||
top: '0',
|
||
bottom: '0',
|
||
width: '1px',
|
||
backgroundColor: '#e5e6eb',
|
||
zIndex: 1,
|
||
}"
|
||
></div>
|
||
</template>
|
||
|
||
<!-- 连续时间线(跨列显示) -->
|
||
<div
|
||
class="timeline-bar"
|
||
:style="{
|
||
position: 'absolute',
|
||
left: '0',
|
||
right: '0',
|
||
top: '0',
|
||
bottom: '0',
|
||
}"
|
||
>
|
||
<!-- 空置状态 -->
|
||
<div
|
||
v-for="(segment, index) in allSegments"
|
||
:key="`segment-${index}`"
|
||
class="timeline-segment"
|
||
:class="segment.type"
|
||
:style="{
|
||
position: 'absolute',
|
||
left: `${segment.startPercentage}%`,
|
||
width: `${segment.percentage}%`,
|
||
height: '100%',
|
||
backgroundColor:
|
||
segment.type === 'vacant'
|
||
? '#f2f3f5'
|
||
: segment.type === 'lease'
|
||
? '#00b42a'
|
||
: 'transparent',
|
||
backgroundImage:
|
||
segment.type === 'unarranged'
|
||
? 'repeating-linear-gradient(45deg, #f2f3f5, #f2f3f5 4px, #d9d9d9 4px, #d9d9d9 8px)'
|
||
: 'none',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 2,
|
||
}"
|
||
>
|
||
<span
|
||
v-if="segment.label && segment.percentage > 5"
|
||
class="segment-label"
|
||
:style="{
|
||
fontSize: '12px',
|
||
color: segment.type === 'lease' ? '#fff' : '#86909c',
|
||
fontWeight: segment.type === 'lease' ? '500' : 'normal',
|
||
padding: '0 4px',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
}"
|
||
>
|
||
{{ segment.label }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed } from 'vue';
|
||
|
||
interface YearData {
|
||
year: string;
|
||
allDays: number;
|
||
allData: Array<{
|
||
companyName: string;
|
||
days: number;
|
||
type: number; // 1: 在租, 其他可能是空置
|
||
}>;
|
||
}
|
||
|
||
interface Props {
|
||
yearsData?: YearData[];
|
||
yearRange: number[];
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
yearsData: () => [],
|
||
yearRange: () => [],
|
||
});
|
||
|
||
// 计算总天数(5年)
|
||
const totalDays = computed(() => {
|
||
// 如果数据为空,使用默认的5年天数
|
||
if (!props.yearsData || props.yearsData.length === 0) {
|
||
return 365 * props.yearRange.length;
|
||
}
|
||
// 确保所有年份都有数据,缺失的年份使用365天
|
||
let total = 0;
|
||
props.yearRange.forEach((year) => {
|
||
const yearData = props.yearsData?.find((d) => d.year === String(year));
|
||
total += yearData?.allDays || 365;
|
||
});
|
||
return total;
|
||
});
|
||
|
||
// 合并所有年份的数据,创建连续的时间段
|
||
interface TimelineSegment {
|
||
type: 'vacant' | 'lease' | 'unarranged';
|
||
percentage: number;
|
||
startPercentage: number; // 在整个时间线中的起始位置百分比
|
||
days: number;
|
||
label?: string;
|
||
startDay: number; // 在整个5年时间线中的起始天数
|
||
endDay: number; // 在整个5年时间线中的结束天数
|
||
}
|
||
|
||
const allSegments = computed(() => {
|
||
if (!props.yearsData || props.yearsData.length === 0) {
|
||
// 如果没有数据,全部显示为未安排
|
||
return [
|
||
{
|
||
type: 'unarranged' as const,
|
||
percentage: 100,
|
||
startPercentage: 0,
|
||
days: totalDays.value,
|
||
startDay: 0,
|
||
endDay: totalDays.value,
|
||
},
|
||
];
|
||
}
|
||
|
||
const segments: TimelineSegment[] = [];
|
||
let globalOffset = 0; // 在整个5年时间线中的累计天数
|
||
|
||
// 按年份顺序处理数据,确保年份顺序正确
|
||
const sortedYearsData = [...props.yearsData].sort((a, b) => {
|
||
return Number(a.year) - Number(b.year);
|
||
});
|
||
|
||
// 创建年份数据映射
|
||
const yearDataMap = new Map<string, YearData>();
|
||
sortedYearsData.forEach((data) => {
|
||
yearDataMap.set(data.year, data);
|
||
});
|
||
|
||
// 按年份范围顺序处理
|
||
props.yearRange.forEach((year) => {
|
||
const yearStr = String(year);
|
||
const yearData = yearDataMap.get(yearStr);
|
||
const yearDays = yearData?.allDays || 365;
|
||
const yearStartDay = globalOffset;
|
||
const yearEndDay = globalOffset + yearDays;
|
||
|
||
if (!yearData || !yearData.allData || yearData.allData.length === 0) {
|
||
// 如果该年份没有数据,全部显示为未安排
|
||
segments.push({
|
||
type: 'unarranged',
|
||
percentage: (yearDays / totalDays.value) * 100,
|
||
startPercentage: (yearStartDay / totalDays.value) * 100,
|
||
days: yearDays,
|
||
startDay: yearStartDay,
|
||
endDay: yearEndDay,
|
||
});
|
||
globalOffset = yearEndDay;
|
||
return;
|
||
}
|
||
|
||
// 收集该年份的所有时间段,按顺序排列
|
||
const yearSegments: Array<{
|
||
type: 'vacant' | 'lease';
|
||
days: number;
|
||
label: string;
|
||
}> = [];
|
||
|
||
yearData.allData.forEach((item) => {
|
||
if (item.type === 1 && item.days > 0) {
|
||
// 在租
|
||
yearSegments.push({
|
||
type: 'lease',
|
||
days: item.days,
|
||
label: item.companyName || '',
|
||
});
|
||
} else if (item.type !== 1 && item.days > 0) {
|
||
// 空置
|
||
yearSegments.push({
|
||
type: 'vacant',
|
||
days: item.days,
|
||
label: `空置${item.days}天`,
|
||
});
|
||
}
|
||
});
|
||
|
||
// 计算已安排的天数
|
||
const arrangedDays = yearSegments.reduce((sum, seg) => sum + seg.days, 0);
|
||
const unarrangedDays = yearDays - arrangedDays;
|
||
|
||
// 添加已安排的时间段(连续显示)
|
||
let offset = 0;
|
||
yearSegments.forEach((seg) => {
|
||
const startDay = yearStartDay + offset;
|
||
const endDay = startDay + seg.days;
|
||
segments.push({
|
||
type: seg.type,
|
||
percentage: (seg.days / totalDays.value) * 100,
|
||
startPercentage: (startDay / totalDays.value) * 100,
|
||
days: seg.days,
|
||
label: seg.label,
|
||
startDay,
|
||
endDay,
|
||
});
|
||
offset += seg.days;
|
||
});
|
||
|
||
// 添加未安排的时间段
|
||
if (unarrangedDays > 0) {
|
||
const startDay = yearStartDay + offset;
|
||
segments.push({
|
||
type: 'unarranged',
|
||
percentage: (unarrangedDays / totalDays.value) * 100,
|
||
startPercentage: (startDay / totalDays.value) * 100,
|
||
days: unarrangedDays,
|
||
startDay,
|
||
endDay: yearEndDay,
|
||
});
|
||
}
|
||
|
||
globalOffset = yearEndDay;
|
||
});
|
||
|
||
return segments;
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.cross-year-timeline-container {
|
||
position: relative;
|
||
overflow: visible;
|
||
}
|
||
|
||
.timeline-bar {
|
||
border-radius: 2px;
|
||
overflow: visible;
|
||
}
|
||
|
||
.timeline-segment {
|
||
transition: all 0.3s;
|
||
min-width: 0;
|
||
}
|
||
|
||
.segment-label {
|
||
user-select: none;
|
||
}
|
||
</style>
|