Skip to content

Commit

Permalink
移植timeblock
Browse files Browse the repository at this point in the history
  • Loading branch information
hanaTsuk1 committed Jan 10, 2025
1 parent 4bd6aa1 commit 8cf6413
Show file tree
Hide file tree
Showing 14 changed files with 459 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ declare global {
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const dayFullMap: typeof import('./utils/shared')['dayFullMap']
const dayMap: typeof import('./utils/shared')['dayMap']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
Expand Down Expand Up @@ -404,6 +405,7 @@ declare module 'vue' {
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly dayFullMap: UnwrapRef<typeof import('./utils/shared')['dayFullMap']>
readonly dayMap: UnwrapRef<typeof import('./utils/shared')['dayMap']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
Expand Down
1 change: 1 addition & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ declare module '@vue/runtime-core' {
StatusBarTeleport: typeof import('./components/statusBar/StatusBarTeleport.vue')['default']
SyncDialog: typeof import('./components/dialog/SyncDialog.vue')['default']
TextSummary: typeof import('./components/widget/TextSummary.vue')['default']
TimeblockWeek: typeof import('./components/timeblock/TimeblockWeek.vue')['default']
TimeCard: typeof import('./components/widget/TimeCard.vue')['default']
TimelineActivity: typeof import('./components/timeline/graph/TimelineActivity.vue')['default']
TimelineFilter: typeof import('./components/timeline/TimelineFilter.vue')['default']
Expand Down
2 changes: 2 additions & 0 deletions src/components/dialog/HelpDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const content = computed(() => {
return `${t('help.chart.step1')}\n\n${t('help.chart.step2')}`
else if (props.page == 'timeline')
return `${t('help.timeline.step1')}\n\n${t('help.timeline.step2')}\n\n${t('help.timeline.step3')}`
else if (props.page == 'timeblock')
return `${t('help.timeblock.step1')}\n\n${t('help.timeblock.step2')}`
else if (props.page == 'manual')
return `${t('help.manual.step1')}\n\n${t('help.manual.step2')}\n\n${t('help.manual.step3')}`
else if (props.page == 'automatic')
Expand Down
14 changes: 14 additions & 0 deletions src/components/dialog/SettingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ const colorModeOptions = computed(() => [
</v-radio-group>
</v-list-item>
<v-divider />
<v-list-subheader>{{ $t('config.header.timeblock') }}</v-list-subheader>
<v-list-item>
<v-list-item-title>
{{ $t('config.timeblockMinMinute') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ $t('config.desc.timeblockMinMinute') }}
</v-list-item-subtitle>
<v-slider
v-model="config.timeblockMinMinute" px-4 py-2 thumb-label hide-details :min="1" :max="10"
:step="1" @touchmove.stop
/>
</v-list-item>
<v-divider />
<v-list-subheader>{{ $t('config.header.monitor') }}</v-list-subheader>
<v-list-item>
<v-list-item-title>
Expand Down
2 changes: 2 additions & 0 deletions src/components/statusBar/StatusBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const page = computed(() => {
return 'chart'
else if (path == '/timeline')
return 'timeline'
else if (path == '/timeblock')
return 'timeblock'
else if (path == '/collection/plan' || path == '/collection/label' || path == '/timer')
return 'manual'
else if (path == '/collection/monitor')
Expand Down
187 changes: 187 additions & 0 deletions src/components/timeblock/TimeblockWeek.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<script setup lang="ts">
import { addDays, getHours, getMinutes, isSameDay, isSameHour, isThisYear, isToday, setHours, subDays } from 'date-fns'
import { UseElementVisibility } from '@vueuse/components'
import type { TimeblockEventNode, TimeblockNode } from './types'
import { Pool } from './node'
const props = defineProps<{
date: Date
list: Array<TimeblockNode>
}>()
const { date: dateVModel } = useVModels(props)
const { locale } = useI18n()
const { formatHHmmss, format } = useDateFns()
const timeline = new Array(24).fill(0).map((_, i) => i)
const ratio = 1.5
const CELL_HEIGHT = 60
const calendar = ref<HTMLElement | null>(null)
const [switchDateVisible, toggleSwitchDateVisible] = useToggle()
const { y } = useScroll(calendar)
const weekdaysDate = computed(() => new Array(7).fill(0).map((_, i) => subDays(dateVModel.value, 6 - i)))
const weekdays = computed(() => weekdaysDate.value.map(i => i.getDay()))
const weekdaysName = computed(() => weekdays.value.map(i => dayFullMap[locale.value][i]))
const dataMap = computed(() => {
const pool = new Pool()
for (const node of props.list)
pool.add(node)
const eventNodeList = pool.calcLayout()
const map = new Map<number, Array<TimeblockEventNode>>()
for (const item of eventNodeList) {
for (const date of weekdaysDate.value) {
if (isSameDay(item.start, date)) {
const dateNum = date.getDate()
map.set(dateNum, [...map.get(dateNum) || [], item])
}
}
}
return map
})
function getBlockList(date: Date, hour: number) {
return (dataMap.value.get(date.getDate()) || []).filter(i => isSameHour(i.start, setHours(date, hour)))
}
function calcMinutes(num: number) {
return ~~(num / 1000 / 60)
}
function moveDate(offset: number) {
dateVModel.value = addDays(dateVModel.value, offset)
}
onMounted(() => {
const hour = getHours(Date.now())
const offset = 16
const top = CELL_HEIGHT * ratio * hour - offset
if (top) {
calendar.value?.scrollTo({
top,
})
}
})
</script>

<template>
<div ref="calendar" h-full overflow-y-auto>
<div sticky top-0 uno-card :class="y > 0 ? 'z-2 shadow-lg' : ''">
<div flex items-center py-1>
<div text-6 font-bold>
{{ format(dateVModel, isThisYear(dateVModel) ? 'MMMM' : 'yyyy/MM') }}
</div>
<div flex-1 />
<div flex items-center space-x-2 mr-1>
<v-btn icon size="x-small" @click="moveDate(-7)">
<div i-mdi:chevron-left text-5 />
</v-btn>
<v-btn color="primary" @click="() => toggleSwitchDateVisible()">
{{ isToday(dateVModel) ? $t('timeblock.today') : format(dateVModel, 'do') }}
</v-btn>
<v-btn icon size="x-small" @click="moveDate(7)">
<div i-mdi:chevron-right text-5 />
</v-btn>
</div>
</div>
<div flex class="[&>*]:flex-1">
<div />
<div
v-for="name, index in weekdaysName" :key="name" flex justify-center items-center space-x-2 flex-grow-2
h-12
>
<div>{{ weekdaysDate[index].getDate() }}</div>
<div>{{ name }}</div>
</div>
</div>
</div>
<div>
<div v-for="hour in timeline" :key="hour" flex class="[&>*]:flex-1">
<div text-center relative>
<div absolute class="top-[-12px]">
{{ `${complement(hour)}:00` }}
</div>
</div>
<div
v-for="currentDate in weekdaysDate" :key="currentDate.getDate()" flex-grow-2 class="border-[#d6dee1]"
border-r border-t relative :style="{
height: `${CELL_HEIGHT * ratio}px`,
}"
>
<div
v-for="{ start, end, canvasEnd, color, name, parallelCount, parallelLine } in getBlockList(currentDate, hour)"
:key="start" absolute z-1 overflow-hidden :style="{
width: `${100 / parallelCount}%`,
height: `${calcMinutes(canvasEnd - start) * ratio}px`,
top: `${getMinutes(start) * ratio}px`,
left: `${(parallelLine - 1) / parallelCount * 100}%`,
borderRadius: '3px',
}"
>
<UseElementVisibility v-slot="{ isVisible }">
<template v-if="isVisible">
<div
class="w-[3px]" absolute left-0 top-0 bottom-0 z-0 :style="{
backgroundColor: color,
}"
/>
<div
class="w-[calc(100%-3px)]" absolute right-0 top-0 bottom-0 z-0 :style="{
backgroundColor: color,
opacity: 0.2,
}"
/>
<div
h-full relative z-1 class="text-[10px] leading-[12px] p-[1px] pt-[2px] ml-[3px]" :style="{
color,
}"
>
<div :class="parallelCount == 1 ? 'flex' : ''">
<div :class="parallelCount == 1 ? 'max-w-[70%]' : 'max-w-full'" truncate font-bold>
{{ name }}
</div>
<div flex-1 />
<div v-if="parallelCount == 1 || calcMinutes(end - start) > ratio * 12">
{{ format(start, 'HH:mm') }}
</div>
</div>
</div>
<v-tooltip activator="parent">
<div flex>
<div>
{{ name }}
</div>
<div w-10 />
<div>{{ format(start, 'HH:mm') }}</div>
</div>
<div>
{{ formatHHmmss(end - start) }}
</div>
</v-tooltip>
</template>
</UseElementVisibility>
</div>
</div>
</div>
</div>
</div>
<advanced-dialog v-model:visible="switchDateVisible" class="w-[500px]!">
<v-confirm-edit
v-model="dateVModel" @save="() => toggleSwitchDateVisible()"
@cancel="() => toggleSwitchDateVisible()"
>
<template #default="{ model: proxyModel, actions }">
<v-date-picker v-model="proxyModel.value" color="primary" class="w-full!">
<template #actions>
<component :is="actions" />
</template>
</v-date-picker>
</template>
</v-confirm-edit>
</advanced-dialog>
</template>
86 changes: 86 additions & 0 deletions src/components/timeblock/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { isSameDay } from 'date-fns'

import type { TimeblockEventNode, TimeblockNode } from './types'

export class Pool {
#grid: TimeblockNode[][] = []
#current: TimeblockNode[] = []
#start = 0
#end = 0

add(node: TimeblockNode) {
if (this.#start == 0 && this.#end == 0) {
this.#start = node.start
this.#end = node.canvasEnd
this.#current.push(node)
return
}
if (this.#overlap(node)) {
this.#start = Math.min(this.#start, node.start)
this.#end = Math.max(this.#end, node.canvasEnd)
this.#current.push(node)
}
else {
this.#clear()
this.add(node)
}
}

calcLayout() {
this.#clear()
return this.#grid.flatMap(this.#calcList)
}

#clear() {
this.#grid.push(this.#current)
this.#current = []
this.#start = 0
this.#end = 0
}

#calcList(list: TimeblockNode[]): TimeblockEventNode[] {
if (list.length == 1) {
return list.map(node => ({
...node,
parallelCount: 1,
parallelLine: 1,
}))
}

let sort = list.flatMap(i => [i.start, i.canvasEnd]).sort((a, b) => a - b)
const data: TimeblockNode[][] = []
let temp: TimeblockNode[] = []
while (list.length) {
const index = list.findIndex(i => sort.includes(i.start) && sort.includes(i.canvasEnd))
const node = list[index]
if (node) {
temp.push(node)
list.splice(index, 1)
const startIndex = sort.indexOf(node.start)
const endIndex = sort.indexOf(node.canvasEnd)
sort = [...sort.slice(0, startIndex), ...sort.slice(endIndex + 1)]
}
else {
data.push(temp)
temp = []
sort = list.flatMap(i => [i.start, i.canvasEnd]).sort((a, b) => a - b)
}
}

data.push(temp)

return data.flatMap((list, index) => list.map(node => ({
...node,
parallelCount: data.length,
parallelLine: index + 1,
})))
}

#overlap(node: TimeblockNode) {
// 排除隔天并列
if (!isSameDay(node.start, this.#start))
return false

return (this.#start <= node.start && this.#end > node.start) || (this.#start < node.canvasEnd && this.#end > node.canvasEnd)
}
}
12 changes: 12 additions & 0 deletions src/components/timeblock/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface TimeblockNode {
start: number
end: number
name: string
color: string
canvasEnd: number
}

export interface TimeblockEventNode extends TimeblockNode {
parallelCount: number
parallelLine: number
}
Loading

0 comments on commit 8cf6413

Please sign in to comment.