上架自检两项整改:横竖屏布局一致性 + 页面转场改为淡入淡出(弹簧曲线)

一、横竖屏(意见 1:折叠屏展开态 / 平板需支持横竖屏切换;接近 1:1 的设备横竖屏布局需保持一致)
- 主界面布局判据由"是否横屏 (orientation: landscape)"改为"窗口短边是否达到大屏断点 840vp":
  min(宽,高) >= 840vp 等价于 (width >= 840vp) && (height >= 840vp),用两条 mediaquery 合成。
- 短边在旋转时不变(宽高互换),因此横竖屏布局天然保持一致 —— 屏幕比例接近 1:1 的设备
  (折叠屏展开态、平板)在旋转时不会再出现"单栏 ⇄ 双栏"的布局跳变。
- 不锁死屏幕方向(module.json5 仍为 auto_rotation_restricted),折叠屏展开态与平板
  依旧可以自由横竖屏切换显示。

二、转场动效(意见 2:使用系统转场,淡入淡出,曲线优先弹簧曲线)
- 全部 7 个 @Entry 页面新增 pageTransition():只做 opacity 淡入淡出,
  不调用 .slide() / .translate(),即不做左右平移 / 上下位移 / 单帧直接切换。
- 转场曲线使用 curves.springMotion() 弹簧曲线。
This commit is contained in:
2026-09-28 14:20:14 +08:00
parent 736a0a6587
commit 2f4657ced3
7 changed files with 155 additions and 31 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
// entry/src/main/ets/pages/AccountsPage.ets // entry/src/main/ets/pages/AccountsPage.ets
// DAV 账号管理(由原 CalDAVSync 首页移植) // DAV 账号管理(由原 CalDAVSync 首页移植)
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { DavAccount, AccountStore, TYPE_KEYS, TYPE_CALDAV, TYPE_CARDDAV } from '../common/AccountStore'; import { DavAccount, AccountStore, TYPE_KEYS, TYPE_CALDAV, TYPE_CARDDAV } from '../common/AccountStore';
@@ -252,6 +252,19 @@ struct AccountsPage {
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
@Builder @Builder
emptyState() { emptyState() {
Column({ space: 12 }) { Column({ space: 12 }) {
+14 -1
View File
@@ -1,7 +1,7 @@
// entry/src/main/ets/pages/AddAccountPage.ets // entry/src/main/ets/pages/AddAccountPage.ets
// 添加账号第一页:URL / 用户名 / 密码 → 连接(凭据经 AppStorage 传给日历本选择页) // 添加账号第一页:URL / 用户名 / 密码 → 连接(凭据经 AppStorage 传给日历本选择页)
import { http } from '@kit.NetworkKit'; import { http } from '@kit.NetworkKit';
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { buffer } from '@kit.ArkTS'; import { buffer } from '@kit.ArkTS';
import url from '@ohos.url'; import url from '@ohos.url';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
@@ -297,6 +297,19 @@ struct AddAccountPage {
.alignItems(HorizontalAlign.Center) .alignItems(HorizontalAlign.Center)
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
@Builder @Builder
formField(label: string, placeholder: string, value: string, formField(label: string, placeholder: string, value: string,
onChange: (value: string) => void, isPassword: boolean) { onChange: (value: string) => void, isPassword: boolean) {
+14 -1
View File
@@ -1,6 +1,6 @@
// entry/src/main/ets/pages/CalendarListPage.ets // entry/src/main/ets/pages/CalendarListPage.ets
// 添加账号第二页:PROPFIND 列出日历本 → 勾选 → 命名 → 保存账号 // 添加账号第二页:PROPFIND 列出日历本 → 勾选 → 命名 → 保存账号
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { buffer } from '@kit.ArkTS'; import { buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { DavAccount, AccountStore, TYPE_CALDAV } from '../common/AccountStore'; import { DavAccount, AccountStore, TYPE_CALDAV } from '../common/AccountStore';
@@ -369,6 +369,19 @@ struct CalendarListPage {
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.alignItems(HorizontalAlign.Center) .alignItems(HorizontalAlign.Center)
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
} }
@Component @Component
+14 -1
View File
@@ -5,7 +5,7 @@
// ⚠️ 取消勾选某个日历本会改变**后面所有本的序号**(calKey = accId_序号)。因此保存时还要检查: // ⚠️ 取消勾选某个日历本会改变**后面所有本的序号**(calKey = accId_序号)。因此保存时还要检查:
// 被取消的那个本是不是「系统日历 → CalDAV」的备份目标 —— 是的话当场把该功能关闭, // 被取消的那个本是不是「系统日历 → CalDAV」的备份目标 —— 是的话当场把该功能关闭,
// 否则用户会以为备份还在工作,实际却可能把系统日程导进别的本(用户实测反馈)。 // 否则用户会以为备份还在工作,实际却可能把系统日程导进别的本(用户实测反馈)。
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS'; import { buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
@@ -451,6 +451,19 @@ struct EditAccountPage {
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.alignItems(HorizontalAlign.Center) .alignItems(HorizontalAlign.Center)
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
} }
@Component @Component
+14 -1
View File
@@ -1,6 +1,6 @@
// entry/src/main/ets/pages/EventEditPage.ets // entry/src/main/ets/pages/EventEditPage.ets
// 日程编辑页:新建 / 修改 / 删除日程(一律属于某个 CalDAV 日历本,不再支持"本机"日程) // 日程编辑页:新建 / 修改 / 删除日程(一律属于某个 CalDAV 日历本,不再支持"本机"日程)
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit'; import { notificationManager } from '@kit.NotificationKit';
@@ -659,6 +659,19 @@ struct EventEditPage {
}) })
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
@Builder @Builder
timeRow(label: string, isStart: boolean) { timeRow(label: string, isStart: boolean) {
Row({ space: 8 }) { Row({ space: 8 }) {
+71 -25
View File
@@ -1,7 +1,7 @@
// entry/src/main/ets/pages/Index.ets // entry/src/main/ets/pages/Index.ets
// 同步日历主界面:月视图(左右滑动翻月)/ 周视图 / 日视图 / 日程列表 // 同步日历主界面:月视图(左右滑动翻月)/ 周视图 / 日视图 / 日程列表
// 混合展示 DAV 与系统日历;每分钟自动同步;可"回到今天" // 混合展示 DAV 与系统日历;每分钟自动同步;可"回到今天"
import { mediaquery, router } from '@kit.ArkUI'; import { curves, mediaquery, router } from '@kit.ArkUI';
import { common, abilityAccessCtrl, bundleManager } from '@kit.AbilityKit'; import { common, abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
import { geoLocationManager } from '@kit.LocationKit'; import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
@@ -54,8 +54,13 @@ struct Index {
@State syncing: boolean = false; @State syncing: boolean = false;
@State loading: boolean = true; @State loading: boolean = true;
@State menuOpen: boolean = false; // 顶部 ≡ 下拉菜单 @State menuOpen: boolean = false; // 顶部 ≡ 下拉菜单
@State isLandscape: boolean = false; // 横屏:月视图切左右双栏(左月历/右当日日程) // 宽屏(平板 / 折叠屏展开态):月视图切左右双栏(左月历 / 右当日日程)。
private landscapeListener: mediaquery.MediaQueryListener | null = null; // 判据是窗口**短边**是否达到大屏断点 840vp:短边在旋转时不变(宽高互换),
// 所以横竖屏布局天然保持一致 —— 满足"屏幕比例接近 1:1 的设备,横竖屏布局需保持一致";
// 同时不锁死屏幕方向,折叠屏展开态与平板仍可自由横竖屏切换。
@State wideLayout: boolean = false;
private widthListener: mediaquery.MediaQueryListener | null = null;
private heightListener: mediaquery.MediaQueryListener | null = null;
private accounts: DavAccount[] = []; private accounts: DavAccount[] = [];
private sources: CalSource[] = []; private sources: CalSource[] = [];
private swiperController: SwiperController = new SwiperController(); private swiperController: SwiperController = new SwiperController();
@@ -112,7 +117,7 @@ struct Index {
} }
} }
this.rebuildPages(); this.rebuildPages();
this.initLandscapeListener(); this.initLayoutListener();
// 首启同意门禁:未同意《隐私政策》与《用户协议》前,不申请任何系统权限、不加载/同步数据(AGC 硬性要求) // 首启同意门禁:未同意《隐私政策》与《用户协议》前,不申请任何系统权限、不加载/同步数据(AGC 硬性要求)
if (ctx === undefined) { if (ctx === undefined) {
this.policyChecked = true; this.policyChecked = true;
@@ -142,19 +147,47 @@ struct Index {
clearInterval(this.nowTimer); clearInterval(this.nowTimer);
this.nowTimer = -1; this.nowTimer = -1;
} }
if (this.landscapeListener !== null) { if (this.widthListener !== null) {
this.landscapeListener.off('change'); this.widthListener.off('change');
this.landscapeListener = null; this.widthListener = null;
}
if (this.heightListener !== null) {
this.heightListener.off('change');
this.heightListener = null;
} }
} }
/** 监听横竖屏:旋转后 isLandscape 驱动月视图在上下/左右布局间切换 */ /**
private initLandscapeListener(): void { * 宽屏判据:窗口短边 min(宽, 高) 是否达到大屏断点 840vp。
this.landscapeListener = * min(w,h) >= 840vp 等价于 (width >= 840vp) && (height >= 840vp),用两条 mediaquery 合成。
this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)'); *
this.isLandscape = this.landscapeListener.matches; * 为什么用"短边"而不是"是否横屏":
this.landscapeListener.on('change', (result: mediaquery.MediaQueryResult) => { * 1) 旋转时短边不变 → 横竖屏布局自动保持一致。屏幕比例接近 1:1 的设备(折叠屏展开态、
this.isLandscape = result.matches; * 平板)在旋转时不会出现"单栏 ⇄ 双栏"的布局跳变;
* 2) 不锁死屏幕方向,仍支持折叠屏展开态与平板的横竖屏自由切换显示。
*/
private applyLayoutMetrics(): void {
let wOk: boolean = false;
let hOk: boolean = false;
if (this.widthListener !== null) {
wOk = this.widthListener.matches;
}
if (this.heightListener !== null) {
hOk = this.heightListener.matches;
}
this.wideLayout = wOk && hOk;
}
private initLayoutListener(): void {
const mq = this.getUIContext().getMediaQuery();
this.widthListener = mq.matchMediaSync('(width >= 840vp)');
this.heightListener = mq.matchMediaSync('(height >= 840vp)');
this.applyLayoutMetrics();
this.widthListener.on('change', (): void => {
this.applyLayoutMetrics();
});
this.heightListener.on('change', (): void => {
this.applyLayoutMetrics();
}); });
} }
@@ -1071,6 +1104,19 @@ struct Index {
}) })
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
/** /**
* 首启隐私政策同意门禁(整页渲染,不用弹窗 —— 弹窗在本环境实测无法弹出)。 * 首启隐私政策同意门禁(整页渲染,不用弹窗 —— 弹窗在本环境实测无法弹出)。
* AGC 要求:明确同意前不得申请权限、不得收集/处理个人信息,故未同意时整页挡住主界面。 * AGC 要求:明确同意前不得申请权限、不得收集/处理个人信息,故未同意时整页挡住主界面。
@@ -1598,8 +1644,8 @@ struct Index {
@Builder @Builder
monthBody() { monthBody() {
if (this.isLandscape) { if (this.wideLayout) {
// 横屏(平板适配):左右双栏——左侧月历,右侧当日日程 // 宽屏(平板 / 折叠屏展开态):左右双栏——左侧月历,右侧当日日程
Row() { Row() {
Column() { Column() {
this.monthWeekHeader() this.monthWeekHeader()
@@ -1667,7 +1713,7 @@ struct Index {
}, (cell: MonthCell) => `${cell.dateMs}_${cell.dateMs === this.selectedDate}`) }, (cell: MonthCell) => `${cell.dateMs}_${cell.dateMs === this.selectedDate}`)
} }
.width('100%') .width('100%')
.layoutWeight(this.isLandscape ? 1 : 0) .layoutWeight(this.wideLayout ? 1 : 0)
}, (week: MonthCell[], idx: number) => String(idx)) }, (week: MonthCell[], idx: number) => String(idx))
} }
.width('100%') .width('100%')
@@ -1679,7 +1725,7 @@ struct Index {
.loop(false) .loop(false)
.indicator(false) .indicator(false)
.width('100%') .width('100%')
.layoutWeight(this.isLandscape ? 1 : 0) .layoutWeight(this.wideLayout ? 1 : 0)
.onChange((index: number) => { .onChange((index: number) => {
this.handleSwiperChange(index); this.handleSwiperChange(index);
}) })
@@ -1890,7 +1936,7 @@ struct Index {
@Builder @Builder
weekBody() { weekBody() {
if (this.isLandscape) { if (this.wideLayout) {
// 横屏(平板适配):左侧一周日期,右侧选中日日程 // 横屏(平板适配):左侧一周日期,右侧选中日日程
Row() { Row() {
Column() { Column() {
@@ -2525,20 +2571,20 @@ struct Index {
dayCell(cell: MonthCell) { dayCell(cell: MonthCell) {
Column({ space: 2 }) { Column({ space: 2 }) {
Text(String(cell.day)) Text(String(cell.day))
.fontSize(this.isLandscape ? 12 : 13) .fontSize(this.wideLayout ? 12 : 13)
.fontWeight(cell.isToday || cell.dateMs === this.selectedDate ? FontWeight.Bold : FontWeight.Normal) .fontWeight(cell.isToday || cell.dateMs === this.selectedDate ? FontWeight.Bold : FontWeight.Normal)
.fontColor(!cell.inMonth .fontColor(!cell.inMonth
? $r('app.color.text_hint') ? $r('app.color.text_hint')
: (cell.dateMs === this.selectedDate ? $r('app.color.button_text') : $r('app.color.text_primary'))) : (cell.dateMs === this.selectedDate ? $r('app.color.button_text') : $r('app.color.text_primary')))
.width(this.isLandscape ? 22 : 26) .width(this.wideLayout ? 22 : 26)
.height(this.isLandscape ? 22 : 26) .height(this.wideLayout ? 22 : 26)
.textAlign(TextAlign.Center) .textAlign(TextAlign.Center)
.borderRadius(this.isLandscape ? 11 : 13) .borderRadius(this.wideLayout ? 11 : 13)
.backgroundColor(cell.dateMs === this.selectedDate .backgroundColor(cell.dateMs === this.selectedDate
? $r('app.color.selected_bg') ? $r('app.color.selected_bg')
: (cell.isToday ? $r('app.color.today_bg') : Color.Transparent)) : (cell.isToday ? $r('app.color.today_bg') : Color.Transparent))
if (!this.isLandscape) { if (!this.wideLayout) {
// 横屏高度有限,省略农历腾出空间 // 宽屏下月历格子变窄,省略农历腾出空间
Text(cell.lunar) Text(cell.lunar)
.fontSize(8) .fontSize(8)
.fontColor(cell.dateMs === this.selectedDate .fontColor(cell.dateMs === this.selectedDate
+14 -1
View File
@@ -1,6 +1,6 @@
// entry/src/main/ets/pages/SettingsPage.ets // entry/src/main/ets/pages/SettingsPage.ets
// 设置页:系统日历模式(仅显示/备份到 CalDAV)+ 混合显示开关 + 自动同步间隔 + 后台同步 // 设置页:系统日历模式(仅显示/备份到 CalDAV)+ 混合显示开关 + 自动同步间隔 + 后台同步
import { router } from '@kit.ArkUI'; import { curves, router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { notificationManager } from '@kit.NotificationKit'; import { notificationManager } from '@kit.NotificationKit';
import { AppSettings } from '../common/AppSettings'; import { AppSettings } from '../common/AppSettings';
@@ -1462,6 +1462,19 @@ struct SettingsPage {
.height('100%') .height('100%')
} }
/**
* 页面转场:淡入淡出。
* 不做左右平移 / 上下位移(不调用 .slide() / .translate()),只做透明度变化,
* 避免单帧直接切换;曲线使用弹簧曲线(springMotion)。
*/
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 300, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
PageTransitionExit({ type: RouteType.None, duration: 250, curve: curves.springMotion(0.35, 0.75) })
.opacity(0);
}
/** 隐私政策与用户协议弹层 */ /** 隐私政策与用户协议弹层 */
@Builder @Builder
legalSheet() { legalSheet() {