首次提交:SyncCalendar 项目
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
// entry/src/main/ets/pages/AccountsPage.ets
|
||||
// DAV 账号管理(由原 CalDAVSync 首页移植)
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { DavAccount, AccountStore, TYPE_KEYS, TYPE_CALDAV, TYPE_CARDDAV, TYPE_WEBDAV } from '../common/AccountStore';
|
||||
import { SyncEngine } from '../common/SyncEngine';
|
||||
import { EditNavParams } from './EditAccountPage';
|
||||
import { EventDb } from '../common/EventDb';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct AccountsPage {
|
||||
@State accounts: DavAccount[] = [];
|
||||
@State showTypeMenu: boolean = false;
|
||||
@State syncing: boolean = false;
|
||||
@State syncingId: string = '';
|
||||
|
||||
aboutToAppear(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
LogUtil.init(ctx);
|
||||
LogUtil.write('---- 打开 DAV 账号页 ----');
|
||||
}
|
||||
this.reloadAccounts();
|
||||
}
|
||||
|
||||
onPageShow(): void {
|
||||
this.reloadAccounts();
|
||||
}
|
||||
|
||||
private async reloadAccounts(): Promise<void> {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
this.accounts = await AccountStore.loadAll(context);
|
||||
const pendingId: string | undefined = AppStorage.get<string>('pendingSyncAccountId');
|
||||
if (pendingId !== undefined && pendingId !== '') {
|
||||
AppStorage.setOrCreate<string>('pendingSyncAccountId', '');
|
||||
const found = this.accounts.find((a: DavAccount): boolean => a.id === pendingId);
|
||||
if (found !== undefined) {
|
||||
this.syncSingleAccount(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private typeLabel(type: string): string {
|
||||
if (type === TYPE_CALDAV) {
|
||||
return 'CalDAV 日历';
|
||||
}
|
||||
if (type === TYPE_CARDDAV) {
|
||||
return 'CardDAV 通讯录';
|
||||
}
|
||||
return 'WebDAV 文件';
|
||||
}
|
||||
|
||||
private formatNow(): string {
|
||||
const d = new Date();
|
||||
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
private async syncSingleAccount(acc: DavAccount): Promise<void> {
|
||||
if (this.syncing) {
|
||||
return;
|
||||
}
|
||||
this.syncing = true;
|
||||
this.syncingId = acc.id;
|
||||
try {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
if (acc.type === TYPE_CALDAV) {
|
||||
await SyncEngine.withTimeout(
|
||||
SyncEngine.syncAccount(context as common.UIAbilityContext, acc), 120000);
|
||||
}
|
||||
acc.itemCount = acc.calendarHrefs.length;
|
||||
acc.lastSyncTime = this.formatNow();
|
||||
await AccountStore.saveAll(context, this.accounts);
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `「${acc.name}」同步完成` });
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `「${acc.name}」同步失败:${e.message}` });
|
||||
} finally {
|
||||
this.syncing = false;
|
||||
this.syncingId = '';
|
||||
}
|
||||
}
|
||||
|
||||
private openAddPage(type: string): void {
|
||||
this.showTypeMenu = false;
|
||||
AppStorage.setOrCreate<string>('pendingAccountType', type);
|
||||
router.pushUrl({ url: 'pages/AddAccountPage' });
|
||||
}
|
||||
|
||||
/** 点击账号 → 进入编辑页(查看/重选日历本) */
|
||||
private openEditPage(acc: DavAccount): void {
|
||||
if (acc.type !== TYPE_CALDAV) {
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: '该类型账号暂不支持编辑日历本' });
|
||||
return;
|
||||
}
|
||||
AppStorage.setOrCreate<string>('pendingEditAccountId', acc.id);
|
||||
const params = new EditNavParams();
|
||||
params.accId = acc.id;
|
||||
router.pushUrl({ url: 'pages/EditAccountPage', params: params });
|
||||
}
|
||||
|
||||
/** 长按账号 → 弹出删除确认 */
|
||||
private askDeleteAccount(acc: DavAccount): void {
|
||||
this.getUIContext().showAlertDialog({
|
||||
title: '删除账号',
|
||||
message: `确定删除「${acc.name}」吗?\n\n该账号下的所有日历本、日程与待办的本地数据将一并删除。\n服务器上的数据不受影响。`,
|
||||
autoCancel: true,
|
||||
alignment: DialogAlignment.Center,
|
||||
primaryButton: {
|
||||
value: '取消',
|
||||
action: (): void => {}
|
||||
},
|
||||
secondaryButton: {
|
||||
value: '删除',
|
||||
fontColor: $r('app.color.error'),
|
||||
action: (): void => {
|
||||
this.confirmDeleteAccount(acc);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行删除:账号配置 + 该账号全部本地日程/待办 */
|
||||
private async confirmDeleteAccount(acc: DavAccount): Promise<void> {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.accounts = this.accounts.filter((a: DavAccount): boolean => a.id !== acc.id);
|
||||
await AccountStore.saveAll(context, this.accounts);
|
||||
await EventDb.deleteAccountEvents(context, acc.id);
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `「${acc.name}」已删除` });
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `删除失败:${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.BottomEnd }) {
|
||||
Column() {
|
||||
// 顶部
|
||||
Row({ space: 10 }) {
|
||||
Text('←')
|
||||
.fontSize(20)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
router.back();
|
||||
})
|
||||
Text('DAV 账号')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 20, top: 12, bottom: 8 })
|
||||
|
||||
if (this.accounts.length === 0) {
|
||||
this.emptyState()
|
||||
} else {
|
||||
Scroll() {
|
||||
Column({ space: 16 }) {
|
||||
ForEach(TYPE_KEYS, (type: string) => {
|
||||
if (this.accounts.some((a: DavAccount): boolean => a.type === type)) {
|
||||
Column({ space: 8 }) {
|
||||
Text(this.typeLabel(type))
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
ForEach(this.accounts.filter((a: DavAccount): boolean => a.type === type),
|
||||
(acc: DavAccount) => {
|
||||
AccountRow({
|
||||
acc: acc,
|
||||
isSyncing: this.syncingId === acc.id,
|
||||
onSelect: (selected: DavAccount): void => {
|
||||
this.openEditPage(selected);
|
||||
},
|
||||
onLongPress: (selected: DavAccount): void => {
|
||||
this.askDeleteAccount(selected);
|
||||
}
|
||||
})
|
||||
}, (acc: DavAccount) => acc.id)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
}
|
||||
}, (type: string) => type)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 20, bottom: 100 })
|
||||
.constraintSize({ minHeight: '100%' })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.Top)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
|
||||
if (this.showTypeMenu) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.onClick(() => {
|
||||
this.showTypeMenu = false;
|
||||
})
|
||||
}
|
||||
|
||||
if (this.showTypeMenu) {
|
||||
Column({ space: 10 }) {
|
||||
this.menuItem('日', 'CalDAV', '日历同步', TYPE_CALDAV)
|
||||
this.menuItem('人', 'CardDAV', '通讯录同步', TYPE_CARDDAV)
|
||||
this.menuItem('文', 'WebDAV', '文件访问', TYPE_WEBDAV)
|
||||
}
|
||||
.width(220)
|
||||
.padding(10)
|
||||
.borderRadius(16)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.shadow({ radius: 16, color: $r('app.color.shadow_color'), offsetX: 0, offsetY: 4 })
|
||||
.margin({ right: 24, bottom: 156 })
|
||||
}
|
||||
|
||||
Button() {
|
||||
Text('+')
|
||||
.fontSize(26)
|
||||
.fontColor($r('app.color.button_text'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
}
|
||||
.width(56)
|
||||
.height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.shadow({ radius: 8, color: $r('app.color.shadow_color'), offsetX: 0, offsetY: 2 })
|
||||
.margin(24)
|
||||
.onClick(() => {
|
||||
this.showTypeMenu = !this.showTypeMenu;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
emptyState() {
|
||||
Column({ space: 12 }) {
|
||||
Text('+')
|
||||
.fontSize(30)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
.width(72)
|
||||
.height(72)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(20)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.border({ width: 1.5, color: $r('app.color.shadow_color') })
|
||||
Text('还没有任何 DAV 账号')
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text('点击右下角 + 添加 CalDAV / CardDAV / WebDAV 账号')
|
||||
.fontSize(13)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
menuItem(badge: string, title: string, desc: string, type: string) {
|
||||
Row({ space: 12 }) {
|
||||
Text(badge)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.width(40)
|
||||
.height(40)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(10)
|
||||
.backgroundColor($r('app.color.input_bg'))
|
||||
Column({ space: 2 }) {
|
||||
Text(title)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text(desc)
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(8)
|
||||
.borderRadius(10)
|
||||
.onClick(() => {
|
||||
this.openAddPage(type);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
struct AccountRow {
|
||||
@ObjectLink acc: DavAccount;
|
||||
@Prop isSyncing: boolean = false;
|
||||
onSelect: (acc: DavAccount) => void = (selected: DavAccount): void => {};
|
||||
onLongPress: (acc: DavAccount) => void = (selected: DavAccount): void => {};
|
||||
|
||||
build() {
|
||||
Row({ space: 12 }) {
|
||||
Text(this.acc.name !== '' ? this.acc.name.substring(0, 1) : 'D')
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.button_text'))
|
||||
.width(40)
|
||||
.height(40)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(10)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
Column({ space: 4 }) {
|
||||
Text(this.acc.name)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.acc.serverUrl)
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.acc.lastSyncTime === ''
|
||||
? '尚未同步'
|
||||
: `上次同步 ${this.acc.lastSyncTime} · ${this.acc.calendarHrefs.length} 个日历本`)
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
if (this.isSyncing) {
|
||||
LoadingProgress()
|
||||
.width(20)
|
||||
.height(20)
|
||||
.color($r('app.color.brand'))
|
||||
} else {
|
||||
Text('›')
|
||||
.fontSize(20)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(12)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.border({ width: 1, color: $r('app.color.shadow_color') })
|
||||
.onClick(() => {
|
||||
this.onSelect(this.acc);
|
||||
})
|
||||
.gesture(LongPressGesture({ repeat: false })
|
||||
.onAction((event: GestureEvent) => {
|
||||
this.onLongPress(this.acc);
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// entry/src/main/ets/pages/AddAccountPage.ets
|
||||
// 添加账号第一页:URL / 用户名 / 密码 → 连接(凭据经 AppStorage 传给日历本选择页)
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { buffer } from '@kit.ArkTS';
|
||||
import url from '@ohos.url';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { TYPE_CALDAV, TYPE_CARDDAV, TYPE_WEBDAV } from '../common/AccountStore';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct AddAccountPage {
|
||||
@State serverUrl: string = '';
|
||||
@State username: string = '';
|
||||
@State password: string = '';
|
||||
@State isLoading: boolean = false;
|
||||
@State statusMsg: string = '';
|
||||
@State statusOk: boolean = false;
|
||||
@State typeLabel: string = 'CalDAV';
|
||||
private accountType: string = TYPE_CALDAV;
|
||||
|
||||
aboutToAppear(): void {
|
||||
const t: string | undefined = AppStorage.get<string>('pendingAccountType');
|
||||
this.accountType = (t === undefined || t === '') ? TYPE_CALDAV : t;
|
||||
if (this.accountType === TYPE_CARDDAV) {
|
||||
this.typeLabel = 'CardDAV';
|
||||
} else if (this.accountType === TYPE_WEBDAV) {
|
||||
this.typeLabel = 'WebDAV';
|
||||
} else {
|
||||
this.typeLabel = 'CalDAV';
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeUrl(): string | null {
|
||||
let rawUrl: string = this.serverUrl.trim();
|
||||
if (rawUrl === '') {
|
||||
return null;
|
||||
}
|
||||
if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://')) {
|
||||
rawUrl = 'https://' + rawUrl;
|
||||
}
|
||||
const hostPattern: RegExp =
|
||||
/^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$|^\d{1,3}(\.\d{1,3}){3}$|^\[[0-9A-Fa-f:]+\]$|^localhost$/;
|
||||
try {
|
||||
const parsed = url.URL.parseURL(rawUrl);
|
||||
const hostname: string = parsed.hostname !== '' ? parsed.hostname : parsed.host;
|
||||
if (hostname !== '' && hostPattern.test(hostname)) {
|
||||
return rawUrl;
|
||||
}
|
||||
this.statusMsg = `URL 主机名无效:${rawUrl}`;
|
||||
this.statusOk = false;
|
||||
return null;
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`URL 解析异常(${e.code}),使用正则兜底: ${e.message}`);
|
||||
const fallbackPattern: RegExp =
|
||||
/^https?:\/\/[^\s/:?#]+(:\d{1,5})?([/?#][^\s]*)?$/;
|
||||
if (fallbackPattern.test(rawUrl)) {
|
||||
return rawUrl;
|
||||
}
|
||||
this.statusMsg = `URL 解析失败:${e.message}`;
|
||||
this.statusOk = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private encodeBasicAuth(): string {
|
||||
try {
|
||||
return buffer.from(`${this.username}:${this.password}`).toString('base64');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`Base64 编码失败: ${e.message}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async sendOnce(serverUrl: string, method: http.RequestMethod, authHeader: string): Promise<number> {
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const resp: http.HttpResponse = await httpRequest.request(serverUrl, {
|
||||
method: method,
|
||||
header: {
|
||||
'Authorization': authHeader,
|
||||
'Accept': '*/*',
|
||||
'User-Agent': 'SyncCalendar/1.0'
|
||||
},
|
||||
connectTimeout: 10000,
|
||||
readTimeout: 10000
|
||||
});
|
||||
return resp.responseCode;
|
||||
} finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private async probeServer(serverUrl: string): Promise<boolean> {
|
||||
const token: string = this.encodeBasicAuth();
|
||||
if (token === '') {
|
||||
this.statusMsg = '凭据编码失败:请在真机或模拟器上运行';
|
||||
this.statusOk = false;
|
||||
return false;
|
||||
}
|
||||
const authHeader: string = 'Basic ' + token;
|
||||
try {
|
||||
let code: number = await this.sendOnce(serverUrl, http.RequestMethod.OPTIONS, authHeader);
|
||||
if (code === 401) {
|
||||
code = await this.sendOnce(serverUrl, http.RequestMethod.GET, authHeader);
|
||||
}
|
||||
if (code === 401) {
|
||||
this.statusMsg = '服务器拒绝凭据(401),请检查用户名密码';
|
||||
this.statusOk = false;
|
||||
return false;
|
||||
}
|
||||
if (code >= 200 && code < 500) {
|
||||
this.statusMsg = '服务器连接成功';
|
||||
this.statusOk = true;
|
||||
return true;
|
||||
}
|
||||
this.statusMsg = `服务器返回异常状态码:${code}`;
|
||||
this.statusOk = false;
|
||||
return false;
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`连接失败: ${e.code} - ${e.message}`);
|
||||
this.statusMsg = `无法连接服务器:${e.message}`;
|
||||
this.statusOk = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async onConnectAndSave(): Promise<void> {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
const targetUrl: string | null = this.normalizeUrl();
|
||||
if (targetUrl === null) {
|
||||
if (this.statusMsg === '' || this.statusMsg === '正在连接服务器…' || this.statusOk) {
|
||||
this.statusMsg = '请输入有效的服务器地址';
|
||||
}
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
if (!this.username.trim()) {
|
||||
this.statusMsg = '请输入用户名';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
if (!this.password) {
|
||||
this.statusMsg = '请输入密码';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.statusMsg = '正在连接服务器…';
|
||||
this.statusOk = false;
|
||||
|
||||
const ok: boolean = await this.probeServer(targetUrl);
|
||||
if (ok) {
|
||||
AppStorage.setOrCreate<string>('pendingDavUrl', targetUrl);
|
||||
AppStorage.setOrCreate<string>('pendingDavUsername', this.username.trim());
|
||||
AppStorage.setOrCreate<string>('pendingDavPassword', this.password);
|
||||
this.getUIContext().getPromptAction().showToast({ message: '连接成功' });
|
||||
router.pushUrl({ url: 'pages/CalendarListPage' });
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 24 }) {
|
||||
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: 8 }) {
|
||||
Text(`添加${this.typeLabel}账号`)
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text('输入服务器账号信息')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
|
||||
Column({ space: 16 }) {
|
||||
this.formField('服务器地址', '例如:https://nas.example.com/caldav/', this.serverUrl,
|
||||
(value: string) => {
|
||||
this.serverUrl = value;
|
||||
}, false)
|
||||
this.formField('用户名', '请输入用户名', this.username,
|
||||
(value: string) => {
|
||||
this.username = value;
|
||||
}, false)
|
||||
this.formField('密码', '请输入密码', this.password,
|
||||
(value: string) => {
|
||||
this.password = value;
|
||||
}, true)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(20)
|
||||
.borderRadius(16)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.shadow({ radius: 12, color: $r('app.color.shadow_color'), offsetX: 0, offsetY: 4 })
|
||||
|
||||
Button() {
|
||||
Row({ space: 8 }) {
|
||||
if (this.isLoading) {
|
||||
LoadingProgress()
|
||||
.width(20)
|
||||
.height(20)
|
||||
.color($r('app.color.button_text'))
|
||||
}
|
||||
Text(this.isLoading ? '连接中…' : '连接并保存')
|
||||
.fontSize(17)
|
||||
.fontColor($r('app.color.button_text'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.borderRadius(24)
|
||||
.backgroundColor(this.isLoading ? $r('app.color.brand_disabled') : $r('app.color.brand'))
|
||||
.enabled(!this.isLoading)
|
||||
.onClick(() => {
|
||||
this.onConnectAndSave();
|
||||
})
|
||||
|
||||
if (this.statusMsg) {
|
||||
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'))
|
||||
}
|
||||
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ left: 24, right: 24, top: 16, bottom: 24 })
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
formField(label: string, placeholder: string, value: string,
|
||||
onChange: (value: string) => void, isPassword: boolean) {
|
||||
Column({ space: 8 }) {
|
||||
Text(label)
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
TextInput({ text: value, placeholder: placeholder })
|
||||
.type(isPassword ? InputType.Password : InputType.Normal)
|
||||
.showPasswordIcon(isPassword)
|
||||
.height(44)
|
||||
.fontSize(15)
|
||||
.backgroundColor($r('app.color.input_bg'))
|
||||
.borderRadius(8)
|
||||
.onChange(onChange)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// entry/src/main/ets/pages/CalendarListPage.ets
|
||||
// 添加账号第二页:PROPFIND 列出日历本 → 勾选 → 命名 → 保存账号
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { buffer } from '@kit.ArkTS';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { DavAccount, AccountStore, TYPE_CALDAV } from '../common/AccountStore';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
|
||||
/**
|
||||
* 日历本条目(@Observed 使勾选状态变化能刷新 UI)
|
||||
*/
|
||||
@Observed
|
||||
export class CalendarItem {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct CalendarListPage {
|
||||
@State calendarList: CalendarItem[] = [];
|
||||
@State accountName: string = '';
|
||||
@State isLoading: boolean = true;
|
||||
@State statusMsg: string = '正在获取日历列表…';
|
||||
@State statusOk: boolean = false;
|
||||
@State isSaving: boolean = false;
|
||||
@State selectedCount: number = 0;
|
||||
@State allSelected: boolean = false;
|
||||
private serverUrl: string = '';
|
||||
private username: string = '';
|
||||
private password: string = '';
|
||||
|
||||
aboutToAppear(): Promise<void> {
|
||||
return this.initPage();
|
||||
}
|
||||
|
||||
private async initPage(): Promise<void> {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
LogUtil.init(ctx);
|
||||
}
|
||||
this.serverUrl = AppStorage.get<string>('pendingDavUrl') ?? '';
|
||||
this.username = AppStorage.get<string>('pendingDavUsername') ?? '';
|
||||
this.password = AppStorage.get<string>('pendingDavPassword') ?? '';
|
||||
LogUtil.write(`添加账号流程开始:服务器=${this.serverUrl} 用户名=${this.username}`);
|
||||
if (this.serverUrl === '') {
|
||||
this.isLoading = false;
|
||||
this.statusMsg = '尚未连接服务器,请先返回重新连接';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
await this.fetchCalendars();
|
||||
}
|
||||
|
||||
private encodeBasicAuth(): string {
|
||||
try {
|
||||
return buffer.from(`${this.username}:${this.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;
|
||||
}
|
||||
const requestBody: string = '<?xml version="1.0" encoding="utf-8"?>' +
|
||||
'<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" ' +
|
||||
'xmlns:ical="http://apple.com/ns/ical/"><d:prop>' +
|
||||
'<d:displayname/><d:resourcetype/><cs:getcolor/><ical:calendar-color/>' +
|
||||
'</d:prop></d:propfind>';
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const resp: http.HttpResponse = await httpRequest.request(this.serverUrl, {
|
||||
method: 'PROPFIND' as http.RequestMethod,
|
||||
header: {
|
||||
'Authorization': 'Basic ' + token,
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'Depth': '1',
|
||||
'User-Agent': 'SyncCalendar/1.0'
|
||||
},
|
||||
extraData: requestBody,
|
||||
connectTimeout: 10000,
|
||||
readTimeout: 15000
|
||||
});
|
||||
console.info(`PROPFIND 响应码: ${resp.responseCode}`);
|
||||
if (resp.responseCode === 401) {
|
||||
this.isLoading = false;
|
||||
this.statusMsg = '登录已失效,请返回重新连接';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
if (resp.responseCode !== 207 && resp.responseCode !== 200) {
|
||||
this.isLoading = false;
|
||||
this.statusMsg = `获取日历列表失败,服务器返回:${resp.responseCode}`;
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
const xml: string = resp.result as string;
|
||||
LogUtil.write(`添加账号 PROPFIND → ${resp.responseCode},响应体 ${xml.length} 字符`);
|
||||
const list: CalendarItem[] = this.parseCalendarList(xml);
|
||||
for (const item of list) {
|
||||
LogUtil.write(`发现日历本:「${item.name}」${item.href} 颜色=${item.color === '' ? '(无)' : item.color}`);
|
||||
}
|
||||
this.isLoading = false;
|
||||
if (list.length === 0) {
|
||||
this.statusMsg = '该路径下未发现日历本(没有包含 calendar 资源类型的集合)';
|
||||
this.statusOk = false;
|
||||
} else {
|
||||
this.calendarList = list;
|
||||
this.updateSelectionState();
|
||||
this.statusMsg = `发现 ${list.length} 个日历本,请勾选要同步的日历`;
|
||||
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;
|
||||
} finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private extractTag(xml: string, tag: string): string {
|
||||
const pattern: string = '<([\\w-]+:)?' + tag + '(?:\\s[^>]*)?>([\\s\\S]*?)<\\/([\\w-]+:)?' + tag + '>';
|
||||
const regex: RegExp = new RegExp(pattern, 'i');
|
||||
const match = regex.exec(xml);
|
||||
return match !== null ? match[2].trim() : '';
|
||||
}
|
||||
|
||||
private parseCalendarList(xml: string): CalendarItem[] {
|
||||
const items: CalendarItem[] = [];
|
||||
const originMatch = /https?:\/\/[^/]+/i.exec(this.serverUrl);
|
||||
const origin: string = originMatch !== null ? originMatch[0] : '';
|
||||
const blocks: string[] = xml.split(/<\/[\w-]*:?response>/i);
|
||||
for (const block of blocks) {
|
||||
if (!/<[\w-]*:?response[\s>]/i.test(block)) {
|
||||
continue;
|
||||
}
|
||||
const href: string = this.extractTag(block, 'href');
|
||||
if (href === '') {
|
||||
continue;
|
||||
}
|
||||
const resourcetype: string = this.extractTag(block, 'resourcetype');
|
||||
if (!/calendar/i.test(resourcetype)) {
|
||||
continue;
|
||||
}
|
||||
let name: string = this.extractTag(block, 'displayname');
|
||||
if (name === '') {
|
||||
const segs: string[] = href.split('/').filter((s: string) => s !== '');
|
||||
if (segs.length > 0) {
|
||||
try {
|
||||
name = decodeURIComponent(segs[segs.length - 1]);
|
||||
} catch (err) {
|
||||
name = segs[segs.length - 1];
|
||||
}
|
||||
} else {
|
||||
name = href;
|
||||
}
|
||||
}
|
||||
// 服务器端颜色:cs:getcolor 或 ical:calendar-color,带 Alpha 时转成 #RRGGBB
|
||||
let color: string = AccountStore.normalizeColor(this.extractTag(block, 'getcolor'));
|
||||
if (color === '') {
|
||||
color = AccountStore.normalizeColor(this.extractTag(block, 'calendar-color'));
|
||||
}
|
||||
const fullHref: string = href.startsWith('http') ? href : origin + href;
|
||||
items.push(new CalendarItem(fullHref, name, color));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private async saveSelection(): Promise<void> {
|
||||
if (this.isSaving) {
|
||||
return;
|
||||
}
|
||||
if (this.accountName.trim() === '') {
|
||||
this.statusMsg = '请先给这个日历账户起一个名字';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
const selectedItems: CalendarItem[] = this.calendarList.filter((c: CalendarItem) => 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 acc = new DavAccount();
|
||||
acc.id = String(Date.now());
|
||||
const accType: string | undefined = AppStorage.get<string>('pendingAccountType');
|
||||
acc.type = (accType === undefined || accType === '') ? TYPE_CALDAV : accType;
|
||||
acc.name = this.accountName.trim();
|
||||
acc.serverUrl = this.serverUrl;
|
||||
acc.username = this.username;
|
||||
acc.password = this.password;
|
||||
acc.calendarHrefs = selectedItems.map((c: CalendarItem): string => c.href);
|
||||
acc.calendarNames = selectedItems.map((c: CalendarItem): string => c.name);
|
||||
acc.calendarColors = selectedItems.map((c: CalendarItem): string => c.color);
|
||||
LogUtil.write(`保存账号「${acc.name}」:id=${acc.id},勾选 ${selectedItems.length}/${this.calendarList.length} 个日历本`);
|
||||
await AccountStore.addAccount(context, acc);
|
||||
AppStorage.setOrCreate<string>('pendingSyncAccountId', acc.id);
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `账号已保存,共 ${selectedItems.length} 个日历本` });
|
||||
this.statusMsg = '保存成功';
|
||||
this.statusOk = true;
|
||||
router.back({ url: 'pages/AccountsPage' });
|
||||
} 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: CalendarItem): boolean => c.selected).length;
|
||||
this.selectedCount = count;
|
||||
this.allSelected = this.calendarList.length > 0 && count === this.calendarList.length;
|
||||
}
|
||||
|
||||
private handleItemToggle(item: CalendarItem): void {
|
||||
item.selected = !item.selected;
|
||||
this.updateSelectionState();
|
||||
}
|
||||
|
||||
private toggleAll(): void {
|
||||
const target: boolean = !this.allSelected;
|
||||
this.calendarList.forEach((c: CalendarItem) => {
|
||||
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('勾选需要同步的日历本,并为账户命名')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.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: CalendarItem) => {
|
||||
CalendarRow({
|
||||
item: item,
|
||||
onSelect: (selectedItem: CalendarItem): void => {
|
||||
this.handleItemToggle(selectedItem);
|
||||
}
|
||||
})
|
||||
}, (item: CalendarItem) => 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)
|
||||
.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 CalendarRow {
|
||||
@ObjectLink item: CalendarItem;
|
||||
onSelect: (item: CalendarItem) => void = (selectedItem: CalendarItem): void => {};
|
||||
|
||||
build() {
|
||||
Row({ space: 12 }) {
|
||||
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(() => {
|
||||
this.onSelect(this.item);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
// entry/src/main/ets/pages/EditAccountPage.ets
|
||||
// 编辑账号:查看/重选该账号下的日历本、修改账户名
|
||||
// 保存后清理失效日历本的本地数据,并触发一次重新同步
|
||||
import { http } from '@kit.NetworkKit';
|
||||
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 { 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) 读取目标账号 id(AppStorage 为主,路由参数兜底)
|
||||
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 过早调用可能为 undefined,getContext 兜底)
|
||||
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(`编辑账号「${foundAcc.name}」: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;
|
||||
}
|
||||
const requestBody: string = '<?xml version="1.0" encoding="utf-8"?>' +
|
||||
'<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" ' +
|
||||
'xmlns:ical="http://apple.com/ns/ical/"><d:prop>' +
|
||||
'<d:displayname/><d:resourcetype/><cs:getcolor/><ical:calendar-color/>' +
|
||||
'</d:prop></d:propfind>';
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const resp: http.HttpResponse = await httpRequest.request(this.acc.serverUrl, {
|
||||
method: 'PROPFIND' as http.RequestMethod,
|
||||
header: {
|
||||
'Authorization': 'Basic ' + token,
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'Depth': '1',
|
||||
'User-Agent': 'SyncCalendar/1.0'
|
||||
},
|
||||
extraData: requestBody,
|
||||
connectTimeout: 10000,
|
||||
readTimeout: 15000
|
||||
});
|
||||
console.info(`编辑账号 PROPFIND 响应码: ${resp.responseCode}`);
|
||||
if (resp.responseCode === 401) {
|
||||
this.isLoading = false;
|
||||
this.statusMsg = '登录已失效,请检查账号密码';
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
if (resp.responseCode !== 207 && resp.responseCode !== 200) {
|
||||
this.isLoading = false;
|
||||
this.statusMsg = `获取日历列表失败,服务器返回:${resp.responseCode}`;
|
||||
this.statusOk = false;
|
||||
return;
|
||||
}
|
||||
const xml: string = resp.result as string;
|
||||
LogUtil.write(`编辑账号 PROPFIND → ${resp.responseCode},响应体 ${xml.length} 字符`);
|
||||
const list: EditCalendarItem[] = this.parseCalendarList(xml);
|
||||
this.isLoading = false;
|
||||
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;
|
||||
} finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private extractTag(xml: string, tag: string): string {
|
||||
const pattern: string = '<([\\w-]+:)?' + tag + '(?:\\s[^>]*)?>([\\s\\S]*?)<\\/([\\w-]+:)?' + tag + '>';
|
||||
const regex: RegExp = new RegExp(pattern, 'i');
|
||||
const match = regex.exec(xml);
|
||||
return match !== null ? match[2].trim() : '';
|
||||
}
|
||||
|
||||
private parseCalendarList(xml: string): EditCalendarItem[] {
|
||||
const items: EditCalendarItem[] = [];
|
||||
const originMatch = /https?:\/\/[^/]+/i.exec(this.acc.serverUrl);
|
||||
const origin: string = originMatch !== null ? originMatch[0] : '';
|
||||
const blocks: string[] = xml.split(/<\/[\w-]*:?response>/i);
|
||||
for (const block of blocks) {
|
||||
if (!/<[\w-]*:?response[\s>]/i.test(block)) {
|
||||
continue;
|
||||
}
|
||||
const href: string = this.extractTag(block, 'href');
|
||||
if (href === '') {
|
||||
continue;
|
||||
}
|
||||
const resourcetype: string = this.extractTag(block, 'resourcetype');
|
||||
if (!/calendar/i.test(resourcetype)) {
|
||||
continue;
|
||||
}
|
||||
let name: string = this.extractTag(block, 'displayname');
|
||||
if (name === '') {
|
||||
const segs: string[] = href.split('/').filter((s: string) => s !== '');
|
||||
if (segs.length > 0) {
|
||||
try {
|
||||
name = decodeURIComponent(segs[segs.length - 1]);
|
||||
} catch (err) {
|
||||
name = segs[segs.length - 1];
|
||||
}
|
||||
} else {
|
||||
name = href;
|
||||
}
|
||||
}
|
||||
let color: string = AccountStore.normalizeColor(this.extractTag(block, 'getcolor'));
|
||||
if (color === '') {
|
||||
color = AccountStore.normalizeColor(this.extractTag(block, 'calendar-color'));
|
||||
}
|
||||
const fullHref: string = href.startsWith('http') ? href : origin + href;
|
||||
items.push(new EditCalendarItem(fullHref, name, color));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** 保存:更新账号的日历本选择与名称,清理失效数据,触发重新同步 */
|
||||
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(`编辑账号保存:「${target.name}」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);
|
||||
}
|
||||
})
|
||||
}, (item: EditCalendarItem) => `${item.href}_${item.selected}`)
|
||||
}
|
||||
}
|
||||
.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);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
// entry/src/main/ets/pages/EventEditPage.ets
|
||||
// 日程编辑页:新建 / 修改 / 删除本地(DAV 或本机)日程
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { DavAccount, AccountStore, CalSource, BookPalette } from '../common/AccountStore';
|
||||
import { EventDb, LocalEvent } from '../common/EventDb';
|
||||
import { DavClient } from '../common/DavClient';
|
||||
import { SyncEngine } from '../common/SyncEngine';
|
||||
|
||||
/** 可选的日历本 */
|
||||
class BookChoice {
|
||||
calKey: string = '';
|
||||
href: string = '';
|
||||
name: string = '';
|
||||
color: string = '#007DFF';
|
||||
}
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct EventEditPage {
|
||||
@State title: string = '';
|
||||
@State location: string = '';
|
||||
@State description: string = '';
|
||||
@State allDay: boolean = false;
|
||||
@State startMs: number = 0;
|
||||
@State endMs: number = 0;
|
||||
@State books: BookChoice[] = [];
|
||||
@State chosenKey: string = '';
|
||||
@State isSaving: boolean = false;
|
||||
@State statusMsg: string = '';
|
||||
@State isExisting: boolean = false;
|
||||
private event: LocalEvent | null = null;
|
||||
|
||||
aboutToAppear(): Promise<void> {
|
||||
return this.initPage();
|
||||
}
|
||||
|
||||
private async initPage(): Promise<void> {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
// 收集可写入的日历本(DAV + 本机)
|
||||
const sources: CalSourceWithHref[] = await CalendarDataBridge.loadWritableSources(context);
|
||||
const choices: BookChoice[] = [];
|
||||
for (const s of sources) {
|
||||
const b = new BookChoice();
|
||||
b.calKey = s.calKey;
|
||||
b.href = s.href;
|
||||
b.name = s.name;
|
||||
b.color = s.color;
|
||||
choices.push(b);
|
||||
}
|
||||
this.books = choices;
|
||||
|
||||
// 编辑既有事件
|
||||
const pendingId: number | undefined = AppStorage.get<number>('pendingEventId');
|
||||
if (pendingId !== undefined && pendingId > 0) {
|
||||
const loaded = await EventDb.getById(context, pendingId);
|
||||
if (loaded !== null) {
|
||||
this.event = loaded;
|
||||
this.isExisting = true;
|
||||
this.title = loaded.title;
|
||||
this.location = loaded.location;
|
||||
this.description = loaded.description;
|
||||
this.allDay = loaded.isAllDay;
|
||||
this.startMs = loaded.startTime;
|
||||
this.endMs = loaded.endTime;
|
||||
this.chosenKey = loaded.calKey;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 新建:默认时间 = 所选日期 9:00-10:00
|
||||
const base: number = AppStorage.get<number>('pendingEventDate') ?? Date.now();
|
||||
const dayStart = new Date(new Date(base).getFullYear(), new Date(base).getMonth(),
|
||||
new Date(base).getDate()).getTime();
|
||||
this.startMs = dayStart + 9 * 3600000;
|
||||
this.endMs = dayStart + 10 * 3600000;
|
||||
if (choices.length > 0) {
|
||||
this.chosenKey = choices[0].calKey;
|
||||
}
|
||||
}
|
||||
|
||||
private chosenBook(): BookChoice | null {
|
||||
return this.books.find((b: BookChoice): boolean => b.calKey === this.chosenKey) ?? null;
|
||||
}
|
||||
|
||||
private fmtDate(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||||
}
|
||||
|
||||
private fmtTime(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
||||
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
private pickStartDate(): void {
|
||||
const cur = new Date(this.startMs);
|
||||
DatePickerDialog.show({
|
||||
start: new Date(2000, 0, 1),
|
||||
end: new Date(2050, 11, 31),
|
||||
selected: cur,
|
||||
onDateAccept: (value: Date) => {
|
||||
const keep = new Date(this.startMs);
|
||||
const newStart: number = new Date(value.getFullYear(), value.getMonth(), value.getDate(),
|
||||
keep.getHours(), keep.getMinutes()).getTime();
|
||||
const dur: number = this.endMs - this.startMs;
|
||||
this.startMs = newStart;
|
||||
this.endMs = this.allDay ? newStart + 86399999 : newStart + dur;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private pickStartTime(): void {
|
||||
const cur = new Date(this.startMs);
|
||||
TimePickerDialog.show({
|
||||
selected: cur,
|
||||
onAccept: (value: TimePickerResult) => {
|
||||
const d = new Date(this.startMs);
|
||||
const newStart: number = new Date(d.getFullYear(), d.getMonth(), d.getDate(),
|
||||
value.hour, value.minute).getTime();
|
||||
const dur: number = this.endMs - this.startMs;
|
||||
this.startMs = newStart;
|
||||
this.endMs = newStart + dur;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private pickEndDate(): void {
|
||||
const cur = new Date(this.endMs);
|
||||
DatePickerDialog.show({
|
||||
start: new Date(2000, 0, 1),
|
||||
end: new Date(2050, 11, 31),
|
||||
selected: cur,
|
||||
onDateAccept: (value: Date) => {
|
||||
const keep = new Date(this.endMs);
|
||||
this.endMs = new Date(value.getFullYear(), value.getMonth(), value.getDate(),
|
||||
keep.getHours(), keep.getMinutes()).getTime();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private pickEndTime(): void {
|
||||
const cur = new Date(this.endMs);
|
||||
TimePickerDialog.show({
|
||||
selected: cur,
|
||||
onAccept: (value: TimePickerResult) => {
|
||||
const d = new Date(this.endMs);
|
||||
this.endMs = new Date(d.getFullYear(), d.getMonth(), d.getDate(),
|
||||
value.hour, value.minute).getTime();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private toggleAllDay(): void {
|
||||
this.allDay = !this.allDay;
|
||||
if (this.allDay) {
|
||||
const s = new Date(this.startMs);
|
||||
const dayStart: number = new Date(s.getFullYear(), s.getMonth(), s.getDate()).getTime();
|
||||
this.startMs = dayStart;
|
||||
this.endMs = dayStart + 86399999;
|
||||
} else {
|
||||
const s = new Date(this.startMs);
|
||||
this.startMs = s.getTime() + 9 * 3600000;
|
||||
this.endMs = this.startMs + 3600000;
|
||||
}
|
||||
}
|
||||
|
||||
private validate(): boolean {
|
||||
if (this.title.trim() === '') {
|
||||
this.statusMsg = '请输入日程标题';
|
||||
return false;
|
||||
}
|
||||
if (this.endMs < this.startMs) {
|
||||
this.statusMsg = '结束时间不能早于开始时间';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async save(): Promise<void> {
|
||||
if (this.isSaving || !this.validate()) {
|
||||
return;
|
||||
}
|
||||
if (this.event !== null && this.event.recurring) {
|
||||
this.statusMsg = '重复日程(系列中的一天)暂不支持修改,请到服务器端调整重复规则';
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
this.statusMsg = '';
|
||||
try {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
const book = this.chosenBook();
|
||||
const e = this.event ?? new LocalEvent();
|
||||
const isNew: boolean = this.event === null;
|
||||
e.title = this.title.trim();
|
||||
e.location = this.location.trim();
|
||||
e.description = this.description.trim();
|
||||
e.startTime = this.startMs;
|
||||
e.endTime = this.allDay ? this.startMs + 86399999 : this.endMs;
|
||||
e.isAllDay = this.allDay;
|
||||
e.calKey = book !== null ? book.calKey : 'local';
|
||||
e.href = book !== null ? book.href : '';
|
||||
if (isNew) {
|
||||
e.uid = `syncal-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
|
||||
e.remotePath = encodeURIComponent(e.uid) + '.ics';
|
||||
await EventDb.insertLocal(context, e);
|
||||
} else {
|
||||
await EventDb.updateLocal(context, e);
|
||||
}
|
||||
// 立即推送(尽力而为,失败不打断,下次同步会再推)
|
||||
if (e.href !== '') {
|
||||
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
||||
const acc = accounts.find((a: DavAccount): boolean => a.calendarHrefs.includes(e.href));
|
||||
if (acc !== undefined) {
|
||||
const auth: string = DavClient.authHeader(acc.username, acc.password);
|
||||
await SyncEngine.pushDirtyForAccount(context, acc, auth);
|
||||
}
|
||||
} else {
|
||||
await SyncEngine.settleLocalEvents(context);
|
||||
}
|
||||
this.getUIContext().getPromptAction().showToast({ message: '日程已保存' });
|
||||
router.back();
|
||||
} catch (err) {
|
||||
const ex = err as BusinessError;
|
||||
console.error(`保存日程失败: ${ex.message}`);
|
||||
this.statusMsg = `保存失败:${ex.message}(已保存到本地,稍后同步会重试)`;
|
||||
// 数据仍在本地且带 dirty 标记,不会丢
|
||||
router.back();
|
||||
}
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
private async removeEvent(): Promise<void> {
|
||||
if (this.event === null || this.isSaving) {
|
||||
return;
|
||||
}
|
||||
if (this.event.recurring) {
|
||||
this.statusMsg = '重复日程(系列中的一天)暂不支持删除,请到服务器端调整重复规则';
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const context = this.getUIContext().getHostContext();
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
await EventDb.markDeleted(context, this.event.id);
|
||||
if (this.event.href !== '') {
|
||||
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
||||
const acc = accounts.find((a: DavAccount): boolean => a.calendarHrefs.includes(this.event?.href ?? ''));
|
||||
if (acc !== undefined) {
|
||||
const auth: string = DavClient.authHeader(acc.username, acc.password);
|
||||
await SyncEngine.pushDirtyForAccount(context, acc, auth);
|
||||
}
|
||||
} else {
|
||||
await SyncEngine.settleLocalEvents(context);
|
||||
}
|
||||
this.getUIContext().getPromptAction().showToast({ message: '日程已删除' });
|
||||
router.back();
|
||||
} catch (err) {
|
||||
const ex = err as BusinessError;
|
||||
this.statusMsg = `删除失败:${ex.message}`;
|
||||
}
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 14 }) {
|
||||
// 顶部
|
||||
Row({ space: 6 }) {
|
||||
Text('取消')
|
||||
.fontSize(16)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
router.back();
|
||||
})
|
||||
Blank()
|
||||
Text(this.isExisting ? '编辑日程' : '新建日程')
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Blank()
|
||||
Text('保存')
|
||||
.fontSize(16)
|
||||
.fontColor(this.isSaving ? $r('app.color.text_hint') : $r('app.color.brand'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.onClick(() => {
|
||||
this.save();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 14 }) {
|
||||
// 标题
|
||||
TextInput({ text: this.title, placeholder: '标题' })
|
||||
.height(46)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.borderRadius(12)
|
||||
.onChange((v: string) => {
|
||||
this.title = v;
|
||||
})
|
||||
|
||||
// 全天
|
||||
Row() {
|
||||
Text('全天')
|
||||
.fontSize(15)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Blank()
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.allDay })
|
||||
.selectedColor($r('app.color.brand'))
|
||||
.onChange(() => {
|
||||
this.toggleAllDay();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
|
||||
// 时间卡片
|
||||
Column({ space: 10 }) {
|
||||
this.timeRow('开始', true)
|
||||
Divider().color($r('app.color.shadow_color'))
|
||||
this.timeRow('结束', false)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(6)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
|
||||
// 日历本选择
|
||||
Column({ space: 8 }) {
|
||||
Text('日历本')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
||||
ForEach(this.books, (b: BookChoice) => {
|
||||
Row({ space: 5 }) {
|
||||
Circle().width(8).height(8).fill(b.color)
|
||||
Text(b.name)
|
||||
.fontSize(12)
|
||||
.fontColor(this.chosenKey === b.calKey
|
||||
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
||||
}
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.borderRadius(14)
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
.backgroundColor(this.chosenKey === b.calKey ? b.color : $r('app.color.chip_off_bg'))
|
||||
.onClick(() => {
|
||||
this.chosenKey = b.calKey;
|
||||
})
|
||||
}, (b: BookChoice) => b.calKey)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
|
||||
// 地点
|
||||
TextInput({ text: this.location, placeholder: '地点(可选)' })
|
||||
.height(44)
|
||||
.fontSize(14)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.borderRadius(12)
|
||||
.onChange((v: string) => {
|
||||
this.location = v;
|
||||
})
|
||||
|
||||
// 描述
|
||||
TextArea({ text: this.description, placeholder: '备注(可选)' })
|
||||
.height(90)
|
||||
.fontSize(14)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.borderRadius(12)
|
||||
.onChange((v: string) => {
|
||||
this.description = v;
|
||||
})
|
||||
|
||||
if (this.statusMsg !== '') {
|
||||
Text(this.statusMsg)
|
||||
.fontSize(13)
|
||||
.fontColor($r('app.color.error'))
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
// 删除
|
||||
if (this.isExisting) {
|
||||
Button('删除日程')
|
||||
.fontSize(15)
|
||||
.fontColor($r('app.color.error'))
|
||||
.backgroundColor($r('app.color.error_bg'))
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(12)
|
||||
.enabled(!this.isSaving)
|
||||
.onClick(() => {
|
||||
this.removeEvent();
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 20, bottom: 30 })
|
||||
.constraintSize({ minHeight: '100%' })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.Top)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ top: 12 })
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
|
||||
@Builder
|
||||
timeRow(label: string, isStart: boolean) {
|
||||
Row({ space: 8 }) {
|
||||
Text(label)
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width(36)
|
||||
Text(this.fmtDate(isStart ? this.startMs : this.endMs))
|
||||
.fontSize(15)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
if (isStart) {
|
||||
this.pickStartDate();
|
||||
} else {
|
||||
this.pickEndDate();
|
||||
}
|
||||
})
|
||||
if (!this.allDay) {
|
||||
Text(this.fmtTime(isStart ? this.startMs : this.endMs))
|
||||
.fontSize(15)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
if (isStart) {
|
||||
this.pickStartTime();
|
||||
} else {
|
||||
this.pickEndTime();
|
||||
}
|
||||
})
|
||||
}
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 8, bottom: 8 })
|
||||
}
|
||||
}
|
||||
|
||||
/** 桥接:从账号存储拿可写来源(DAV 日历本 + 本机),附上 href */
|
||||
class CalendarDataBridge {
|
||||
static async loadWritableSources(context: common.Context): Promise<CalSourceWithHref[]> {
|
||||
const result: CalSourceWithHref[] = [];
|
||||
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
||||
for (const acc of accounts) {
|
||||
for (let i = 0; i < acc.calendarHrefs.length; i++) {
|
||||
const s = new CalSourceWithHref();
|
||||
s.calKey = `${acc.id}_${i}`;
|
||||
let name: string = i < acc.calendarNames.length ? acc.calendarNames[i] : '';
|
||||
if (name === '') {
|
||||
name = acc.calendarHrefs.length === 1 ? acc.name : `日历本 ${i + 1}`;
|
||||
}
|
||||
s.name = `${acc.name} · ${name}`;
|
||||
let color: string = i < acc.calendarColors.length ? AccountStore.normalizeColor(acc.calendarColors[i]) : '';
|
||||
s.color = color !== '' ? color : BookPalette.colorFor(i);
|
||||
s.href = acc.calendarHrefs[i];
|
||||
result.push(s);
|
||||
}
|
||||
}
|
||||
const local = new CalSourceWithHref();
|
||||
local.calKey = 'local';
|
||||
local.name = '本机(不同步)';
|
||||
local.color = '#5A6068';
|
||||
result.push(local);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class CalSourceWithHref extends CalSource {
|
||||
href: string = '';
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
// entry/src/main/ets/pages/SettingsPage.ets
|
||||
// 设置页:系统日历混合显示开关 + 自动同步间隔
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { AppSettings } from '../common/AppSettings';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
|
||||
const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60];
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct SettingsPage {
|
||||
@State showSystem: boolean = true;
|
||||
@State intervalMinutes: number = 1;
|
||||
private context?: common.Context;
|
||||
|
||||
aboutToAppear(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
this.context = ctx;
|
||||
LogUtil.init(ctx);
|
||||
AppSettings.getShowSystemCalendar(ctx).then((v: boolean): void => {
|
||||
this.showSystem = v;
|
||||
});
|
||||
AppSettings.getSyncIntervalMinutes(ctx).then((v: number): void => {
|
||||
this.intervalMinutes = v;
|
||||
});
|
||||
}
|
||||
|
||||
private async saveShowSystem(value: boolean): Promise<void> {
|
||||
if (this.context === undefined) {
|
||||
return;
|
||||
}
|
||||
await AppSettings.setShowSystemCalendar(this.context, value);
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: value ? '已开启系统日历混合显示,返回首页生效' : '已关闭系统日历混合显示,返回首页生效' });
|
||||
}
|
||||
|
||||
private async saveInterval(minutes: number): Promise<void> {
|
||||
if (this.context === undefined) {
|
||||
return;
|
||||
}
|
||||
await AppSettings.setSyncIntervalMinutes(this.context, minutes);
|
||||
AppStorage.setOrCreate('syncIntervalMinutes', minutes);
|
||||
this.getUIContext().getPromptAction()
|
||||
.showToast({ message: `自动同步间隔已设为 ${minutes} 分钟` });
|
||||
}
|
||||
|
||||
private intervalLabel(minutes: number): string {
|
||||
return minutes >= 60 ? `${minutes / 60} 小时` : `${minutes} 分钟`;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 顶部
|
||||
Row({ space: 10 }) {
|
||||
Text('←')
|
||||
.fontSize(20)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
router.back();
|
||||
})
|
||||
Text('设置')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 20, top: 12, bottom: 8 })
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
// 系统日历混合显示
|
||||
Row({ space: 10 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('混合显示系统日历')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text('关闭后只显示 DAV 账号的日程')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.showSystem })
|
||||
.selectedColor($r('app.color.brand'))
|
||||
.onChange((isOn: boolean) => {
|
||||
if (isOn !== this.showSystem) {
|
||||
this.showSystem = isOn;
|
||||
this.saveShowSystem(isOn);
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.border({ width: 1, color: $r('app.color.shadow_color') })
|
||||
|
||||
// 自动同步间隔
|
||||
Row({ space: 10 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('自动同步间隔')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text('应用打开期间按此间隔自动同步')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
Select([{ value: '1 分钟' }, { value: '5 分钟' }, { value: '15 分钟' },
|
||||
{ value: '30 分钟' }, { value: '1 小时' }] as SelectOption[])
|
||||
.selected(INTERVAL_OPTIONS.indexOf(this.intervalMinutes))
|
||||
.value(this.intervalLabel(this.intervalMinutes))
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.font({ size: 14 })
|
||||
.optionFont({ size: 14 })
|
||||
.selectedOptionFont({ size: 14 })
|
||||
.onSelect((index: number) => {
|
||||
if (index >= 0 && index < INTERVAL_OPTIONS.length) {
|
||||
this.intervalMinutes = INTERVAL_OPTIONS[index];
|
||||
this.saveInterval(INTERVAL_OPTIONS[index]);
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.border({ width: 1, color: $r('app.color.shadow_color') })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 20, top: 8, bottom: 20 })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.align(Alignment.Top)
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// entry/src/main/ets/pages/widget/Widget2x2.ets
|
||||
// 2x2 服务卡片:日期 + 农历 + 下一条日程
|
||||
let storage2x2 = new LocalStorage();
|
||||
|
||||
class CardItem2x2 {
|
||||
title: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
date: string = '';
|
||||
showDate: boolean = false;
|
||||
calName: string = '';
|
||||
color: string = '#007DFF';
|
||||
}
|
||||
|
||||
@Entry(storage2x2)
|
||||
@Component
|
||||
struct Widget2x2Card {
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('dateText') dateText: string = '';
|
||||
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||
|
||||
private parseItems(): CardItem2x2[] {
|
||||
try {
|
||||
return JSON.parse(this.eventsJson) as CardItem2x2[];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(this.dateText)
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(1)
|
||||
Blank()
|
||||
Text(this.lunarText)
|
||||
.fontSize(11)
|
||||
.fontColor('#8A8A8A')
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Divider()
|
||||
.strokeWidth(0.5)
|
||||
.color('#E5E5E5')
|
||||
|
||||
if (this.parseItems().length === 0) {
|
||||
Column({ space: 4 }) {
|
||||
Text('暂无日程')
|
||||
.fontSize(13)
|
||||
.fontColor('#8A8A8A')
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Circle().width(6).height(6).fill(this.parseItems()[0].color)
|
||||
Text(this.parseItems()[0].title)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (this.parseItems()[0].calName !== '') {
|
||||
Text(this.parseItems()[0].calName)
|
||||
.fontSize(9)
|
||||
.fontColor(this.parseItems()[0].color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '30%' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Blank()
|
||||
Row({ space: 6 }) {
|
||||
Text(this.parseItems()[0].date)
|
||||
.fontSize(10)
|
||||
.fontColor('#8A8A8A')
|
||||
Text(this.parseItems()[0].time === '全天'
|
||||
? '全天'
|
||||
: `${this.parseItems()[0].time} - ${this.parseItems()[0].endTime}`)
|
||||
.fontSize(11)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#007DFF')
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.onClick(() => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: {}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// entry/src/main/ets/pages/widget/Widget4x2.ets
|
||||
// 2x4 服务卡片:日期 + 农历 + 未来几条日程(时间轴样式,按天分组)
|
||||
let storage2x4 = new LocalStorage();
|
||||
|
||||
class CardItem2x4 {
|
||||
title: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
date: string = '';
|
||||
showDate: boolean = false;
|
||||
calName: string = '';
|
||||
color: string = '#007DFF';
|
||||
}
|
||||
|
||||
@Entry(storage2x4)
|
||||
@Component
|
||||
struct Widget4x2Card {
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('dateText') dateText: string = '';
|
||||
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||
|
||||
private parseItems(): CardItem2x4[] {
|
||||
try {
|
||||
const all: CardItem2x4[] = JSON.parse(this.eventsJson) as CardItem2x4[];
|
||||
return all.slice(0, 4);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 全天事件行:圆角矩形 + 左侧日历色竖条 + 标题 + “全天”标记(无时间轴) */
|
||||
@Builder
|
||||
buildAllDayRow(item: CardItem2x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(16)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
Text('全天')
|
||||
.fontSize(9)
|
||||
.fontColor('#FFFFFF')
|
||||
.backgroundColor(item.color)
|
||||
.borderRadius(6)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
/** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */
|
||||
@Builder
|
||||
buildTimedRow(item: CardItem2x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(36)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
// 时间列:开始时间在上、结束时间在下、中间竖线连接
|
||||
Column({ space: 2 }) {
|
||||
Text(item.time)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#333333')
|
||||
Column()
|
||||
.width(1.5)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor('#D8D8D8')
|
||||
.borderRadius(1)
|
||||
Text(item.endTime)
|
||||
.fontSize(10)
|
||||
.fontColor('#999999')
|
||||
}
|
||||
.width(38)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.height(36)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(this.dateText)
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(1)
|
||||
Text(this.lunarText)
|
||||
.fontSize(11)
|
||||
.fontColor('#8A8A8A')
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Divider().strokeWidth(0.5).color('#E5E5E5')
|
||||
|
||||
if (this.parseItems().length === 0) {
|
||||
Column() {
|
||||
Text('暂无日程')
|
||||
.fontSize(13)
|
||||
.fontColor('#8A8A8A')
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 4 }) {
|
||||
ForEach(this.parseItems(), (item: CardItem2x4, idx: number) => {
|
||||
ListItem() {
|
||||
Column({ space: 3 }) {
|
||||
if (item.showDate) {
|
||||
Text(item.date)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#666666')
|
||||
.width('100%')
|
||||
}
|
||||
if (item.time === '全天') {
|
||||
this.buildAllDayRow(item)
|
||||
} else {
|
||||
this.buildTimedRow(item)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}, (item: CardItem2x4, idx: number) => `${idx}_${item.title}_${item.time}`)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.cachedCount(4)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.onClick(() => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: {}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// entry/src/main/ets/pages/widget/Widget4x4.ets
|
||||
// 4x4 服务卡片:日期 + 农历 + 从今天开始的日程(时间轴样式,按天分组,可滑动)
|
||||
let storage4x4 = new LocalStorage();
|
||||
|
||||
class CardItem4x4 {
|
||||
title: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
date: string = '';
|
||||
showDate: boolean = false;
|
||||
calName: string = '';
|
||||
color: string = '#007DFF';
|
||||
}
|
||||
|
||||
@Entry(storage4x4)
|
||||
@Component
|
||||
struct Widget4x4Card {
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('dateText') dateText: string = '';
|
||||
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||
|
||||
private parseItems(): CardItem4x4[] {
|
||||
try {
|
||||
return JSON.parse(this.eventsJson) as CardItem4x4[];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 全天事件行:圆角矩形 + 左侧日历色竖条 + 标题 + “全天”标记(无时间轴) */
|
||||
@Builder
|
||||
buildAllDayRow(item: CardItem4x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(16)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
Text('全天')
|
||||
.fontSize(9)
|
||||
.fontColor('#FFFFFF')
|
||||
.backgroundColor(item.color)
|
||||
.borderRadius(6)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
/** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */
|
||||
@Builder
|
||||
buildTimedRow(item: CardItem4x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(38)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
// 时间列:开始时间在上、结束时间在下、中间竖线连接
|
||||
Column({ space: 2 }) {
|
||||
Text(item.time)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#333333')
|
||||
Column()
|
||||
.width(1.5)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor('#D8D8D8')
|
||||
.borderRadius(1)
|
||||
Text(item.endTime)
|
||||
.fontSize(10)
|
||||
.fontColor('#999999')
|
||||
}
|
||||
.width(38)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.height(38)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(this.dateText)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1A1A1A')
|
||||
Text(this.lunarText)
|
||||
.fontSize(12)
|
||||
.fontColor('#8A8A8A')
|
||||
Blank()
|
||||
Text('同步日历')
|
||||
.fontSize(10)
|
||||
.fontColor('#B0B0B0')
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Divider().strokeWidth(0.5).color('#E5E5E5')
|
||||
|
||||
if (this.parseItems().length === 0) {
|
||||
Column({ space: 6 }) {
|
||||
Text('📅')
|
||||
.fontSize(24)
|
||||
Text('暂无日程')
|
||||
.fontSize(13)
|
||||
.fontColor('#8A8A8A')
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 4 }) {
|
||||
ForEach(this.parseItems(), (item: CardItem4x4, idx: number) => {
|
||||
ListItem() {
|
||||
Column({ space: 3 }) {
|
||||
if (item.showDate) {
|
||||
Text(item.date)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#666666')
|
||||
.width('100%')
|
||||
}
|
||||
if (item.time === '全天') {
|
||||
this.buildAllDayRow(item)
|
||||
} else {
|
||||
this.buildTimedRow(item)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}, (item: CardItem4x4, idx: number) => `${idx}_${item.title}_${item.time}`)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Auto)
|
||||
.cachedCount(8)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(14)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.onClick(() => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: {}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// entry/src/main/ets/pages/widget/Widget6x4.ets
|
||||
// 6x4 服务卡片:日期 + 农历 + 从今天开始的日程(时间轴样式,比 4x4 显示更多)
|
||||
let storage6x4 = new LocalStorage();
|
||||
|
||||
class CardItem6x4 {
|
||||
title: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
date: string = '';
|
||||
showDate: boolean = false;
|
||||
calName: string = '';
|
||||
color: string = '#007DFF';
|
||||
}
|
||||
|
||||
@Entry(storage6x4)
|
||||
@Component
|
||||
struct Widget6x4Card {
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('dateText') dateText: string = '';
|
||||
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||
|
||||
private parseItems(): CardItem6x4[] {
|
||||
try {
|
||||
return JSON.parse(this.eventsJson) as CardItem6x4[];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 全天事件行:圆角矩形 + 左侧日历色竖条 + 标题 + “全天”标记(无时间轴) */
|
||||
@Builder
|
||||
buildAllDayRow(item: CardItem6x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(16)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
Text('全天')
|
||||
.fontSize(9)
|
||||
.fontColor('#FFFFFF')
|
||||
.backgroundColor(item.color)
|
||||
.borderRadius(6)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
/** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */
|
||||
@Builder
|
||||
buildTimedRow(item: CardItem6x4) {
|
||||
Row({ space: 8 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(38)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(item.color)
|
||||
// 时间列:开始时间在上、结束时间在下、中间竖线连接
|
||||
Column({ space: 2 }) {
|
||||
Text(item.time)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#333333')
|
||||
Column()
|
||||
.width(1.5)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor('#D8D8D8')
|
||||
.borderRadius(1)
|
||||
Text(item.endTime)
|
||||
.fontSize(10)
|
||||
.fontColor('#999999')
|
||||
}
|
||||
.width(38)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.height(38)
|
||||
Text(item.title)
|
||||
.fontSize(12)
|
||||
.fontColor('#1A1A1A')
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (item.calName !== '') {
|
||||
Text(item.calName)
|
||||
.fontSize(9)
|
||||
.fontColor(item.color)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '25%' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#F5F7FA')
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(this.dateText)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1A1A1A')
|
||||
Text(this.lunarText)
|
||||
.fontSize(12)
|
||||
.fontColor('#8A8A8A')
|
||||
Blank()
|
||||
Text('同步日历')
|
||||
.fontSize(10)
|
||||
.fontColor('#B0B0B0')
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Divider().strokeWidth(0.5).color('#E5E5E5')
|
||||
|
||||
if (this.parseItems().length === 0) {
|
||||
Column({ space: 6 }) {
|
||||
Text('📅')
|
||||
.fontSize(24)
|
||||
Text('暂无日程')
|
||||
.fontSize(13)
|
||||
.fontColor('#8A8A8A')
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 4 }) {
|
||||
ForEach(this.parseItems(), (item: CardItem6x4, idx: number) => {
|
||||
ListItem() {
|
||||
Column({ space: 3 }) {
|
||||
if (item.showDate) {
|
||||
Text(item.date)
|
||||
.fontSize(10)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#666666')
|
||||
.width('100%')
|
||||
}
|
||||
if (item.time === '全天') {
|
||||
this.buildAllDayRow(item)
|
||||
} else {
|
||||
this.buildTimedRow(item)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}, (item: CardItem6x4, idx: number) => `${idx}_${item.title}_${item.time}`)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Auto)
|
||||
.cachedCount(12)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(14)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.onClick(() => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: {}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user