fix: refine lunar countdown ui semantics and mobile add
ci / docker (push) Successful in 5m13s

This commit is contained in:
2026-09-06 16:53:19 +08:00
parent a06b80604a
commit 5ac771d0cd
6 changed files with 38 additions and 15 deletions
+5 -2
View File
@@ -351,12 +351,15 @@ class CountdownUpdate(BaseModel):
def countdown_dict(row: Countdown, today: date | None = None): def countdown_dict(row: Countdown, today: date | None = None):
today = today or datetime.now(ZoneInfo("Asia/Shanghai")).date() today = today or datetime.now(ZoneInfo("Asia/Shanghai")).date()
if row.calendar_mode == "lunar" and (row.ignore_year or row.repeat_rule != "none"): if row.calendar_mode == "lunar" and (row.ignore_year or row.repeat_rule != "none"):
# 农历按年重复:忽略年份或指定重复时,都按“每年农历”语义计算下一次
display_date = next_lunar_occurrence( display_date = next_lunar_occurrence(
row.lunar_month, row.lunar_day, row.ignore_year, row.repeat_rule, today row.lunar_month, row.lunar_day, True, "yearly", today
) or row.event_date ) or row.event_date
effective_repeat = "yearly"
else: else:
repeat_rule = "yearly" if row.ignore_year else row.repeat_rule repeat_rule = "yearly" if row.ignore_year else row.repeat_rule
display_date = countdown_occurrence(row.event_date, repeat_rule, today) display_date = countdown_occurrence(row.event_date, repeat_rule, today)
effective_repeat = repeat_rule
days, day_text = countdown_status(display_date, today) days, day_text = countdown_status(display_date, today)
lunar_year = None lunar_year = None
lunar_text = None lunar_text = None
@@ -377,9 +380,9 @@ def countdown_dict(row: Countdown, today: date | None = None):
"lunar_month": row.lunar_month, "lunar_month": row.lunar_month,
"lunar_day": row.lunar_day, "lunar_day": row.lunar_day,
"ignore_year": row.ignore_year, "ignore_year": row.ignore_year,
"repeat_rule": effective_repeat,
"lunar_text": lunar_text, "lunar_text": lunar_text,
"kind": row.kind, "kind": row.kind,
"repeat_rule": row.repeat_rule,
"icon": row.icon, "icon": row.icon,
"pinned": row.pinned, "pinned": row.pinned,
"archived_at": row.archived_at, "archived_at": row.archived_at,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,3 +1,3 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-Ds2Y4RQ7.js"></script> <!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-DV6C4yNR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQ5ZvkC6.css"> <link rel="stylesheet" crossorigin href="/assets/index-CaNgnksF.css">
</head><body><div id="app"></div></body></html> </head><body><div id="app"></div></body></html>
+28 -8
View File
@@ -19,6 +19,24 @@ const currentYear = new Date().getFullYear()
const freshForm = (): Form => ({ title:'', event_date:dateKey(new Date()), kind:'countdown', repeat_rule:'none', icon:'📅', calendar_mode:'solar', lunar_year:currentYear, lunar_month:1, lunar_day:1, leap_month:false, ignore_year:false }) const freshForm = (): Form => ({ title:'', event_date:dateKey(new Date()), kind:'countdown', repeat_rule:'none', icon:'📅', calendar_mode:'solar', lunar_year:currentYear, lunar_month:1, lunar_day:1, leap_month:false, ignore_year:false })
const form = ref<Form>(freshForm()) const form = ref<Form>(freshForm())
function primaryDate(item: Countdown) {
// 农历项主信息用农历;否则公历。忽略年份/每年重复时副行给出换算公历日期。
if (item.calendar_mode === 'lunar' && item.lunar_text) return item.lunar_text
return formatDate(item.display_date)
}
function secondaryDate(item: Countdown) {
if (item.calendar_mode === 'lunar' && item.lunar_text) {
const suffix = item.ignore_year ? ' · 每年农历' : ''
return `${formatDate(item.display_date)} · ${item.lunar_text}${suffix}`
}
return ''
}
function repeatBadge(item: Countdown) {
if (item.calendar_mode === 'lunar') return item.ignore_year ? '每年' : (item.repeat_rule !== 'none' ? repeatLabel(item.repeat_rule) : '')
return item.repeat_rule !== 'none' ? repeatLabel(item.repeat_rule) : ''
}
async function request(path:string, options:RequestInit={}) { async function request(path:string, options:RequestInit={}) {
const headers:Record<string,string> = { ...(options.headers as Record<string,string> || {}) } const headers:Record<string,string> = { ...(options.headers as Record<string,string> || {}) }
if (options.body) headers['Content-Type'] = 'application/json' if (options.body) headers['Content-Type'] = 'application/json'
@@ -29,7 +47,7 @@ async function request(path:string, options:RequestInit={}) {
} }
async function safe(work:()=>Promise<void>) { busy.value=true; error.value=''; try { await work() } catch(reason) { error.value=reason instanceof Error ? reason.message : '请求失败' } finally { busy.value=false } } async function safe(work:()=>Promise<void>) { busy.value=true; error.value=''; try { await work() } catch(reason) { error.value=reason instanceof Error ? reason.message : '请求失败' } finally { busy.value=false } }
async function load() { await safe(async()=>{ const [a,b]=await Promise.all([request('/countdowns'),request('/countdowns?archived=true')]); items.value=a; archived.value=b }) } async function load() { await safe(async()=>{ const [a,b]=await Promise.all([request('/countdowns'),request('/countdowns?archived=true')]); items.value=a; archived.value=b }) }
function add() { editingId.value=null; form.value=freshForm(); open.value=true } function add() { onAdd() }
function edit(item:Countdown) { function edit(item:Countdown) {
editingId.value=item.id editingId.value=item.id
form.value={ title:item.title, event_date:item.event_date, kind:item.kind, repeat_rule:item.repeat_rule, icon:item.icon, calendar_mode:item.calendar_mode, lunar_year:item.lunar_year || Number(item.event_date.slice(0,4)), lunar_month:Math.abs(item.lunar_month || 1), lunar_day:item.lunar_day || 1, leap_month:(item.lunar_month || 0)<0, ignore_year:item.ignore_year } form.value={ title:item.title, event_date:item.event_date, kind:item.kind, repeat_rule:item.repeat_rule, icon:item.icon, calendar_mode:item.calendar_mode, lunar_year:item.lunar_year || Number(item.event_date.slice(0,4)), lunar_month:Math.abs(item.lunar_month || 1), lunar_day:item.lunar_day || 1, leap_month:(item.lunar_month || 0)<0, ignore_year:item.ignore_year }
@@ -50,26 +68,28 @@ async function archiveItem(item:Countdown){await safe(async()=>{await request(`/
async function restore(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}/restore`,{method:'POST'});await load();emit('notice','已恢复')})} async function restore(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}/restore`,{method:'POST'});await load();emit('notice','已恢复')})}
async function purge(item:Countdown){if(!confirm(`永久删除“${item.title}”?这个操作不能撤销。`))return;await safe(async()=>{await request(`/countdowns/${item.id}/purge`,{method:'DELETE'});await load();emit('notice','已永久删除')})} async function purge(item:Countdown){if(!confirm(`永久删除“${item.title}”?这个操作不能撤销。`))return;await safe(async()=>{await request(`/countdowns/${item.id}/purge`,{method:'DELETE'});await load();emit('notice','已永久删除')})}
function formatDate(value:string){const [y,m,d]=value.split('-');return `${y}${Number(m)}${Number(d)}`} function formatDate(value:string){const [y,m,d]=value.split('-');return `${y}${Number(m)}${Number(d)}`}
function formatDateShort(value:string){const [y,m,d]=value.split('-');return `${y}/${Number(m)}/${Number(d)}`}
function repeatLabel(value:Countdown['repeat_rule']){return({none:'不重复',weekly:'每周',monthly:'每月',yearly:'每年'})[value]} function repeatLabel(value:Countdown['repeat_rule']){return({none:'不重复',weekly:'每周',monthly:'每月',yearly:'每年'})[value]}
function onAdd(){editingId.value=null;form.value=freshForm();open.value=true}
onMounted(load) onMounted(load)
</script> </script>
<template> <template>
<section class="countdown-view" :class="{ loading:busy }"> <section class="countdown-view" :class="{ loading:busy }">
<header class="countdown-hero"><div><small>记住值得期待与纪念的日子</small><h2>倒数日</h2></div><button class="countdown-add" @click="add"><Plus/>添加日子</button></header> <header class="countdown-hero"><div><small>记住值得期待与纪念的日子</small><h2>倒数日</h2></div><button class="countdown-add" @click="onAdd"><Plus/>添加日子</button></header>
<p v-if="error" class="inline-error">{{error}}</p> <p v-if="error" class="inline-error">{{error}}</p>
<div class="countdown-grid"> <div class="countdown-grid">
<article v-for="item in items" :key="item.id" class="countdown-card" :class="{ pinned:item.pinned }"> <article v-for="item in items" :key="item.id" class="countdown-card" :class="{ pinned:item.pinned }">
<div class="countdown-card-head"><span class="countdown-icon">{{item.icon}}</span><span class="countdown-type">{{countdownKindLabel(item.kind)}}</span><span v-if="item.calendar_mode==='lunar'" class="countdown-type">农历</span><Pin v-if="item.pinned" class="pinned-icon"/></div> <div class="countdown-card-head"><span class="countdown-icon">{{item.icon}}</span><span class="countdown-type">{{countdownKindLabel(item.kind)}}</span><span v-if="item.calendar_mode==='lunar'" class="countdown-type">农历</span><span v-if="repeatBadge(item)" class="countdown-type">{{repeatBadge(item)}}</span><Pin v-if="item.pinned" class="pinned-icon"/></div>
<h3>{{item.title}}</h3><div class="countdown-number"><strong>{{Math.abs(item.days)}}</strong><span v-if="item.days!==0">天</span></div><b class="countdown-copy">{{countdownDayText(item.days)}}</b> <h3>{{item.title}}</h3><div class="countdown-number"><strong>{{Math.abs(item.days)}}</strong><span v-if="item.days!==0">天</span></div><b class="countdown-copy">{{countdownDayText(item.days)}}</b>
<p>{{formatDate(item.display_date)}}<span v-if="item.repeat_rule!=='none'"> · {{repeatLabel(item.repeat_rule)}}</span></p> <p><b>{{primaryDate(item)}}</b><span v-if="secondaryDate(item)"> · {{secondaryDate(item)}}</span></p>
<p v-if="item.lunar_text"><span class="countdown-type">{{item.lunar_text}}</span><span v-if="item.ignore_year"> · 每年农历</span></p>
<div class="countdown-actions"><button v-if="!item.pinned" @click="pin(item)"><Pin/>置顶</button><button @click="edit(item)"><Pencil/>编辑</button><button @click="archiveItem(item)"><Archive/>归档</button></div> <div class="countdown-actions"><button v-if="!item.pinned" @click="pin(item)"><Pin/>置顶</button><button @click="edit(item)"><Pencil/>编辑</button><button @click="archiveItem(item)"><Archive/>归档</button></div>
</article> </article>
<button v-if="!items.length&&!busy" class="countdown-empty" @click="add"><CalendarHeart/><b>添加第一个重要日子</b><span>生日纪念日或一场期待已久的旅行</span></button> <button v-if="!items.length&&!busy" class="countdown-empty" @click="onAdd"><CalendarHeart/><b>添加第一个重要日子</b><span>生日纪念日或一场期待已久的旅行</span></button>
</div> </div>
<button v-if="archived.length" class="archived-toggle" @click="showArchived=!showArchived"><ArchiveRestore/>已归档{{archived.length}}</button> <button v-if="archived.length" class="archived-toggle" @click="showArchived=!showArchived"><ArchiveRestore/>已归档{{archived.length}}</button>
<div v-if="showArchived" class="archived-countdowns"><article v-for="item in archived" :key="item.id"><span>{{item.icon}}</span><b>{{item.title}}</b><small>{{formatDate(item.event_date)}}<template v-if="item.lunar_text"> · {{item.lunar_text}}</template></small><button @click="restore(item)"><ArchiveRestore/>恢复</button><button class="danger-text" @click="purge(item)"><Trash2/>永久删除</button></article></div> <div v-if="showArchived" class="archived-countdowns"><article v-for="item in archived" :key="item.id"><span>{{item.icon}}</span><b>{{item.title}}</b><small>{{formatDateShort(item.display_date)}}<template v-if="item.lunar_text"> · {{item.lunar_text}}</template><template v-if="item.calendar_mode==='lunar'"> · 农历</template></small><button @click="restore(item)"><ArchiveRestore/>恢复</button><button class="danger-text" @click="purge(item)"><Trash2/>永久删除</button></article></div>
<button class="fab countdown-fab mobile-only" aria-label="添加倒数日" @click="add"><Plus/></button>
<div v-if="open" class="countdown-modal-mask" @click.self="open=false"><form class="countdown-modal" role="dialog" aria-modal="true" @submit.prevent="save"> <div v-if="open" class="countdown-modal-mask" @click.self="open=false"><form class="countdown-modal" role="dialog" aria-modal="true" @submit.prevent="save">
<header><div><small>{{editingId?'调整重要日子':'记下重要日子'}}</small><h3>{{editingId?'编辑倒数日':'新建倒数日'}}</h3></div><button type="button" aria-label="关闭" @click="open=false"><X/></button></header> <header><div><small>{{editingId?'调整重要日子':'记下重要日子'}}</small><h3>{{editingId?'编辑倒数日':'新建倒数日'}}</h3></div><button type="button" aria-label="关闭" @click="open=false"><X/></button></header>
<label>图标与名称<div class="countdown-title-fields"><input v-model="form.icon" maxlength="8" aria-label="图标"><input v-model="form.title" maxlength="200" required placeholder="例如:去北海道旅行" autofocus></div></label> <label>图标与名称<div class="countdown-title-fields"><input v-model="form.icon" maxlength="8" aria-label="图标"><input v-model="form.title" maxlength="200" required placeholder="例如:去北海道旅行" autofocus></div></label>
@@ -80,7 +100,7 @@ onMounted(load)
<label>月份<select v-model.number="form.lunar_month"><option v-for="month in 12" :key="month" :value="month">{{month}}</option></select></label> <label>月份<select v-model.number="form.lunar_month"><option v-for="month in 12" :key="month" :value="month">{{month}}</option></select></label>
<label>日期<select v-model.number="form.lunar_day"><option v-for="day in 30" :key="day" :value="day">{{day}}</option></select></label> <label>日期<select v-model.number="form.lunar_day"><option v-for="day in 30" :key="day" :value="day">{{day}}</option></select></label>
</div> </div>
<label v-if="form.calendar_mode==='lunar'"><input v-model="form.leap_month" type="checkbox"> 闰月负数月份编码</label> <label v-if="form.calendar_mode==='lunar'"><input v-model="form.leap_month" type="checkbox"> 闰月如果是闰月勾选这里</label>
<label><input v-model="form.ignore_year" type="checkbox"> 忽略年份<span v-if="form.calendar_mode==='lunar'">每年按农历计算</span></label> <label><input v-model="form.ignore_year" type="checkbox"> 忽略年份<span v-if="form.calendar_mode==='lunar'">每年按农历计算</span></label>
<label>类型<select v-model="form.kind"><option value="countdown">倒数日</option><option value="anniversary">纪念日</option><option value="birthday">生日</option></select></label> <label>类型<select v-model="form.kind"><option value="countdown">倒数日</option><option value="anniversary">纪念日</option><option value="birthday">生日</option></select></label>
<label>重复<select v-model="form.repeat_rule"><option value="none">不重复</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option></select></label> <label>重复<select v-model="form.repeat_rule"><option value="none">不重复</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option></select></label>
File diff suppressed because one or more lines are too long