feat: refine countdown timeline UI
ci / docker (push) Successful in 4m21s

This commit is contained in:
2026-09-07 17:19:17 +08:00
parent 6ca613f5fa
commit 3772b56617
3 changed files with 110 additions and 33 deletions
+23 -1
View File
@@ -10,6 +10,28 @@ describe('countdown modal accessibility', () => {
expect(source).toContain('@keydown.esc="closeDialog"') expect(source).toContain('@keydown.esc="closeDialog"')
expect(source).toContain('ref="titleInput"') expect(source).toContain('ref="titleInput"')
expect(source).toContain('titleInput.value?.focus()') expect(source).toContain('titleInput.value?.focus()')
expect(source).toContain(':inert="open"') expect(source).toContain(':inert="open || Boolean(detailItem)"')
})
it('uses a compact timeline and moves row actions into a detail sheet', () => {
expect(source).toContain('class="countdown-timeline"')
expect(source).toContain('class="countdown-row"')
expect(source).toContain('@click="openDetail(item)"')
expect(source).toContain('class="countdown-detail-sheet"')
expect(source).toContain('ref="detailCloseButton"')
expect(source).toContain('detailCloseButton.value?.focus()')
expect(source).not.toContain('class="countdown-actions"')
})
it('keeps advanced creation fields collapsed behind more settings', () => {
expect(source).toContain('class="countdown-advanced"')
expect(source).toContain('<span>更多设置</span>')
expect(source).toContain('@change="applyKindDefaults"')
expect(source).toContain(':disabled="form.ignore_year"')
})
it('keeps the empty state directly actionable', () => {
expect(source).toContain('添加第一个重要日子')
expect(source).toContain('@click="openFromEmpty"')
}) })
}) })
+86 -31
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { Archive, ArchiveRestore, CalendarHeart, Pencil, Pin, Trash2, X } from 'lucide-vue-next' import { Archive, ArchiveRestore, CalendarHeart, ChevronDown, Pencil, Pin, Trash2, X } from 'lucide-vue-next'
import { csrfHeader } from './lib/csrf' import { csrfHeader } from './lib/csrf'
import { calendarModeLabel, countdownDayText, countdownKindLabel, dateKey, formatApiErrorDetail } from './lib/mvp-utils' import { calendarModeLabel, countdownDayText, countdownKindLabel, dateKey, formatApiErrorDetail } from './lib/mvp-utils'
@@ -11,16 +11,19 @@ type Countdown = {
ignore_year: boolean; lunar_text: string|null ignore_year: boolean; lunar_text: string|null
} }
type Form = { title:string; event_date:string; kind:Countdown['kind']; repeat_rule:Countdown['repeat_rule']; icon:string; calendar_mode:Countdown['calendar_mode']; lunar_year:number; lunar_month:number; lunar_day:number; leap_month:boolean; ignore_year:boolean } type Form = { title:string; event_date:string; kind:Countdown['kind']; repeat_rule:Countdown['repeat_rule']; icon:string; calendar_mode:Countdown['calendar_mode']; lunar_year:number; lunar_month:number; lunar_day:number; leap_month:boolean; ignore_year:boolean }
type CountdownGroup = { key: string; title: string; items: Countdown[] }
const emit = defineEmits<{ notice: [message: string] }>() const emit = defineEmits<{ notice: [message: string] }>()
const items = ref<Countdown[]>([]), archived = ref<Countdown[]>([]) const items = ref<Countdown[]>([]), archived = ref<Countdown[]>([])
const showArchived = ref(false), open = ref(false), busy = ref(false) const showArchived = ref(false), open = ref(false), busy = ref(false)
const editingId = ref<string|null>(null), error = ref('') const editingId = ref<string|null>(null), error = ref('')
const detailItem = ref<Countdown|null>(null), showAdvanced = ref(false)
const currentYear = new Date().getFullYear() 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())
const composerOrigin = ref({ x: window.innerWidth - 43, y: window.innerHeight - 104 }) const composerOrigin = ref({ x: window.innerWidth - 43, y: window.innerHeight - 104 })
const composerStyle = computed(() => ({ '--fab-origin-x': `${composerOrigin.value.x}px`, '--fab-origin-y': `${composerOrigin.value.y}px` })) const composerStyle = computed(() => ({ '--fab-origin-x': `${composerOrigin.value.x}px`, '--fab-origin-y': `${composerOrigin.value.y}px` }))
const titleInput = ref<HTMLInputElement | null>(null) const titleInput = ref<HTMLInputElement | null>(null)
const detailCloseButton = ref<HTMLButtonElement | null>(null)
let previousFocus: HTMLElement | null = null let previousFocus: HTMLElement | null = null
function focusDialog() { function focusDialog() {
@@ -29,6 +32,7 @@ function focusDialog() {
} }
function closeDialog() { function closeDialog() {
open.value = false open.value = false
showAdvanced.value = false
void nextTick(() => previousFocus?.focus()) void nextTick(() => previousFocus?.focus())
} }
function trapDialogFocus(event: KeyboardEvent) { function trapDialogFocus(event: KeyboardEvent) {
@@ -59,7 +63,29 @@ function repeatBadge(item: Countdown) {
if (item.calendar_mode === 'lunar') return item.ignore_year ? '每年' : (item.repeat_rule !== 'none' ? repeatLabel(item.repeat_rule) : '') 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) : '' return item.repeat_rule !== 'none' ? repeatLabel(item.repeat_rule) : ''
} }
function visibleBadges(item: Countdown) {
return [countdownKindLabel(item.kind), item.calendar_mode === 'lunar' ? '农历' : '', repeatBadge(item)].filter(Boolean).slice(0, 2)
}
function sorted(itemsToSort: Countdown[]) {
return [...itemsToSort].sort((a, b) => Number(b.pinned) - Number(a.pinned) || a.days - b.days || a.title.localeCompare(b.title, 'zh-Hans-CN'))
}
const upcomingItems = computed(() => sorted(items.value.filter((item) => item.days >= 0)))
const focusItem = computed(() => upcomingItems.value[0] ?? sorted(items.value)[0] ?? null)
const countdownSummary = computed(() => {
if (!items.value.length) return '还没有记录重要日子'
const pinned = items.value.filter((item) => item.pinned).length
return pinned ? `${items.value.length} 个重要日子 · ${pinned} 个置顶` : `${items.value.length} 个重要日子`
})
const countdownGroups = computed<CountdownGroup[]>(() => {
const recent = sorted(items.value.filter((item) => item.days >= 0 && item.days <= 30))
const later = sorted(items.value.filter((item) => item.days > 30))
const past = sorted(items.value.filter((item) => item.days < 0))
return [
{ key:'recent', title:'最近', items:recent },
{ key:'later', title:'稍后', items:later },
{ key:'past', title:'已经过去', items:past },
].filter((group) => group.items.length)
})
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> || {}) }
@@ -75,29 +101,38 @@ 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 edit(item:Countdown) { function edit(item:Countdown) {
detailItem.value=null
editingId.value=item.id editingId.value=item.id
showAdvanced.value=false
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 }
open.value=true open.value=true
focusDialog() focusDialog()
} }
function applyKindDefaults() {
if (form.value.kind === 'birthday' || form.value.kind === 'anniversary') form.value.repeat_rule='yearly'
if (form.value.kind === 'countdown') form.value.repeat_rule='none'
}
async function save() { async function save() {
if (!form.value.title.trim()) return if (!form.value.title.trim()) return
await safe(async()=>{ await safe(async()=>{
const payload:any={ title:form.value.title.trim(), event_date:form.value.calendar_mode==='lunar' ? `${form.value.lunar_year}-01-01` : form.value.event_date, kind:form.value.kind, repeat_rule:form.value.repeat_rule, icon:form.value.icon, calendar_mode:form.value.calendar_mode, ignore_year:form.value.ignore_year } const payload:any={ title:form.value.title.trim(), event_date:form.value.calendar_mode==='lunar' ? `${form.value.lunar_year}-01-01` : form.value.event_date, kind:form.value.kind, repeat_rule:form.value.ignore_year ? 'yearly' : form.value.repeat_rule, icon:form.value.icon, calendar_mode:form.value.calendar_mode, ignore_year:form.value.ignore_year }
if (form.value.calendar_mode==='lunar') { payload.lunar_month=form.value.leap_month ? -form.value.lunar_month : form.value.lunar_month; payload.lunar_day=form.value.lunar_day } if (form.value.calendar_mode==='lunar') { payload.lunar_month=form.value.leap_month ? -form.value.lunar_month : form.value.lunar_month; payload.lunar_day=form.value.lunar_day }
const path=editingId.value ? `/countdowns/${editingId.value}` : '/countdowns' const path=editingId.value ? `/countdowns/${editingId.value}` : '/countdowns'
await request(path,{ method:editingId.value?'PATCH':'POST', body:JSON.stringify(payload) }) await request(path,{ method:editingId.value?'PATCH':'POST', body:JSON.stringify(payload) })
closeDialog(); await load(); emit('notice',editingId.value?'倒数日已更新':'倒数日已添加') closeDialog(); await load(); emit('notice',editingId.value?'倒数日已更新':'倒数日已添加')
}) })
} }
async function pin(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}/pin`,{method:'POST'});await load();emit('notice','已置顶')})} async function pin(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}/pin`,{method:'POST'});detailItem.value=null;await load();emit('notice','已置顶')})}
async function archiveItem(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}`,{method:'DELETE'});await load();emit('notice','已归档')})} async function archiveItem(item:Countdown){await safe(async()=>{await request(`/countdowns/${item.id}`,{method:'DELETE'});detailItem.value=null;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 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 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 openCountdownComposer(origin?: { x: number; y: number }){if(origin)composerOrigin.value=origin;editingId.value=null;form.value=freshForm();open.value=true;focusDialog()} function openDetail(item:Countdown){detailItem.value=item;previousFocus=document.activeElement instanceof HTMLElement ? document.activeElement : null;void nextTick(() => detailCloseButton.value?.focus())}
function closeDetail(){detailItem.value=null;void nextTick(() => previousFocus?.focus())}
function openFromEmpty(){openCountdownComposer()}
function openCountdownComposer(origin?: { x: number; y: number }){if(origin)composerOrigin.value=origin;detailItem.value=null;editingId.value=null;showAdvanced.value=false;form.value=freshForm();open.value=true;focusDialog()}
defineExpose({ openCountdownComposer }) defineExpose({ openCountdownComposer })
onMounted(load) onMounted(load)
onBeforeUnmount(() => { previousFocus = null }) onBeforeUnmount(() => { previousFocus = null })
@@ -105,36 +140,56 @@ onBeforeUnmount(() => { previousFocus = null })
<template> <template>
<section class="countdown-view" :class="{ loading:busy }"> <section class="countdown-view" :class="{ loading:busy }">
<div class="countdown-content" :inert="open" :aria-hidden="open ? 'true' : undefined"> <div class="countdown-content" :inert="open || Boolean(detailItem)" :aria-hidden="open || detailItem ? 'true' : undefined">
<header class="countdown-hero"><div><small>记住值得期待与纪念的日子</small><h2>倒数日</h2></div></header> <header class="countdown-hero"><small>{{countdownSummary}}</small></header>
<p v-if="error" class="inline-error">{{error}}</p> <p v-if="error" class="inline-error">{{error}}</p>
<div class="countdown-grid"> <div v-if="items.length" class="countdown-layout">
<article v-for="item in items" :key="item.id" class="countdown-card" :class="{ pinned:item.pinned }"> <div class="countdown-timeline">
<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> <section v-for="group in countdownGroups" :key="group.key" class="countdown-group">
<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>{{group.title}}</h3>
<p><b>{{primaryDate(item)}}</b><span v-if="secondaryDate(item)"> · {{secondaryDate(item)}}</span></p> <button v-for="item in group.items" :key="item.id" type="button" class="countdown-row" :class="{ pinned:item.pinned, today:item.days===0, past:item.days<0 }" @click="openDetail(item)">
<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> <span class="countdown-icon">{{item.icon}}</span>
</article> <span class="countdown-main"><b>{{item.title}}</b><small>{{primaryDate(item)}}<template v-if="secondaryDate(item)"> · {{secondaryDate(item)}}</template></small><span class="countdown-badges"><em v-for="badge in visibleBadges(item)" :key="badge">{{badge}}</em></span></span>
<div v-if="!items.length&&!busy" class="countdown-empty"><CalendarHeart/><b>还没有重要日子</b><span>用右下角的圆圈添加生日纪念日或旅行</span></div> <span class="countdown-state"><strong>{{item.days===0?'今天':Math.abs(item.days)}}</strong><small>{{countdownDayText(item.days)}}</small></span>
</div> <Pin v-if="item.pinned" class="pinned-icon"/>
<button v-if="archived.length" class="archived-toggle" @click="showArchived=!showArchived"><ArchiveRestore/>已归档{{archived.length}}</button> </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>{{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> </section>
</div>
<aside v-if="focusItem" class="countdown-focus">
<small>最近的重要日子</small><span>{{focusItem.icon}}</span><h3>{{focusItem.title}}</h3>
<div class="countdown-number"><strong>{{focusItem.days===0?'今天':Math.abs(focusItem.days)}}</strong><span v-if="focusItem.days!==0"></span></div>
<b class="countdown-copy">{{countdownDayText(focusItem.days)}}</b><p>{{primaryDate(focusItem)}}</p>
</aside>
</div>
<div v-else-if="!busy" class="countdown-empty"><CalendarHeart/><b>还没有重要日子</b><span>生日、纪念日,或一场期待已久的旅行</span><button type="button" class="primary-small" @click="openFromEmpty">添加第一个重要日子</button></div>
<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>{{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>
</div> </div>
<Transition name="countdown-detail">
<div v-if="detailItem" class="countdown-detail-mask" @click.self="closeDetail"><article class="countdown-detail-sheet" role="dialog" aria-modal="true" aria-labelledby="countdown-detail-title" @keydown.esc="closeDetail">
<header><span class="countdown-icon">{{detailItem.icon}}</span><div><small>重要日子详情</small><h3 id="countdown-detail-title">{{detailItem.title}}</h3></div><button ref="detailCloseButton" type="button" aria-label="关闭详情" @click="closeDetail"><X/></button></header>
<div class="countdown-detail-days"><strong>{{detailItem.days===0?'今天':Math.abs(detailItem.days)}}</strong><span v-if="detailItem.days!==0"></span><b>{{countdownDayText(detailItem.days)}}</b></div>
<dl><div><dt>日期</dt><dd>{{primaryDate(detailItem)}}</dd></div><div v-if="secondaryDate(detailItem)"><dt>换算</dt><dd>{{secondaryDate(detailItem)}}</dd></div><div><dt>类型</dt><dd>{{countdownKindLabel(detailItem.kind)}} · {{detailItem.calendar_mode==='lunar'?'农历':'公历'}} · {{repeatBadge(detailItem) || '不重复'}}</dd></div></dl>
<footer><button v-if="!detailItem.pinned" type="button" @click="pin(detailItem)"><Pin/>置顶</button><button type="button" @click="edit(detailItem)"><Pencil/>编辑</button><button type="button" class="danger-text" @click="archiveItem(detailItem)"><Archive/>归档</button></footer>
</article></div>
</Transition>
<Transition name="countdown-compose"> <Transition name="countdown-compose">
<div v-if="open" class="countdown-modal-mask" @click.self="closeDialog"><form class="countdown-modal" :style="composerStyle" role="dialog" aria-modal="true" aria-labelledby="countdown-dialog-title" @submit.prevent="save" @keydown.esc="closeDialog" @keydown="trapDialogFocus"> <div v-if="open" class="countdown-modal-mask" @click.self="closeDialog"><form class="countdown-modal" :style="composerStyle" role="dialog" aria-modal="true" aria-labelledby="countdown-dialog-title" @submit.prevent="save" @keydown.esc="closeDialog" @keydown="trapDialogFocus">
<header><div><small>{{editingId?'调整重要日子':'记下重要日子'}}</small><h3 id="countdown-dialog-title">{{editingId?'编辑倒数日':'新建倒数日'}}</h3></div><button type="button" aria-label="关闭" @click="closeDialog"><X/></button></header> <header><div><small>{{editingId?'调整重要日子':'快速记下重要日子'}}</small><h3 id="countdown-dialog-title">{{editingId?'编辑倒数日':'新建倒数日'}}</h3></div><button type="button" aria-label="关闭" @click="closeDialog"><X/></button></header>
<label>图标与名称<div class="countdown-title-fields"><input v-model="form.icon" maxlength="8" aria-label="图标"><input ref="titleInput" 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 ref="titleInput" v-model="form.title" maxlength="200" required placeholder="例如:去北海道旅行" autofocus></div></label>
<label>历法<select v-model="form.calendar_mode"><option value="solar">{{calendarModeLabel('solar')}}</option><option value="lunar">{{calendarModeLabel('lunar')}}</option></select></label>
<label v-if="form.calendar_mode==='solar'">日期<input v-model="form.event_date" type="date" required></label> <label v-if="form.calendar_mode==='solar'">日期<input v-model="form.event_date" type="date" required></label>
<div v-else class="countdown-title-fields"> <label>类型<select v-model="form.kind" @change="applyKindDefaults"><option value="countdown">倒数日</option><option value="anniversary">纪念日</option><option value="birthday">生日</option></select></label>
<label>年份<input v-model.number="form.lunar_year" type="number" min="1900" max="2099" required></label> <details class="countdown-advanced" :open="showAdvanced" @toggle="showAdvanced=($event.target as HTMLDetailsElement).open"><summary><span>更多设置</span><ChevronDown/></summary>
<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="form.calendar_mode"><option value="solar">{{calendarModeLabel('solar')}}</option><option value="lunar">{{calendarModeLabel('lunar')}}</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 v-if="form.calendar_mode==='lunar'" class="countdown-title-fields">
</div> <label>年份<input v-model.number="form.lunar_year" type="number" min="1900" max="2099" required></label>
<label v-if="form.calendar_mode==='lunar'"><input v-model="form.leap_month" type="checkbox"> 闰月如果是闰月勾选这里</label> <label>月份<select v-model.number="form.lunar_month"><option v-for="month in 12" :key="month" :value="month">{{month}}</option></select></label>
<label><input v-model="form.ignore_year" type="checkbox"> 忽略年份<span v-if="form.calendar_mode==='lunar'">每年按农历计算</span></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="form.kind"><option value="countdown">倒数日</option><option value="anniversary">纪念日</option><option value="birthday">生日</option></select></label> </div>
<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 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>重复<select v-model="form.repeat_rule" :disabled="form.ignore_year"><option value="none">不重复</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option></select></label>
</details>
<footer><button type="button" class="secondary" @click="closeDialog">取消</button><button class="primary-small">保存</button></footer> <footer><button type="button" class="secondary" @click="closeDialog">取消</button><button class="primary-small">保存</button></footer>
</form></div> </form></div>
</Transition> </Transition>
File diff suppressed because one or more lines are too long