feat: 新增视图切换状态管理及颜色/日期工具

- date.js: 新增 startOfWeek, startOfMonth, hhmmToMinutes 辅助函数
- colors.js (新增): 基于 uid 哈希的事件颜色分配, 12 色调色板
- useCalendar.js: 新增 viewMode (edit/month/week) 和 previewDate 状态,
  以及 setViewMode/prevMonth/nextMonth/prevWeek/nextWeek 操作
This commit is contained in:
zikai
2026-07-22 04:04:29 +00:00
parent 50fc8de9eb
commit f4c11bdc3e
3 changed files with 112 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { parseICS, serializeICS } from '../lib/ical-io.js'
import { organize as organizeEvents } from '../lib/organize.js' import { organize as organizeEvents } from '../lib/organize.js'
import { createEvent, cloneEvent } from '../lib/event-factory.js' import { createEvent, cloneEvent } from '../lib/event-factory.js'
import { splitRecurringEvent } from '../lib/split.js' import { splitRecurringEvent } from '../lib/split.js'
import { todayISO, addDays, startOfMonth, startOfWeek, parseISODate, pad2 } from '../lib/date.js'
const MAX_HISTORY = 50 const MAX_HISTORY = 50
@@ -15,6 +16,8 @@ const state = reactive({
redoStack: [], redoStack: [],
fileName: null, fileName: null,
dirty: false, dirty: false,
viewMode: 'edit', // 'edit' | 'month' | 'week'
previewDate: todayISO(), // 预览锚定日期 (月预览取当月, 周预览取当周)
}) })
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
@@ -142,6 +145,33 @@ function markSaved() {
state.dirty = false state.dirty = false
} }
// ------------------------------------------------------------------ //
// 视图切换 / 预览导航
// ------------------------------------------------------------------ //
function setViewMode(mode) {
state.viewMode = mode
}
function prevMonth() {
const { year, month } = parseISODate(startOfMonth(state.previewDate))
const d = new Date(year, month - 2, 1) // month-2 = 上个月 (0 基)
state.previewDate = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-01`
}
function nextMonth() {
const { year, month } = parseISODate(startOfMonth(state.previewDate))
const d = new Date(year, month, 1) // month = 下个月 (0 基)
state.previewDate = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-01`
}
function prevWeek() {
state.previewDate = addDays(state.previewDate, -7)
}
function nextWeek() {
state.previewDate = addDays(state.previewDate, 7)
}
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
// 计算属性 // 计算属性
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
@@ -177,5 +207,10 @@ export function useCalendar() {
redo, redo,
serialize, serialize,
markSaved, markSaved,
setViewMode,
prevMonth,
nextMonth,
prevWeek,
nextWeek,
} }
} }

44
src/lib/colors.js Normal file
View File

@@ -0,0 +1,44 @@
/**
* 事件颜色分配工具。
*
* 基于事件 uid 的哈希值从预定义调色板中选取颜色, 确保相同事件始终
* 同色, 不同事件大概率不同色。预定义调色板保证颜色区分度高且可读。
*/
// 12 种区分度高的颜色, 每种包含背景色、边框色、文字色
const PALETTE = [
{ bg: '#e3f2fd', border: '#1976d2', text: '#0d47a1' }, // 蓝
{ bg: '#f3e5f5', border: '#7b1fa2', text: '#4a148c' }, // 紫
{ bg: '#e8f5e9', border: '#388e3c', text: '#1b5e20' }, // 绿
{ bg: '#fff3e0', border: '#e65100', text: '#bf360c' }, // 橙
{ bg: '#fce4ec', border: '#c2185b', text: '#880e4f' }, // 粉
{ bg: '#e0f7fa', border: '#00838f', text: '#006064' }, // 青
{ bg: '#f1f8e9', border: '#827717', text: '#33691e' }, // 黄绿
{ bg: '#ede7f6', border: '#512da8', text: '#311b92' }, // 靛
{ bg: '#fbe9e7', border: '#d84315', text: '#bf360c' }, // 红橙
{ bg: '#e0f2f1', border: '#00695c', text: '#004d40' }, // 蓝绿
{ bg: '#fff8e1', border: '#f9a825', text: '#f57f17' }, // 金
{ bg: '#f9fbe7', border: '#afb42b', text: '#827717' }, // 橄榄
]
/**
* 对字符串进行简单哈希, 返回非负整数。
* @param {string} str
* @returns {number}
*/
function hashStr(str) {
let h = 0
for (let i = 0; i < str.length; i++) {
h = (h * 31 + str.charCodeAt(i)) | 0
}
return Math.abs(h)
}
/**
* 根据 uid 返回调色板中的颜色对象。
* @param {string} uid
* @returns {{ bg: string, border: string, text: string }}
*/
export function eventColor(uid) {
return PALETTE[hashStr(uid) % PALETTE.length]
}

View File

@@ -56,3 +56,36 @@ export function nowCompactTimestamp() {
export function toHHMM(date) { export function toHHMM(date) {
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}` return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
} }
/**
* 返回 isoDate 所在周的周一 (ISO 周, 周一为首日)。
* @param {string} isoDate - 'YYYY-MM-DD'
* @returns {string} 周一的 'YYYY-MM-DD'
*/
export function startOfWeek(isoDate) {
const d = toDate(isoDate)
const dow = d.getDay() // 0=周日 .. 6=周六
const diff = dow === 0 ? -6 : 1 - dow // 回退到周一
d.setDate(d.getDate() + diff)
return toISODate(d)
}
/**
* 返回 isoDate 所在月的第一天。
* @param {string} isoDate - 'YYYY-MM-DD'
* @returns {string} 当月 1 号的 'YYYY-MM-DD'
*/
export function startOfMonth(isoDate) {
const { year, month } = parseISODate(isoDate)
return `${year}-${pad2(month)}-01`
}
/**
* 将 'HH:MM' 转换为当天分钟数 (0~1439)。
* @param {string} hhmm - 'HH:MM'
* @returns {number}
*/
export function hhmmToMinutes(hhmm) {
const [h, m] = hhmm.split(':').map(Number)
return h * 60 + m
}