From 7cf9203ec36df40d68491b1fa44c08267a9a266d Mon Sep 17 00:00:00 2001 From: Yang Yongquan Date: Tue, 15 Sep 2026 15:07:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E7=AD=89=E6=89=93=E5=BC=80=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Yang Yongquan --- entry/src/main/ets/common/AppSettings.ets | 12 +++-- entry/src/main/ets/common/DocViewer.ets | 56 +++++++++++++++++++++++ entry/src/main/ets/pages/Index.ets | 40 +++++++++++----- entry/src/main/ets/pages/SettingsPage.ets | 43 +++++++++++++++-- 4 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 entry/src/main/ets/common/DocViewer.ets diff --git a/entry/src/main/ets/common/AppSettings.ets b/entry/src/main/ets/common/AppSettings.ets index f434cec..be67894 100644 --- a/entry/src/main/ets/common/AppSettings.ets +++ b/entry/src/main/ets/common/AppSettings.ets @@ -23,11 +23,17 @@ export class AppSettings { // 否则一旦某次因故没显示出来,旧标记会把功能永久锁死(v2 正是这样被锁住 → 本次升到 v3)。 private static readonly KEY_TIPS_SHOWN: string = 'tips_shown_v3'; - /** 隐私政策网址(华为 AGC 隐私政策托管服务生成,上架时填此地址) */ - static readonly PRIVACY_URL: string = - 'https://agreement-drcn.hispace.dbankcloud.cn/index.html?lang=zh&agreementId=2039401852717510592'; + // 隐私政策 / 用户服务协议网址(自托管,AGC 上架的"隐私政策网址"填 PRIVACY_URL)。 + // ⚠️ 两份文件必须与安装包实际申请的 user_grant 权限保持一致: + // 应用仅申请 READ_CALENDAR / READ_WHOLE_CALENDAR / LOCATION / APPROXIMATELY_LOCATION(均前台), + // 文档中不得出现"在后台获取位置"(LOCATION_IN_BACKGROUND)与"添加/修改/删除日历活动"(WRITE_CALENDAR)。 + /** 隐私政策网址 */ + static readonly PRIVACY_URL: string = 'https://synccalendar.yangyq.net/upa.html'; /** 用户服务协议网址 */ static readonly AGREEMENT_URL: string = 'https://synccalendar.yangyq.net/usa.html'; + /** 文档标题(内置浏览器标题栏用) */ + static readonly PRIVACY_TITLE: string = '隐私政策'; + static readonly AGREEMENT_TITLE: string = '用户服务协议'; /** * 是否已同意《隐私政策》与《用户协议》(首启同意弹窗)。 diff --git a/entry/src/main/ets/common/DocViewer.ets b/entry/src/main/ets/common/DocViewer.ets new file mode 100644 index 0000000..faa8050 --- /dev/null +++ b/entry/src/main/ets/common/DocViewer.ets @@ -0,0 +1,56 @@ +// entry/src/main/ets/common/DocViewer.ets +// 内置文档浏览器:整页覆盖层 + Web 组件,用于在应用内展示《隐私政策》《用户服务协议》。 +// 为什么不用系统浏览器 / 弹窗: +// 1) 用户要求"点了就在应用里看,随手可关",不跳出应用; +// 2) 本环境实测系统弹层(CustomDialogController / bindSheet)由异步流程驱动时不可靠, +// 而"必须有"的界面一律用整页覆盖层(与首启同意门禁 consentGate 同一套路)。 +// 宿主用法:@State showDoc / docTitle / docUrl,根 Stack 最后一层 if (this.showDoc) { DocViewer({...}) }。 +import { webview } from '@kit.ArkWeb'; + +@Component +export struct DocViewer { + /** 标题栏文案(如"隐私政策") */ + title: string = ''; + /** 文档地址(http / https) */ + url: string = ''; + /** 点击"关闭"时回调 —— 由宿主把 showDoc 置回 false */ + onClose: () => void = () => {}; + + private controller: webview.WebviewController = new webview.WebviewController(); + + build() { + Column() { + // 标题栏:左标题、右关闭(点击即退出,符合"随手关掉"的预期) + Row() { + Text(this.title) + .fontSize(16) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + Text('✕') + .fontSize(16) + .fontColor($r('app.color.text_secondary')) + .padding({ left: 14, right: 14, top: 10, bottom: 10 }) + .onClick((): void => { + this.onClose(); + }) + } + .width('100%') + .height(52) + .padding({ left: 16, right: 4 }) + .backgroundColor($r('app.color.card_bg')) + .border({ width: { bottom: 0.5 }, color: { bottom: $r('app.color.shadow_color') } }) + + // 文档正文:交给系统 Web 内核渲染(页面自身已适配移动端与深色模式) + Web({ src: this.url, controller: this.controller }) + .width('100%') + .layoutWeight(1) + .domStorageAccess(true) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.page_bg')) + } +} diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 501edf9..29c17b9 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -15,6 +15,7 @@ import { CardDataService } from '../common/CardDataService'; import { ReminderService } from '../common/ReminderService'; import { AppSettings } from '../common/AppSettings'; import { TipsPanel } from '../common/TipsPanel'; +import { DocViewer } from '../common/DocViewer'; import { EventDb, LocalEvent, RemoteEvent } from '../common/EventDb'; import { RruleUtil } from '../common/RruleUtil'; import { IcsUtil } from '../common/IcsUtil'; @@ -80,6 +81,10 @@ struct Index { @State policyAgreed: boolean = false; // 是否已同意《隐私政策》与《用户协议》 private notifAsked: boolean = false; // 通知权限是否已申请(同意后才申请) @State showTips: boolean = false; // 首启功能引导(整页覆盖层:遮罩 + 底部卡片 + Swiper 三页) + // ---- 内置文档浏览器(《隐私政策》/《用户服务协议》,整页 Web 覆盖层,不跳出应用) ---- + @State showDoc: boolean = false; + @State docTitle: string = ''; + @State docUrl: string = ''; aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); @@ -277,17 +282,18 @@ struct Index { } } - /** 打开外部链接(隐私政策 / 用户协议 / 开源仓库等) */ - private async openExternalUrl(url: string): Promise { - const ctx = this.getUIContext().getHostContext(); - if (ctx === undefined) { + /** + * 用内置浏览器(整页 Web 覆盖层)打开法律文档 —— 不跳出应用、随时可关闭。 + * 仅放行 http(s):避免 URL 被误传为 file://、自定义 scheme 等而拉起本地文件或任意第三方应用。 + */ + private openDoc(title: string, url: string): void { + if (!url.startsWith('https://') && !url.startsWith('http://')) { + this.getUIContext().getPromptAction().showToast({ message: '仅支持打开 http(s) 链接' }); return; } - try { - await (ctx as common.UIAbilityContext).openLink(url); - } catch (err) { - this.getUIContext().getPromptAction().showToast({ message: '无法打开链接' }); - } + this.docTitle = title; + this.docUrl = url; + this.showDoc = true; } // ---------- 工具 ---------- @@ -1016,6 +1022,18 @@ struct Index { if (this.showTips) { this.tipsOverlay() } + + // 内置文档浏览器(《隐私政策》/《用户服务协议》): + // 根 Stack 最上层 —— 未同意时也能从同意门禁直接打开,看完点"✕"即刻回到门禁,无需切回应用。 + if (this.showDoc) { + DocViewer({ + title: this.docTitle, + url: this.docUrl, + onClose: (): void => { + this.showDoc = false; + } + }) + } } .width('100%') .height('100%') @@ -1054,7 +1072,7 @@ struct Index { .backgroundColor($r('app.color.card_bg')) .fontColor($r('app.color.brand')) .onClick(() => { - this.openExternalUrl(AppSettings.PRIVACY_URL); + this.openDoc(AppSettings.PRIVACY_TITLE, AppSettings.PRIVACY_URL); }) Button('《用户服务协议》') .width('100%') @@ -1062,7 +1080,7 @@ struct Index { .backgroundColor($r('app.color.card_bg')) .fontColor($r('app.color.brand')) .onClick(() => { - this.openExternalUrl(AppSettings.AGREEMENT_URL); + this.openDoc(AppSettings.AGREEMENT_TITLE, AppSettings.AGREEMENT_URL); }) Blank() Text('点击“同意并继续”,即表示你已阅读并同意上述两份文件。') diff --git a/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets index 590010b..d8f74be 100644 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ b/entry/src/main/ets/pages/SettingsPage.ets @@ -8,6 +8,7 @@ import { BackgroundSyncService } from '../common/BackgroundSyncService'; import { AccountStore, DavAccount } from '../common/AccountStore'; import { ReminderService } from '../common/ReminderService'; import { TipsPanel } from '../common/TipsPanel'; +import { DocViewer } from '../common/DocViewer'; const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60]; @@ -34,6 +35,10 @@ struct SettingsPage { @State showFeatures: boolean = false; // 软件特性弹层 @State showHelp: boolean = false; // 使用帮助弹层 @State showLegal: boolean = false; // 隐私政策与用户协议弹层 + // 内置文档浏览器(《隐私政策》/《用户服务协议》):整页 Web 覆盖层 + @State showDoc: boolean = false; + @State docTitle: string = ''; + @State docUrl: string = ''; @State showTips: boolean = false; // 功能引导卡片(底部弹出) @State defaultView: string = 'month'; // 打开 App 默认视图:month | week | agenda private context?: common.Context; @@ -377,8 +382,24 @@ struct SettingsPage { } } + /** + * 用内置浏览器(整页 Web 覆盖层)打开法律文档 —— 不跳出应用、随时可关闭。 + * 先收起"隐私政策与用户协议"弹层,再显示覆盖层,避免两层叠在一起看不清。 + */ + private openDoc(title: string, url: string): void { + if (!url.startsWith('https://') && !url.startsWith('http://')) { + this.getUIContext().getPromptAction().showToast({ message: '仅支持打开 http(s) 链接' }); + return; + } + this.showLegal = false; + this.docTitle = title; + this.docUrl = url; + this.showDoc = true; + } + build() { - Column() { + Stack() { + Column() { // 顶部 Row({ space: 10 }) { Text('←') @@ -933,10 +954,24 @@ struct SettingsPage { .layoutWeight(1) .align(Alignment.Top) .scrollBar(BarState.Off) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.page_bg')) + + // 内置文档浏览器(《隐私政策》/《用户服务协议》):整页覆盖层,点"✕"即刻返回设置页 + if (this.showDoc) { + DocViewer({ + title: this.docTitle, + url: this.docUrl, + onClose: (): void => { + this.showDoc = false; + } + }) + } } .width('100%') .height('100%') - .backgroundColor($r('app.color.page_bg')) } /** 功能引导卡片(与首页共用 TipsPanel) */ @@ -959,14 +994,14 @@ struct SettingsPage { .fontSize(15) .backgroundColor($r('app.color.brand')) .onClick(() => { - this.openUrl(AppSettings.PRIVACY_URL); + this.openDoc(AppSettings.PRIVACY_TITLE, AppSettings.PRIVACY_URL); }) Button('查看《用户服务协议》') .width('100%') .fontSize(15) .backgroundColor($r('app.color.brand')) .onClick(() => { - this.openUrl(AppSettings.AGREEMENT_URL); + this.openDoc(AppSettings.AGREEMENT_TITLE, AppSettings.AGREEMENT_URL); }) Text('《隐私政策》说明本应用如何收集、使用与保护你的信息(日历、位置等);《用户服务协议》说明使用本应用的权利与义务。两份文件随功能调整更新,最新版本以上述链接为准。') .fontSize(12)