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>
|