diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 1be7fb9..e58d61f 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -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'] @@ -404,6 +405,7 @@ declare module 'vue' { readonly createTemplatePromise: UnwrapRef readonly createUnrefFn: UnwrapRef readonly customRef: UnwrapRef + readonly dayFullMap: UnwrapRef readonly dayMap: UnwrapRef readonly debouncedRef: UnwrapRef readonly debouncedWatch: UnwrapRef diff --git a/src/components.d.ts b/src/components.d.ts index 7455c54..34966f8 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -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'] diff --git a/src/components/dialog/HelpDialog.vue b/src/components/dialog/HelpDialog.vue index 301f666..bb56886 100644 --- a/src/components/dialog/HelpDialog.vue +++ b/src/components/dialog/HelpDialog.vue @@ -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') diff --git a/src/components/dialog/SettingDialog.vue b/src/components/dialog/SettingDialog.vue index 55e8aee..0f06195 100644 --- a/src/components/dialog/SettingDialog.vue +++ b/src/components/dialog/SettingDialog.vue @@ -184,6 +184,20 @@ const colorModeOptions = computed(() => [ + {{ $t('config.header.timeblock') }} + + + {{ $t('config.timeblockMinMinute') }} + + + {{ $t('config.desc.timeblockMinMinute') }} + + + + {{ $t('config.header.monitor') }} diff --git a/src/components/statusBar/StatusBar.vue b/src/components/statusBar/StatusBar.vue index 8eddb52..639063a 100644 --- a/src/components/statusBar/StatusBar.vue +++ b/src/components/statusBar/StatusBar.vue @@ -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') diff --git a/src/components/timeblock/TimeblockWeek.vue b/src/components/timeblock/TimeblockWeek.vue new file mode 100644 index 0000000..a8cd9b9 --- /dev/null +++ b/src/components/timeblock/TimeblockWeek.vue @@ -0,0 +1,187 @@ + + + diff --git a/src/components/timeblock/node.ts b/src/components/timeblock/node.ts new file mode 100644 index 0000000..05c6ee9 --- /dev/null +++ b/src/components/timeblock/node.ts @@ -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) + } +} diff --git a/src/components/timeblock/types.ts b/src/components/timeblock/types.ts new file mode 100644 index 0000000..6a2b478 --- /dev/null +++ b/src/components/timeblock/types.ts @@ -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 +} diff --git a/src/locales/en-US.yaml b/src/locales/en-US.yaml index c7fd3a8..cc7c80b 100644 --- a/src/locales/en-US.yaml +++ b/src/locales/en-US.yaml @@ -1,6 +1,7 @@ nav: overview: Overview timeline: Timeline + timeblock: Timeblock plan: Plan timer: Timer label: Label @@ -26,6 +27,7 @@ config: serverPort: Port timelineMinMinute: Minimum time timelineGroupGapMinute: Grouping Intervals + timeblockMinMinute: Minimum time themeColor: Theme color colorMode: Theme Mode watcherWhitelist: Monitor whitelist @@ -38,6 +40,7 @@ config: timelineGroupGapMinute: When displaying the timeline, separate segments of the same type that are separated by the current minute watcherWhitelist: Programs that are in the following folders and have not been added will be added when running faviconService: Select service providers in the timeline for website icon requests + timeblockMinMinute: When displaying the timeblock, filter out segments smaller than the current minute scheduledExportPath: Click to select the save path runAsAdmin: Start the computer as an admin to increase the permissions of the automatic monitoring restartService: After changing the port, click to restart the service @@ -52,6 +55,7 @@ config: service: Service header: timeline: Timeline + timeblock: Timeblock monitor: Monitor export: period: @@ -368,6 +372,7 @@ statusBar: dimension: Dimension organization chart: Chart display timeline: Timeline display + timeblock: Timeblock display listMode: text: List mode tooltip: @@ -462,6 +467,8 @@ watcherWhitelist: duplicate: Cannot add repeatedly refresh: error: Error occurred during refresh +timeblock: + today: Today extension: tab: dandanplay: Dandanplay @@ -495,6 +502,9 @@ help: step1: The timeline records all the data that occurred in a day in detail. Use the calendar on the right to jump to other dates at any time, or you can **click icon** to operate on the data step2: There are many operations in the bottom bar under the timeline, you can **filter**, **expand**, **search** and other functions step3: For a general introduction to the timeline, go to [link](https://shion.app/guide/display#timeline) + timeblock: + step1: The time block roughly displays the recent data + step2: For a general introduction to the time block, please go to [link](https://shion.app/guide/display#timeblock) manual: step1: You need to create a **plan** first, then create a **label** under the plan, and then you can start **recording** step2: For example, you create a **reading** plan, and a **specific book** is the label, and the record is **an activity** diff --git a/src/locales/zh-CN.yaml b/src/locales/zh-CN.yaml index 16ac13b..182994c 100644 --- a/src/locales/zh-CN.yaml +++ b/src/locales/zh-CN.yaml @@ -1,6 +1,7 @@ nav: overview: 总览 timeline: 时间线 + timeblock: 时间块 plan: 计划 timer: 计时 label: 标签 @@ -26,6 +27,7 @@ config: serverPort: 端口号 timelineMinMinute: 最小时间 timelineGroupGapMinute: 分组间隔 + timeblockMinMinute: 最小时间 themeColor: 主题色 colorMode: 主题模式 watcherWhitelist: 自动监听白名单 @@ -38,6 +40,7 @@ config: timelineGroupGapMinute: 时间线显示时,分离同类型相隔当前分钟的片段 watcherWhitelist: 在下列文件夹中且未添加的程序,运行时自动加入监听 faviconService: 选择时间线中进行网站图标请求的服务提供者 + timeblockMinMinute: 时间块显示时,筛选掉小于当前分钟的片段 scheduledExportPath: 点击选择保存路径 runAsAdmin: 以管理员身份开机启动,提高自动监听获取窗口的权限 restartService: 更改端口号后,点击重启服务 @@ -52,6 +55,7 @@ config: service: 第三方服务 header: timeline: 时间线 + timeblock: 时间块 monitor: 自动监听 export: period: @@ -368,6 +372,7 @@ statusBar: dimension: 维度组织 chart: 图表展示 timeline: 时间线展示 + timeblock: 时间块展示 listMode: text: 列表模式 tooltip: @@ -462,6 +467,8 @@ watcherWhitelist: duplicate: 不能重复添加 refresh: error: 刷新时发生错误 +timeblock: + today: 今天 extension: tab: dandanplay: 弹弹play @@ -495,6 +502,9 @@ help: step1: 时间线细致地记录了一天发生的所有数据,配合右侧的日历随时跳转到其他日期,也可以**点击图标**对数据进行操作 step2: 时间线下的底部栏有着许多操作,你可以进行**筛选**、**展开**、**搜索**等等功能 step3: 时间线的大致介绍可以前往[链接](https://shion.app/zh/guide/display#timeline) + timeblock: + step1: 时间块大致展示最近的数据 + step2: 时间块的大致介绍,可以前往[链接](https://shion.app/zh/guide/display#timeblock) manual: step1: 你需要先创建一个**计划**,然后在计划之下创建一个**标签**,随后便可以开始进行**记录** step2: 举个例子,你创建了一个**读书**的计划,而一本**具体的书**便是标签,记录则是**一次活动** diff --git a/src/pages/timeblock.vue b/src/pages/timeblock.vue new file mode 100644 index 0000000..302b0e1 --- /dev/null +++ b/src/pages/timeblock.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/stores/useConfigStore.ts b/src/stores/useConfigStore.ts index 3121e4c..a8220b6 100644 --- a/src/stores/useConfigStore.ts +++ b/src/stores/useConfigStore.ts @@ -16,6 +16,7 @@ interface Config { launchVisible: boolean timelineMinMinute: number timelineGroupGapMinute: number + timeblockMinMinute: number themeColor: string tour: boolean autoShowChangelogDisable: boolean @@ -54,6 +55,7 @@ export const useConfigStore = defineStore('config', () => { launchVisible: true, timelineMinMinute: 0, timelineGroupGapMinute: 30, + timeblockMinMinute: 1, themeColor: '#512DA8', tour: true, autoShowChangelogDisable: false, diff --git a/src/stores/useNavStore.ts b/src/stores/useNavStore.ts index 44f537f..7944ad0 100644 --- a/src/stores/useNavStore.ts +++ b/src/stores/useNavStore.ts @@ -28,6 +28,13 @@ export const useNavStore = defineStore('nav', () => { to: '/timeline', key: 'timeline', }, + { + icon: 'i-mdi:timeline-outline', + activeIcon: 'i-mdi:timeline', + name: t('nav.timeblock'), + to: '/timeblock', + key: 'timeblock', + }, { icon: 'i-mdi:database-outline', activeIcon: 'i-mdi:database', diff --git a/src/utils/shared.ts b/src/utils/shared.ts index cb6a65f..62f8cf9 100644 --- a/src/utils/shared.ts +++ b/src/utils/shared.ts @@ -16,3 +16,8 @@ export const dayMap = { 'zh-CN': ['日', '一', '二', '三', '四', '五', '六'], 'en-US': ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], } + +export const dayFullMap = { + 'zh-CN': ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], + 'en-US': ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], +}