Files
SyncCalendar/entry/src/main/ets/pages/EditAccountPage.ets
T
laoyang 5eaeeb0f4c 增强了安全配置。
Signed-off-by: Yang Yongquan <i@yangyq.net>
2026-09-15 12:51:58 +08:00

463 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// entry/src/main/ets/pages/EditAccountPage.ets
// 编辑账号:查看/重选该账号下的日历本、修改账户名
// 保存后清理失效日历本的本地数据,并触发一次重新同步
import { router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { DavAccount, AccountStore } from '../common/AccountStore';
import { DavClient, DavCalendarDiscovery, DavCalendarEntry } from '../common/DavClient';
import { EventDb } from '../common/EventDb';
import { LogUtil } from '../common/LogUtil';
/**
* 日历本条目(@Observed 使勾选状态变化能刷新 UI)
*/
@Observed
export class EditCalendarItem {
href: string;
name: string;
color: string; // 服务器定义的颜色(calendar-color),可能为空
selected: boolean;
constructor(href: string, name: string, color: string) {
this.href = href;
this.name = name;
this.color = color;
this.selected = false;
}
}
/**
* 路由参数(AppStorage 的兜底通道)
*/
export class EditNavParams {
accId: string = '';
}
@Entry
@Component
struct EditAccountPage {
@State accountName: string = '';
@State serverUrl: string = '';
@State username: string = '';
@State calendarList: EditCalendarItem[] = [];
@State isLoading: boolean = true;
@State statusMsg: string = '正在获取日历列表…';
@State statusOk: boolean = false;
@State isSaving: boolean = false;
@State selectedCount: number = 0;
@State allSelected: boolean = false;
private acc: DavAccount = new DavAccount();
private found: boolean = false;
aboutToAppear(): Promise<void> {
return this.initPage();
}
private async initPage(): Promise<void> {
try {
// 1) 读取目标账号 idAppStorage 为主,路由参数兜底)
let accId: string | undefined = AppStorage.get<string>('pendingEditAccountId');
if (accId === undefined || accId === '') {
const rawParams: Object | undefined = router.getParams();
if (rawParams instanceof EditNavParams && rawParams.accId !== '') {
accId = rawParams.accId;
}
}
console.info(`编辑账号 initPage: accId=${accId ?? '(undefined)'}`);
if (accId === undefined || accId === '') {
this.isLoading = false;
this.statusMsg = '未指定要编辑的账号,请返回账号列表重新点击';
this.statusOk = false;
return;
}
// 2) 获取应用上下文(getHostContext 过早调用可能为 undefinedgetContext 兜底)
let context: common.Context | undefined = undefined;
try {
context = this.getUIContext().getHostContext();
} catch (err) {
console.info('getHostContext 异常,使用 getContext 兜底');
}
if (context === undefined) {
context = getContext(this);
}
if (context === undefined) {
this.isLoading = false;
this.statusMsg = '无法获取应用上下文';
this.statusOk = false;
return;
}
// 3) 从账号列表里找到该账号
const accounts: DavAccount[] = await AccountStore.loadAll(context);
const foundAcc = accounts.find((a: DavAccount): boolean => a.id === accId);
if (foundAcc === undefined) {
this.isLoading = false;
this.statusMsg = `账号不存在(id=${accId}),可能已被删除`;
this.statusOk = false;
return;
}
this.acc = foundAcc;
this.found = true;
this.accountName = foundAcc.name;
this.serverUrl = foundAcc.serverUrl;
this.username = foundAcc.username;
LogUtil.write(`编辑账号 id=${foundAcc.id}:当前已选 ${foundAcc.calendarHrefs.length} 个日历本`);
for (let i = 0; i < foundAcc.calendarHrefs.length; i++) {
const nm: string = i < foundAcc.calendarNames.length ? foundAcc.calendarNames[i] : '';
LogUtil.write(` 已选日历本[${i}]「${nm}」${foundAcc.calendarHrefs[i]}`);
}
await this.fetchCalendars();
} catch (err) {
const e = err as BusinessError;
console.error(`编辑账号初始化失败: ${e.code} - ${e.message}`);
this.isLoading = false;
this.statusMsg = `页面初始化失败:${e.message}`;
this.statusOk = false;
}
}
private encodeBasicAuth(): string {
try {
return buffer.from(`${this.acc.username}:${this.acc.password}`).toString('base64');
} catch (err) {
const e = err as BusinessError;
console.error(`Base64 编码失败: ${e.message}`);
return '';
}
}
private async fetchCalendars(): Promise<void> {
const token: string = this.encodeBasicAuth();
if (token === '') {
this.isLoading = false;
this.statusMsg = '凭据编码失败:请在真机或模拟器上运行';
this.statusOk = false;
return;
}
// PROPFIND 与解析统一走 DavClient.listCalendars(与「添加账号」页共用同一实现)
try {
const disc: DavCalendarDiscovery = await DavClient.listCalendars(this.acc.serverUrl, 'Basic ' + token);
this.isLoading = false;
if (disc.statusCode === 401) {
this.statusMsg = '登录已失效,请检查账号密码';
this.statusOk = false;
return;
}
if (disc.statusCode !== 207 && disc.statusCode !== 200) {
this.statusMsg = `获取日历列表失败,服务器返回:${disc.statusCode}`;
this.statusOk = false;
return;
}
const list: EditCalendarItem[] = disc.entries.map((e: DavCalendarEntry): EditCalendarItem =>
new EditCalendarItem(e.href, e.displayName, e.color));
if (list.length === 0) {
LogUtil.write('编辑账号:未发现任何日历本');
this.statusMsg = '该路径下未发现日历本';
this.statusOk = false;
} else {
// 已勾选的日历本按账号当前配置预选
for (const item of list) {
item.selected = this.acc.calendarHrefs.includes(item.href);
LogUtil.write(`编辑账号发现日历本:「${item.name}」${item.href} 颜色=${item.color === '' ? '(无)' : item.color} 预选=${item.selected}`);
}
this.calendarList = list;
this.updateSelectionState();
this.statusMsg = `该账号共 ${list.length} 个日历本,当前已选 ${this.selectedCount} 个`;
this.statusOk = true;
}
} catch (err) {
const e = err as BusinessError;
console.error(`获取日历列表失败: ${e.code} - ${e.message}`);
this.isLoading = false;
this.statusMsg = `获取日历列表失败:${e.message}`;
this.statusOk = false;
}
}
/** 保存:更新账号的日历本选择与名称,清理失效数据,触发重新同步 */
private async saveSelection(): Promise<void> {
if (this.isSaving || !this.found) {
return;
}
if (this.accountName.trim() === '') {
this.statusMsg = '账户名不能为空';
this.statusOk = false;
return;
}
const selectedItems: EditCalendarItem[] = this.calendarList.filter((c: EditCalendarItem) => c.selected);
if (selectedItems.length === 0) {
this.statusMsg = '请至少勾选一个日历本';
this.statusOk = false;
return;
}
this.isSaving = true;
try {
const context = this.getUIContext().getHostContext();
if (context === undefined) {
this.statusMsg = '无法获取应用上下文';
this.statusOk = false;
this.isSaving = false;
return;
}
const accounts: DavAccount[] = await AccountStore.loadAll(context);
const target = accounts.find((a: DavAccount): boolean => a.id === this.acc.id);
if (target === undefined) {
this.statusMsg = '账号不存在,可能已被删除';
this.statusOk = false;
this.isSaving = false;
return;
}
target.name = this.accountName.trim();
target.calendarHrefs = selectedItems.map((c: EditCalendarItem): string => c.href);
target.calendarNames = selectedItems.map((c: EditCalendarItem): string => c.name);
target.calendarColors = selectedItems.map((c: EditCalendarItem): string => c.color);
LogUtil.write(`编辑账号保存:id=${target.id},新勾选 ${selectedItems.length}/${this.calendarList.length} 个日历本`);
await AccountStore.saveAll(context, accounts);
// 重选后 calKey(accId_序号)会变化,清理已取消勾选的日历本数据
const validKeys: string[] =
selectedItems.map((c: EditCalendarItem, i: number): string => `${target.id}_${i}`);
await EventDb.pruneAccountEvents(context, target.id, validKeys);
AppStorage.setOrCreate<string>('pendingSyncAccountId', target.id);
this.getUIContext().getPromptAction()
.showToast({ message: `已保存,同步 ${selectedItems.length} 个日历本` });
router.back();
} catch (err) {
const e = err as BusinessError;
console.error(`保存失败: ${e.code} - ${e.message}`);
this.statusMsg = `保存失败:${e.message}`;
this.statusOk = false;
}
this.isSaving = false;
}
private updateSelectionState(): void {
const count: number = this.calendarList.filter((c: EditCalendarItem): boolean => c.selected).length;
this.selectedCount = count;
this.allSelected = this.calendarList.length > 0 && count === this.calendarList.length;
}
private handleItemToggle(item: EditCalendarItem): void {
// 选中状态已在 EditCalendarRow 内部直接翻转,这里只刷新计数
this.updateSelectionState();
}
private toggleAll(): void {
const target: boolean = !this.allSelected;
this.calendarList.forEach((c: EditCalendarItem) => {
c.selected = target;
});
this.updateSelectionState();
}
build() {
Column({ space: 14 }) {
Row({ space: 6 }) {
Text('←')
.fontSize(18)
.fontColor($r('app.color.brand'))
Text('返回')
.fontSize(16)
.fontColor($r('app.color.brand'))
}
.width('100%')
.onClick(() => {
router.back();
})
Column({ space: 6 }) {
Text('编辑账号')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(this.username !== '' ? `${this.serverUrl} · ${this.username}` : this.serverUrl)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Column({ space: 8 }) {
Row({ space: 4 }) {
Text('日历账户名称')
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.fontWeight(FontWeight.Medium)
Text('*')
.fontSize(15)
.fontColor($r('app.color.error'))
}
TextInput({ text: this.accountName, placeholder: '例如:我的群晖日历' })
.height(46)
.fontSize(16)
.fontColor($r('app.color.text_primary'))
.placeholderColor($r('app.color.text_hint'))
.backgroundColor($r('app.color.input_bg'))
.borderRadius(10)
.border({ width: 1.5, color: $r('app.color.brand') })
.padding({ left: 12, right: 12 })
.onChange((value: string) => {
this.accountName = value;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor($r('app.color.card_bg'))
.border({ width: 1, color: $r('app.color.shadow_color') })
Row({ space: 8 }) {
Text(this.calendarList.length > 0
? `已选 ${this.selectedCount} / ${this.calendarList.length}` : ' ')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
Blank()
if (this.calendarList.length > 0) {
Button(this.allSelected ? '取消全选' : '全选')
.fontSize(13)
.fontColor($r('app.color.brand'))
.backgroundColor(Color.Transparent)
.border({ width: 1, color: $r('app.color.brand'), radius: 14 })
.height(30)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.toggleAll();
})
}
}
.width('100%')
Scroll() {
Column({ space: 4 }) {
if (this.isLoading) {
Column({ space: 12 }) {
LoadingProgress()
.width(36)
.height(36)
.color($r('app.color.brand'))
Text('正在从服务器获取日历列表…')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.padding(32)
} else if (this.calendarList.length === 0) {
Text(this.statusMsg)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
.padding(24)
.textAlign(TextAlign.Center)
} else {
ForEach(this.calendarList, (item: EditCalendarItem) => {
EditCalendarRow({
item: item,
onSelect: (selectedItem: EditCalendarItem): void => {
this.handleItemToggle(selectedItem);
}
})
// key 只用 href(稳定标识):若把选中态也拼进 key,勾选一次就会导致整行销毁重建
}, (item: EditCalendarItem) => item.href)
}
}
.width('100%')
.padding(8)
.constraintSize({ minHeight: '100%' })
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.align(Alignment.Top)
.borderRadius(16)
.backgroundColor($r('app.color.card_bg'))
.shadow({ radius: 12, color: $r('app.color.shadow_color'), offsetX: 0, offsetY: 4 })
if (this.statusMsg !== '' && !this.isLoading) {
Row({ space: 6 }) {
Text(this.statusOk ? '✓' : '✕')
.fontSize(14)
.fontColor(this.statusOk ? $r('app.color.success') : $r('app.color.error'))
Text(this.statusMsg)
.fontSize(14)
.fontColor(this.statusOk ? $r('app.color.success') : $r('app.color.error'))
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor(this.statusOk ? $r('app.color.success_bg') : $r('app.color.error_bg'))
}
Button() {
Text(this.isSaving ? '保存中…' : '保存并同步')
.fontSize(17)
.fontColor($r('app.color.button_text'))
.fontWeight(FontWeight.Medium)
}
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor(this.isSaving ? $r('app.color.brand_disabled') : $r('app.color.brand'))
.enabled(!this.isSaving && !this.isLoading && this.found)
.onClick(() => {
this.saveSelection();
})
}
.width('100%')
.height('100%')
.padding({ left: 24, right: 24, top: 16, bottom: 24 })
.backgroundColor($r('app.color.page_bg'))
.alignItems(HorizontalAlign.Center)
}
}
@Component
struct EditCalendarRow {
@ObjectLink item: EditCalendarItem;
onSelect: (item: EditCalendarItem) => void = (selectedItem: EditCalendarItem): void => {};
build() {
Row({ space: 12 }) {
if (this.item.color !== '') {
Circle()
.width(12)
.height(12)
.fill(this.item.color)
}
Text(this.item.name)
.fontSize(16)
.fontColor($r('app.color.text_primary'))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Blank()
if (this.item.selected) {
Text('✓')
.fontSize(14)
.fontColor($r('app.color.button_text'))
.width(24)
.height(24)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor($r('app.color.brand'))
} else {
Text('')
.width(24)
.height(24)
.borderRadius(12)
.border({ width: 1.5, color: $r('app.color.text_hint') })
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 14, bottom: 14 })
.borderRadius(8)
.onClick(() => {
// 直接翻转 @ObjectLink 属性,子组件自身即可触发刷新
this.item.selected = !this.item.selected;
this.onSelect(this.item);
})
}
}