diff --git a/frontend/src/CalendarIntegration.test.ts b/frontend/src/CalendarIntegration.test.ts index 2c6cd47..711da00 100644 --- a/frontend/src/CalendarIntegration.test.ts +++ b/frontend/src/CalendarIntegration.test.ts @@ -4,7 +4,7 @@ const app=readFileSync('src/App.vue','utf8');const utils=readFileSync('src/lib/m describe('calendar shell integration',()=>{ it('puts calendar beside the other first-level tools on desktop and mobile',()=>{const nav=app.slice(app.indexOf(''),app.indexOf('',app.indexOf('')));expect(nav.indexOf("switchView('habits')")).toBeLessThan(nav.indexOf("switchView('countdowns')"));expect(nav.indexOf("switchView('countdowns')")).toBeLessThan(nav.indexOf("switchView('memos')"));expect(nav.indexOf("switchView('memos')")).toBeLessThan(nav.indexOf("switchView('calendar')"));const bottom=app.slice(app.indexOf('',app.indexOf('{expect(utils).toContain("'calendar'");expect(app).toContain("import CalendarPanel from './CalendarPanel.vue'");expect(app).toContain("activeView==='calendar'")}) - it('places desktop event detail in the shell right pane while retaining the mobile sheet',()=>{const panel=readFileSync('src/CalendarPanel.vue','utf8');expect(app).toContain("'detail-open': Boolean(selectedTask) || habitDetailOpen || calendarDetailOpen");expect(app).toContain(':compact-layout="compactLayout" @detail="calendarDetailOpen=$event"');expect(app).toContain("selectedTask || habitDetailOpen || calendarDetailOpen");expect(app).toContain('watch([selectedTask, habitDetailOpen, calendarDetailOpen, sidebarCollapsed], reconcileDesktopPaneWidths)');expect(panel).toContain('defineProps<{ compactLayout: boolean }>()');expect(panel).toContain(':modal="compactLayout"');expect(panel).toContain('inline-target=".shell"');expect(panel).toContain("watch(selected,event=>emit('detail',Boolean(event)))");expect(css).toContain('@media(min-width:931px){.calendar-view{padding-top:4px}.calendar-event-detail')}) + it('places desktop event detail in the shell right pane while retaining the mobile sheet',()=>{const panel=readFileSync('src/CalendarPanel.vue','utf8');expect(app).toContain("'detail-open': Boolean(selectedTask) || habitDetailOpen || calendarDetailOpen");expect(app).toContain(':compact-layout="compactLayout" @detail="calendarDetailOpen=$event"');expect(app).toContain("selectedTask || habitDetailOpen || calendarDetailOpen");expect(app).toContain('watch([selectedTask, habitDetailOpen, calendarDetailOpen, sidebarCollapsed], reconcileDesktopPaneWidths)');expect(panel).toContain('defineProps<{ compactLayout: boolean }>()');expect(panel).toContain(":key=\"compactLayout?'calendar-event-mobile':'calendar-event-desktop'\"");expect(panel).toContain(':modal="compactLayout"');expect(panel).toContain('inline-target=".shell"');expect(panel).toContain("watch(selected,event=>emit('detail',Boolean(event)))");expect(panel).toContain("watch(()=>props.compactLayout,()=>{selected.value=null})");expect(css).toContain('@media(min-width:931px){.calendar-view{padding-top:4px}.calendar-event-detail')}) it('keeps five 44px mobile targets across required breakpoints',()=>{const shellCss=readFileSync('src/style.css','utf8');expect(shellCss).toContain('grid-template-columns:repeat(5,minmax(0,1fr))');expect(css).toContain('min-height:44px');expect(css).toContain('@media(max-width:720px)');expect(css).toContain('@media(min-width:931px)');expect(css).toContain('@media(min-width:1440px)')}) it('uses the shell page title once and contains the horizontal source scroller',()=>{const panel=readFileSync('src/CalendarPanel.vue','utf8');expect(panel).not.toContain('日历订阅');expect(css).toContain('.calendar-view{min-width:0;');expect(css).toContain('.calendar-filters{min-width:0;max-width:100%;')}) }) diff --git a/frontend/src/CalendarPanel.vue b/frontend/src/CalendarPanel.vue index affdede..c9dd572 100644 --- a/frontend/src/CalendarPanel.vue +++ b/frontend/src/CalendarPanel.vue @@ -10,7 +10,7 @@ type Subscription = { id:string; name:string; url:string; color:string; enabled: type CalendarEvent = { id:string; title:string; starts_at:string; ends_at:string; all_day:boolean; source_id:string; source_name:string; color:string; description?:string|null; location?:string|null } type EventResponse = { events:CalendarEvent[]; sources:Array<{id:string;name:string;stale:boolean}> } type Form = { name:string; url:string; color:string; enabled:boolean } -defineProps<{ compactLayout: boolean }>() +const props=defineProps<{ compactLayout: boolean }>() const emit=defineEmits<{notice:[message:string];detail:[open:boolean]}>() const subscriptions=ref([]),events=ref([]),loading=ref(false),error=ref('') let eventsRequestGeneration=0 @@ -56,6 +56,7 @@ async function refresh(item:Subscription){if(busyId.value)return;busyId.value=it async function remove(item:Subscription){if(busyId.value)return;if(await appDialog.value?.show({title:`删除“${item.name}”?`,description:'该来源的事件也会从日历中移除。',danger:true,confirmText:'删除'})!==true)return;busyId.value=item.id;error.value='';try{await request(`/calendar-subscriptions/${item.id}`,{method:'DELETE'});hiddenSources.value.delete(item.id);await Promise.all([loadSubscriptions(),loadEvents()]);emit('notice','日历订阅已删除')}catch(reason){error.value=reason instanceof Error?reason.message:'删除失败'}finally{busyId.value=''}} watch(manageOpen,open=>{if(!open)formOpen.value=false}) watch(selected,event=>emit('detail',Boolean(event))) +watch(()=>props.compactLayout,()=>{selected.value=null}) onBeforeUnmount(()=>emit('detail',false)) onMounted(()=>void load()) @@ -69,7 +70,7 @@ onMounted(()=>void load()) {{source.name}} {{selectedDayLabel}} · {{visibleEvents.length}} 个日程{{displayTime(event)}}{{eventTitle(event)}}{{eventSource(event)}} · {{event.location}} 这一天还没有日程{{subscriptions.length?'可以选择本周其他日期或检查来源筛选':'先添加一个 iCal 日历订阅'}}添加日历源 - {{eventTitle(selected)}}时间{{displayDay(localDayKey(eventStart(selected)))}} {{displayTime(selected)}} – {{displayTime({...selected,starts_at:eventEnd(selected)})}}来源{{eventSource(selected)}}地点{{selected.location}}备注{{selected.description}} + {{eventTitle(selected)}}时间{{displayDay(localDayKey(eventStart(selected)))}} {{displayTime(selected)}} – {{displayTime({...selected,starts_at:eventEnd(selected)})}}来源{{eventSource(selected)}}地点{{selected.location}}备注{{selected.description}} 日历源添加订阅{{source.name}}{{source.url}}{{source.last_error}}启用 {{editing?'编辑订阅':'添加订阅'}}名称iCal 地址颜色启用此订阅
{{selected.description}}