feat: 完成空间管理模块

This commit is contained in:
Zzz1zz
2026-01-23 01:14:47 +08:00
parent a53cd0e8b8
commit f582098dc6
46 changed files with 3231 additions and 2309 deletions
@@ -0,0 +1,272 @@
<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>