refactor: 移除重做/应用按钮, 自动应用更改, 删除死代码, 修复性能问题
UI 变更: - 移除重做按钮及 redo/canRedo/redoStack 相关逻辑 (useCalendar, HeaderBar) - 移除应用更改按钮, EventDetail 改为 watch form 自动应用 (deep watch) - 用 syncing 标志防止 syncForm 回写时触发自动应用的循环 死代码清理: - weekday.js: 移除未使用的 BYDAY_TO_INDEX 和 dowLabel - date.js: 移除未使用的 toHHMM - App.vue: 移除未使用的 .empty CSS - DropZone.vue: 移除空的 .icon 容器及 CSS - test/weekday.test.js: 移除对应死代码的 2 个测试用例 性能修复: - WeekPreview: layoutEvents 从模板内联调用改为 computed (laidOutByDate), 避免每次渲染重新计算布局 - MonthPreview: occByDate 过滤为仅网格内 42 天, 避免展开全部 occurrence 代码质量: - useCalendar: prevMonth/nextMonth 合并为 shiftMonth(delta), 消除重复 - WeekPreview/MonthPreview: 移除未使用导入 (toISODate, toDate)
This commit is contained in:
@@ -91,9 +91,4 @@ body {
|
||||
}
|
||||
.app-main { flex: 1; display: flex; overflow: hidden; }
|
||||
.detail-area { flex: 1; overflow-y: auto; }
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #95a5a6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,6 @@ function onPick(e) {
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<div class="dropzone-content">
|
||||
<div class="icon"></div>
|
||||
<p class="hint">拖入 .ics 文件,或</p>
|
||||
<label class="pick-btn">
|
||||
点击选择文件
|
||||
@@ -68,10 +67,6 @@ function onPick(e) {
|
||||
.dropzone-content {
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hint {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useCalendar } from '../composables/useCalendar.js'
|
||||
import { DOW, DOW_EN, bydayFromDate } from '../lib/weekday.js'
|
||||
import { toDate } from '../lib/date.js'
|
||||
@@ -12,8 +12,12 @@ const form = ref({})
|
||||
const showSplit = ref(false)
|
||||
const splitDate = ref('')
|
||||
|
||||
// 标记是否正在由选中事件同步表单, 避免 watch form 时回写
|
||||
let syncing = false
|
||||
|
||||
function syncForm(ev) {
|
||||
if (!ev) { form.value = {}; return }
|
||||
syncing = true
|
||||
if (!ev) { form.value = {}; syncing = false; return }
|
||||
form.value = {
|
||||
summary: ev.summary || '',
|
||||
location: ev.location || '',
|
||||
@@ -26,11 +30,14 @@ function syncForm(ev) {
|
||||
byday: ev.rrule?.byday || 'MO',
|
||||
until_date: ev.rrule?.untilDate || '',
|
||||
}
|
||||
syncing = false
|
||||
}
|
||||
|
||||
watch(selectedEvent, syncForm, { immediate: true })
|
||||
|
||||
function applyChanges() {
|
||||
// 自动应用: 表单变化时立即写回 store
|
||||
watch(form, () => {
|
||||
if (syncing) return
|
||||
const ev = selectedEvent.value
|
||||
if (!ev) return
|
||||
const patch = {
|
||||
@@ -58,7 +65,7 @@ function applyChanges() {
|
||||
patch.exdates = []
|
||||
}
|
||||
updateEvent(ev.uid, patch)
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
function onDelete() {
|
||||
const ev = selectedEvent.value
|
||||
@@ -166,7 +173,6 @@ function onToggleOcc(date) {
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" @click="applyChanges">应用更改</button>
|
||||
<button
|
||||
v-if="selectedEvent.rrule"
|
||||
class="btn btn-secondary"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useCalendar } from '../composables/useCalendar.js'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { state, canUndo, canRedo, organize, undo, redo, serialize, markSaved, loadFile } = useCalendar()
|
||||
const { state, canUndo, organize, undo, serialize, markSaved, loadFile } = useCalendar()
|
||||
const fileInput = ref(null)
|
||||
|
||||
function onOrganize() {
|
||||
@@ -44,7 +44,6 @@ function onDownload() {
|
||||
整理去重
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="undo" :disabled="!canUndo">撤销</button>
|
||||
<button class="btn btn-secondary" @click="redo" :disabled="!canRedo">重做</button>
|
||||
<button class="btn btn-secondary" @click="onImport">导入文件</button>
|
||||
<button class="btn btn-success" @click="onDownload" :disabled="state.events.length === 0">
|
||||
下载 ICS
|
||||
|
||||
@@ -4,41 +4,42 @@ import { useCalendar } from '../composables/useCalendar.js'
|
||||
import { expandOccurrences } from '../lib/recur.js'
|
||||
import { eventColor } from '../lib/colors.js'
|
||||
import {
|
||||
startOfMonth, startOfWeek, addDays, toISODate, toDate,
|
||||
parseISODate, todayISO,
|
||||
startOfMonth, startOfWeek, addDays, parseISODate, todayISO,
|
||||
} from '../lib/date.js'
|
||||
import { DOW } from '../lib/weekday.js'
|
||||
|
||||
const { state, prevMonth, nextMonth } = useCalendar()
|
||||
|
||||
// 表头: 周一~周日
|
||||
const headers = DOW.slice(1).concat(DOW[0]) // 周一..周日
|
||||
const headers = DOW.slice(1).concat(DOW[0])
|
||||
|
||||
// 当前预览月的年月标签
|
||||
const monthLabel = computed(() => {
|
||||
const { year, month } = parseISODate(startOfMonth(state.previewDate))
|
||||
return `${year} 年 ${month} 月`
|
||||
const monthLabel = computed(function () {
|
||||
const parts = parseISODate(startOfMonth(state.previewDate))
|
||||
return parts.year + ' 年 ' + parts.month + ' 月'
|
||||
})
|
||||
|
||||
// 构建月历网格 (6 行 x 7 列, 从当月首日所在周的周一开始)
|
||||
const gridDays = computed(() => {
|
||||
const gridDays = computed(function () {
|
||||
const monthStart = startOfMonth(state.previewDate)
|
||||
const gridStart = startOfWeek(monthStart)
|
||||
const days = []
|
||||
for (let i = 0; i < 42; i++) {
|
||||
for (var i = 0; i < 42; i++) {
|
||||
days.push(addDays(gridStart, i))
|
||||
}
|
||||
return days
|
||||
})
|
||||
|
||||
// 按日期分组所有 occurrence, 过滤掉 skipped
|
||||
// 按日期分组 occurrence, 仅保留网格内的日期, 过滤掉 skipped
|
||||
// 返回 Map<dateStr, Array<{ event, date }>>
|
||||
const occByDate = computed(() => {
|
||||
const occByDate = computed(function () {
|
||||
const gridSet = new Set(gridDays.value)
|
||||
const map = new Map()
|
||||
for (const ev of state.events) {
|
||||
const occs = expandOccurrences(ev)
|
||||
for (const occ of occs) {
|
||||
if (occ.skipped) continue
|
||||
if (!gridSet.has(occ.date)) continue
|
||||
if (!map.has(occ.date)) map.set(occ.date, [])
|
||||
map.get(occ.date).push({ event: ev, date: occ.date })
|
||||
}
|
||||
@@ -46,8 +47,6 @@ const occByDate = computed(() => {
|
||||
return map
|
||||
})
|
||||
|
||||
const today = todayISO()
|
||||
|
||||
function isInMonth(dateStr) {
|
||||
const target = parseISODate(state.previewDate)
|
||||
const day = parseISODate(dateStr)
|
||||
@@ -58,17 +57,16 @@ function colorStyle(uid) {
|
||||
const c = eventColor(uid)
|
||||
return {
|
||||
backgroundColor: c.bg,
|
||||
borderLeft: `3px solid ${c.border}`,
|
||||
borderLeft: '3px solid ' + c.border,
|
||||
color: c.text,
|
||||
}
|
||||
}
|
||||
|
||||
// tooltip 详细信息
|
||||
function tooltip(ev) {
|
||||
const parts = [ev.summary || '(无标题)']
|
||||
parts.push(`${ev.dtstartTime} - ${ev.dtendTime}`)
|
||||
if (ev.location) parts.push(`地点: ${ev.location}`)
|
||||
if (ev.description) parts.push(`描述: ${ev.description}`)
|
||||
parts.push(ev.dtstartTime + ' - ' + ev.dtendTime)
|
||||
if (ev.location) parts.push('地点: ' + ev.location)
|
||||
if (ev.description) parts.push('描述: ' + ev.description)
|
||||
return parts.join('\n')
|
||||
}
|
||||
</script>
|
||||
@@ -88,7 +86,7 @@ function tooltip(ev) {
|
||||
class="grid-cell"
|
||||
:class="{
|
||||
'out-of-month': !isInMonth(dateStr),
|
||||
'is-today': dateStr === today,
|
||||
'is-today': dateStr === todayISO(),
|
||||
}"
|
||||
>
|
||||
<span class="day-num">{{ parseISODate(dateStr).day }}</span>
|
||||
|
||||
@@ -12,32 +12,29 @@ const { state, prevWeek, nextWeek } = useCalendar()
|
||||
|
||||
const HOUR_HEIGHT = 48 // 每小时像素高度
|
||||
const headers = DOW.slice(1).concat(DOW[0]) // 周一..周日
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i)
|
||||
const hours = Array.from({ length: 24 }, function (_, i) { return i })
|
||||
|
||||
// 当前周的 7 天日期 (周一~周日)
|
||||
const weekDays = computed(() => {
|
||||
const weekDays = computed(function () {
|
||||
const ws = startOfWeek(state.previewDate)
|
||||
return Array.from({ length: 7 }, (_, i) => addDays(ws, i))
|
||||
return Array.from({ length: 7 }, function (_, i) { return addDays(ws, i) })
|
||||
})
|
||||
|
||||
// 周范围标签
|
||||
const weekLabel = computed(() => {
|
||||
const weekLabel = computed(function () {
|
||||
const days = weekDays.value
|
||||
const first = days[0]
|
||||
const last = days[6]
|
||||
const f = parseISODate(first)
|
||||
const l = parseISODate(last)
|
||||
return `${f.year}/${f.month}/${f.day} - ${l.year}/${l.month}/${l.day}`
|
||||
const f = parseISODate(days[0])
|
||||
const l = parseISODate(days[6])
|
||||
return f.year + '/' + f.month + '/' + f.day + ' - ' + l.year + '/' + l.month + '/' + l.day
|
||||
})
|
||||
|
||||
const today = todayISO()
|
||||
|
||||
// 收集本周所有 occurrence, 按日期分组
|
||||
// 每条记录: { event, date, startMin, endMin }
|
||||
const occByDate = computed(() => {
|
||||
const map = new Map()
|
||||
// 收集本周所有 occurrence 并做并排布局, 按日期分组
|
||||
// 返回 Map<dateStr, Array<layoutItem>>
|
||||
// layoutItem: { event, date, startMin, endMin, col, totalCols }
|
||||
const laidOutByDate = computed(function () {
|
||||
const weekSet = new Set(weekDays.value)
|
||||
const rawByDate = new Map()
|
||||
|
||||
for (const ev of state.events) {
|
||||
const occs = expandOccurrences(ev)
|
||||
for (const occ of occs) {
|
||||
@@ -45,34 +42,45 @@ const occByDate = computed(() => {
|
||||
if (!weekSet.has(occ.date)) continue
|
||||
const startMin = hhmmToMinutes(ev.dtstartTime)
|
||||
const endMin = hhmmToMinutes(ev.dtendTime)
|
||||
if (!map.has(occ.date)) map.set(occ.date, [])
|
||||
map.get(occ.date).push({ event: ev, date: occ.date, startMin, endMin })
|
||||
if (!rawByDate.has(occ.date)) rawByDate.set(occ.date, [])
|
||||
rawByDate.get(occ.date).push({ event: ev, date: occ.date, startMin: startMin, endMin: endMin })
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
// 对同一天的事件做并排布局: 检测重叠并分配列
|
||||
function layoutEvents(items) {
|
||||
if (items.length === 0) return []
|
||||
// 按开始时间排序
|
||||
const sorted = items.slice().sort(function (a, b) { return a.startMin - b.startMin })
|
||||
// 简单重叠分列: 逐个分配到第一个不冲突的列
|
||||
const placed = [] // { item, col, totalCols }
|
||||
for (const item of sorted) {
|
||||
let col = 0
|
||||
// 对每天的事件做并排布局: 检测重叠并分配列
|
||||
const result = new Map()
|
||||
for (const entry of rawByDate) {
|
||||
var date = entry[0]
|
||||
var items = entry[1]
|
||||
if (items.length === 0) { result.set(date, []); continue }
|
||||
var sorted = items.slice().sort(function (a, b) { return a.startMin - b.startMin })
|
||||
var placed = [] // { item, col }
|
||||
for (var si = 0; si < sorted.length; si++) {
|
||||
var item = sorted[si]
|
||||
var col = 0
|
||||
while (true) {
|
||||
const conflict = placed.find(
|
||||
(p) => p.col === col && p.item.startMin < item.endMin && item.startMin < p.item.endMin,
|
||||
)
|
||||
var conflict = null
|
||||
for (var pi = 0; pi < placed.length; pi++) {
|
||||
var p = placed[pi]
|
||||
if (p.col === col && p.item.startMin < item.endMin && item.startMin < p.item.endMin) {
|
||||
conflict = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!conflict) break
|
||||
col++
|
||||
}
|
||||
placed.push({ item, col })
|
||||
placed.push({ item: item, col: col })
|
||||
}
|
||||
const maxCol = Math.max.apply(null, placed.map(function (p) { return p.col }).concat([0]))
|
||||
return placed.map(function (p) { return Object.assign({}, p.item, { col: p.col, totalCols: maxCol + 1 }) })
|
||||
var maxCol = 0
|
||||
for (var mi = 0; mi < placed.length; mi++) { if (placed[mi].col > maxCol) maxCol = placed[mi].col }
|
||||
var laid = placed.map(function (p) {
|
||||
return Object.assign({}, p.item, { col: p.col, totalCols: maxCol + 1 })
|
||||
})
|
||||
result.set(date, laid)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function eventStyle(occ) {
|
||||
const c = eventColor(occ.event.uid)
|
||||
@@ -80,21 +88,21 @@ function eventStyle(occ) {
|
||||
const height = Math.max((occ.endMin - occ.startMin) * (HOUR_HEIGHT / 60), 18)
|
||||
const widthPct = 100 / occ.totalCols
|
||||
return {
|
||||
top: `${top}px`,
|
||||
height: `${height}px`,
|
||||
left: `${occ.col * widthPct}%`,
|
||||
width: `${widthPct}%`,
|
||||
top: top + 'px',
|
||||
height: height + 'px',
|
||||
left: (occ.col * widthPct) + '%',
|
||||
width: widthPct + '%',
|
||||
backgroundColor: c.bg,
|
||||
borderLeft: `3px solid ${c.border}`,
|
||||
borderLeft: '3px solid ' + c.border,
|
||||
color: c.text,
|
||||
}
|
||||
}
|
||||
|
||||
function tooltip(ev) {
|
||||
const parts = [ev.summary || '(无标题)']
|
||||
parts.push(`${ev.dtstartTime} - ${ev.dtendTime}`)
|
||||
if (ev.location) parts.push(`地点: ${ev.location}`)
|
||||
if (ev.description) parts.push(`描述: ${ev.description}`)
|
||||
parts.push(ev.dtstartTime + ' - ' + ev.dtendTime)
|
||||
if (ev.location) parts.push('地点: ' + ev.location)
|
||||
if (ev.description) parts.push('描述: ' + ev.description)
|
||||
return parts.join('\n')
|
||||
}
|
||||
</script>
|
||||
@@ -107,7 +115,6 @@ function tooltip(ev) {
|
||||
<button class="nav-btn" @click="nextWeek">下一周</button>
|
||||
</div>
|
||||
<div class="week-grid-wrapper">
|
||||
<!-- 时间轴 + 7 列网格 -->
|
||||
<div class="week-grid">
|
||||
<!-- 左侧时间标签列 -->
|
||||
<div class="time-col">
|
||||
@@ -118,21 +125,19 @@ function tooltip(ev) {
|
||||
</div>
|
||||
<!-- 7 天列 -->
|
||||
<div v-for="(dateStr, idx) in weekDays" :key="dateStr" class="day-col">
|
||||
<div class="day-header" :class="{ 'is-today': dateStr === today }">
|
||||
<div class="day-header" :class="{ 'is-today': dateStr === todayISO() }">
|
||||
{{ headers[idx] }}
|
||||
<span class="day-date">{{ parseISODate(dateStr).day }}</span>
|
||||
</div>
|
||||
<div class="day-body" :style="{ height: 24 * HOUR_HEIGHT + 'px' }">
|
||||
<!-- 背景网格线 -->
|
||||
<div
|
||||
v-for="h in hours"
|
||||
:key="h"
|
||||
class="hour-line"
|
||||
:style="{ top: h * HOUR_HEIGHT + 'px', height: HOUR_HEIGHT + 'px' }"
|
||||
></div>
|
||||
<!-- 事件块 -->
|
||||
<div
|
||||
v-for="occ in layoutEvents(occByDate.get(dateStr) || [])"
|
||||
v-for="occ in (laidOutByDate.get(dateStr) || [])"
|
||||
:key="occ.event.uid + occ.date"
|
||||
class="event-block"
|
||||
:style="eventStyle(occ)"
|
||||
@@ -195,7 +200,7 @@ function tooltip(ev) {
|
||||
border-right: 1px solid #e0e3e6;
|
||||
}
|
||||
.time-spacer {
|
||||
height: 40px; /* 与 day-header 等高 */
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #e0e3e6;
|
||||
}
|
||||
.time-slot {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { parseICS, serializeICS } from '../lib/ical-io.js'
|
||||
import { organize as organizeEvents } from '../lib/organize.js'
|
||||
import { createEvent, cloneEvent } from '../lib/event-factory.js'
|
||||
import { splitRecurringEvent } from '../lib/split.js'
|
||||
import { todayISO, addDays, startOfMonth, startOfWeek, parseISODate, pad2 } from '../lib/date.js'
|
||||
import { todayISO, addDays, startOfMonth, parseISODate, pad2 } from '../lib/date.js'
|
||||
|
||||
const MAX_HISTORY = 50
|
||||
|
||||
@@ -13,7 +13,6 @@ const state = reactive({
|
||||
events: [],
|
||||
selectedUid: null,
|
||||
history: [],
|
||||
redoStack: [],
|
||||
fileName: null,
|
||||
dirty: false,
|
||||
viewMode: 'edit', // 'edit' | 'month' | 'week'
|
||||
@@ -34,7 +33,6 @@ function snapshot() {
|
||||
function pushHistory() {
|
||||
state.history.push(snapshot())
|
||||
if (state.history.length > MAX_HISTORY) state.history.shift()
|
||||
state.redoStack = []
|
||||
}
|
||||
|
||||
function restore(snap) {
|
||||
@@ -44,7 +42,7 @@ function restore(snap) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 操作
|
||||
// 文件加载
|
||||
// ------------------------------------------------------------------ //
|
||||
function loadFile(file) {
|
||||
return file.text().then((text) => {
|
||||
@@ -53,12 +51,14 @@ function loadFile(file) {
|
||||
state.events = parsed.events
|
||||
state.selectedUid = null
|
||||
state.history = []
|
||||
state.redoStack = []
|
||||
state.fileName = file.name
|
||||
state.dirty = false
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 事件操作
|
||||
// ------------------------------------------------------------------ //
|
||||
function selectEvent(uid) {
|
||||
state.selectedUid = uid
|
||||
}
|
||||
@@ -72,6 +72,10 @@ function organize() {
|
||||
return result.stats
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定事件的字段 (自动记录历史)。
|
||||
* 供 EventDetail 的自动应用 (watch) 调用。
|
||||
*/
|
||||
function updateEvent(uid, patch) {
|
||||
pushHistory()
|
||||
const ev = state.events.find((e) => e.uid === uid)
|
||||
@@ -127,16 +131,12 @@ function toggleOccurrence(uid, date) {
|
||||
|
||||
function undo() {
|
||||
if (state.history.length === 0) return
|
||||
state.redoStack.push(snapshot())
|
||||
restore(state.history.pop())
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (state.redoStack.length === 0) return
|
||||
state.history.push(snapshot())
|
||||
restore(state.redoStack.pop())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 序列化
|
||||
// ------------------------------------------------------------------ //
|
||||
function serialize() {
|
||||
return serializeICS({ meta: state.meta, events: state.events })
|
||||
}
|
||||
@@ -152,16 +152,18 @@ function setViewMode(mode) {
|
||||
state.viewMode = mode
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
function shiftMonth(delta) {
|
||||
const { year, month } = parseISODate(startOfMonth(state.previewDate))
|
||||
const d = new Date(year, month - 2, 1) // month-2 = 上个月 (0 基)
|
||||
const d = new Date(year, month - 1 + delta, 1)
|
||||
state.previewDate = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-01`
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
shiftMonth(-1)
|
||||
}
|
||||
|
||||
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`
|
||||
shiftMonth(1)
|
||||
}
|
||||
|
||||
function prevWeek() {
|
||||
@@ -180,7 +182,6 @@ const selectedEvent = computed(() =>
|
||||
)
|
||||
|
||||
const canUndo = computed(() => state.history.length > 0)
|
||||
const canRedo = computed(() => state.redoStack.length > 0)
|
||||
const isLoaded = computed(() => state.events.length > 0)
|
||||
const stats = computed(() => {
|
||||
const nRecur = state.events.filter((e) => e.rrule !== null).length
|
||||
@@ -192,7 +193,6 @@ export function useCalendar() {
|
||||
state,
|
||||
selectedEvent,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isLoaded,
|
||||
stats,
|
||||
loadFile,
|
||||
@@ -204,7 +204,6 @@ export function useCalendar() {
|
||||
splitEvent,
|
||||
toggleOccurrence,
|
||||
undo,
|
||||
redo,
|
||||
serialize,
|
||||
markSaved,
|
||||
setViewMode,
|
||||
|
||||
@@ -52,11 +52,6 @@ export function nowCompactTimestamp() {
|
||||
return new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
|
||||
}
|
||||
|
||||
/** 将 JS Date 的时间格式化为 'HH:MM'。 */
|
||||
export function toHHMM(date) {
|
||||
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 isoDate 所在周的周一 (ISO 周, 周一为首日)。
|
||||
* @param {string} isoDate - 'YYYY-MM-DD'
|
||||
|
||||
@@ -4,14 +4,7 @@ export const DOW = ['周日', '周一', '周二', '周三', '周四', '周五',
|
||||
// BYDAY 编码, 按 JS getDay() 索引: DOW_EN[getDay()] => 'SU'..'SA'
|
||||
export const DOW_EN = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
|
||||
|
||||
export const BYDAY_TO_INDEX = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 }
|
||||
|
||||
/** 返回 JS Date 对应的 BYDAY 编码 ('SU'..'SA')。 */
|
||||
export function bydayFromDate(date) {
|
||||
return DOW_EN[date.getDay()]
|
||||
}
|
||||
|
||||
/** 返回 JS Date 对应的中文星期标签。 */
|
||||
export function dowLabel(date) {
|
||||
return DOW[date.getDay()]
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DOW, DOW_EN, BYDAY_TO_INDEX, bydayFromDate, dowLabel } from '../src/lib/weekday.js'
|
||||
import { DOW, DOW_EN, bydayFromDate } from '../src/lib/weekday.js'
|
||||
|
||||
describe('weekday', () => {
|
||||
it('DOW has 7 entries starting Sunday', () => {
|
||||
it('DOW 有 7 项, 首项为周日', () => {
|
||||
expect(DOW).toHaveLength(7)
|
||||
expect(DOW[0]).toBe('周日')
|
||||
expect(DOW[6]).toBe('周六')
|
||||
})
|
||||
|
||||
it('DOW_EN indexed by JS getDay (0=Sunday)', () => {
|
||||
it('DOW_EN 按 JS getDay 索引 (0=周日)', () => {
|
||||
expect(DOW_EN).toEqual(['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'])
|
||||
})
|
||||
|
||||
it('BYDAY_TO_INDEX maps SU..SA to 0..6', () => {
|
||||
expect(BYDAY_TO_INDEX.SU).toBe(0)
|
||||
expect(BYDAY_TO_INDEX.SA).toBe(6)
|
||||
})
|
||||
|
||||
it('bydayFromDate returns BYDAY code for a Date', () => {
|
||||
// 2026-07-27 is a Monday -> getDay()=1 -> 'MO'
|
||||
it('bydayFromDate 返回 Date 对应的 BYDAY 编码', () => {
|
||||
// 2026-07-27 是周一 -> getDay()=1 -> 'MO'
|
||||
expect(bydayFromDate(new Date(2026, 6, 27))).toBe('MO')
|
||||
// 2026-08-01 is a Saturday -> getDay()=6 -> 'SA'
|
||||
// 2026-08-01 是周六 -> getDay()=6 -> 'SA'
|
||||
expect(bydayFromDate(new Date(2026, 7, 1))).toBe('SA')
|
||||
})
|
||||
|
||||
it('dowLabel returns Chinese label for a Date', () => {
|
||||
expect(dowLabel(new Date(2026, 6, 27))).toBe('周一')
|
||||
expect(dowLabel(new Date(2026, 6, 26))).toBe('周日')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user