200 lines
5.7 KiB
Vue
200 lines
5.7 KiB
Vue
<script lang="ts" setup>
|
|
import type { ThingType } from './type';
|
|
import dayjs from 'dayjs';
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
yStep: 80,
|
|
minuteStep: 80 / 60,
|
|
value: dayjs().valueOf(),
|
|
weekDates: () => [],
|
|
groupedEvents: () => new Map(),
|
|
timeLineWidth: 80,
|
|
});
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
defineComponent({ name: 'GantView' });
|
|
|
|
interface Props {
|
|
value?: number // 选中的日期 时间戳
|
|
yStep?: number
|
|
timeLineWidth?: number
|
|
minuteStep?: number
|
|
weekDates?: { date: number, day: string, weekDay: string }[]
|
|
groupedEvents?: Map<number, ThingType[]>
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'clickThing', value: ThingType): void
|
|
}
|
|
|
|
// Y 轴为 00:00 - 24:00 的时间轴 , 时间步进为 1H , 用dayjs 转换为 00:00 格式
|
|
const Y_AXIS = Array.from({ length: 24 }, (_, i) => {
|
|
return {
|
|
label: dayjs().startOf('day').add(i, 'hour').format('HH'),
|
|
top: i * props.yStep,
|
|
};
|
|
});
|
|
|
|
/** ** 正确的格式一定为某一天的起始 */
|
|
const selectedDate = computed(() => {
|
|
return dayjs(props.value).startOf('day').valueOf();
|
|
});
|
|
|
|
watch(selectedDate, () => {
|
|
scrollToThing();
|
|
});
|
|
|
|
onMounted(() => {
|
|
// nextTick(() => {
|
|
// scrollToThing();
|
|
// });
|
|
});
|
|
|
|
function scrollToThing(): number {
|
|
const _thing = props.groupedEvents.get(selectedDate.value);
|
|
|
|
const currentTime = dayjs().startOf('minute');
|
|
|
|
if (!_thing) {
|
|
return currentTime.diff(currentTime.startOf('day'), 'minute') * props.minuteStep - props.yStep;
|
|
}
|
|
|
|
let closestEvent: ThingType | null = null;
|
|
|
|
// 查找与当前时刻最接近的事件
|
|
for (const event of _thing) {
|
|
const eventTime = dayjs(event.startTime);
|
|
if (
|
|
!closestEvent
|
|
|| Math.abs(eventTime.diff(currentTime)) < Math.abs(dayjs(closestEvent.startTime).diff(currentTime))
|
|
) {
|
|
closestEvent = event;
|
|
}
|
|
}
|
|
|
|
// 如果没有找到与当前时刻最接近的事件,则查找当前时刻未来的第一个事件
|
|
if (!closestEvent) {
|
|
closestEvent = _thing.find(event => dayjs(event.startTime).isAfter(currentTime)) || null;
|
|
}
|
|
|
|
// 如果还没有找到,则查找列表中与当前时刻最相近的事件
|
|
if (!closestEvent) {
|
|
closestEvent = _thing.reduce((prev, curr) => {
|
|
const prevDiff = Math.abs(dayjs(prev.startTime).diff(currentTime));
|
|
const currDiff = Math.abs(dayjs(curr.startTime).diff(currentTime));
|
|
return currDiff < prevDiff ? curr : prev;
|
|
});
|
|
}
|
|
|
|
// 如果还没有找到,则使用列表中的第一个事件
|
|
if (!closestEvent && _thing[0]) {
|
|
closestEvent = _thing[0];
|
|
}
|
|
|
|
if (!closestEvent.toTop)
|
|
closestEvent.toTop = 0;
|
|
return closestEvent.toTop;
|
|
}
|
|
|
|
const scrollTop = computed(() => {
|
|
const top = uni.upx2px(scrollToThing()) - props.yStep / 4;
|
|
return top;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<view class="relative flex-1 bg-tertiaryContainer">
|
|
<scroll-view
|
|
:scroll-anchoring="true"
|
|
:scroll-with-animation="true"
|
|
:scroll-top="scrollTop"
|
|
:scroll-y="true"
|
|
class="absolute left-0 top-0 w-full h-full "
|
|
>
|
|
<view class="flex flex-row">
|
|
<!-- time line -->
|
|
<view
|
|
:style="{
|
|
width: `${props.timeLineWidth}rpx`,
|
|
}"
|
|
class="flex flex-col"
|
|
>
|
|
<view
|
|
v-for="(item, inex) in Y_AXIS"
|
|
:key="inex"
|
|
:style="{
|
|
height: `${props.yStep}rpx`,
|
|
}"
|
|
>
|
|
<text
|
|
class="relative block text-20rpx translate-y-[-50%] text-onTertiaryContainer line-height-none text-center"
|
|
:class="{
|
|
'translate-y-0': inex === 0,
|
|
}"
|
|
>
|
|
{{ item.label }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="relative flex flex-1 flex-col">
|
|
<!-- 网格 -->
|
|
<view
|
|
class="absolute left-0 top-0 z-1 w-full h-full"
|
|
:style="{
|
|
'background-image': `linear-gradient(to right, #f7f7f7 1rpx,
|
|
transparent 1rpx, transparent calc(100% / 7.5)),
|
|
linear-gradient(to bottom, #f7f7f7 1rpx, transparent 1rpx, transparent ${props.yStep}rpx)`,
|
|
'background-size': `calc(100%/7.5) ${props.yStep}rpx`,
|
|
}"
|
|
/>
|
|
<!-- 事件组 分为 7day -->
|
|
<view
|
|
class="z-2 flex flex-1 flex-row"
|
|
:style="{
|
|
width: `calc((100%/7.5)*7)`,
|
|
}"
|
|
>
|
|
<!--
|
|
:style="{
|
|
'background-color': item.date === selectedDate ? `rgb(var(--primary-100-color) / 0.2)` : 'transparent'
|
|
}"
|
|
-->
|
|
<view
|
|
v-for="(item, index) in props.weekDates"
|
|
:key="index"
|
|
class="relative flex flex-1 transition-background-color will-change-background-color"
|
|
>
|
|
<!-- 'background-color': addColorAlpha(itm.color, 0.06) -->
|
|
<view
|
|
v-for="itm in props.groupedEvents.get(item.date)"
|
|
:key="itm.id"
|
|
class="absolute w-full flex-shrink-0 cursor-pointer flex-self-stretch"
|
|
:style="{
|
|
top: `${itm.toTop}rpx`,
|
|
height: `${itm.height}rpx`,
|
|
background: itm.color,
|
|
borderTop: `0rpx solid ${itm.color}`,
|
|
}"
|
|
@click="emit('clickThing', itm)"
|
|
>
|
|
<text
|
|
class="text-28 !text-white"
|
|
:style="{
|
|
color: itm.color,
|
|
}"
|
|
>
|
|
{{ itm.name }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<style lang="scss" scoped></style>
|