refactor(待办): 待办编辑改为独立页面,与新建日程一致 (#47)

## 用户诉求
「做成跟新建日程一样,就是打开一个新的页面。左上角取消,右边保存,
所有信息拉下来。」

之前是 Index 里的底部弹层(todoEditorOverlay),即使加了内部 Scroll,
形态仍与日程页不一致。

## 改动
### 新增 pages/TodoEditPage.ets
完全对齐 EventEditPage 的结构:
- 固定顶部栏:`取消`(左)· `待办标题`(中)· `保存`(右)
- 中间整页 Scroll 表单:标题/日历本/截止时间/重复/优先级/提醒/标签/地点/
  父任务/附件/备注
- 编辑态底部「删除待办」,与日程页一致

### 传参方式
与日程页一致用 AppStorage:
  `pendingTodoId`(0=新建,>0=编辑)
  `pendingTodoDate`(新建时预填截止日)

### Index.ets 改动
- `openTodoCreator` / `openTodoEditor` 改为 `router.pushUrl` 跳转
- 删除死代码 104 行(旧弹层的两处入口逻辑体)
- 注册路由 `pages/TodoEditPage`

### 注意点
`CalSource` 没有 `href` 字段,需从账号按 `calKey` 反解;
且要按 `comps` 过滤掉纯日历本(写不进 VTODO)。

## 编译
BUILD SUCCESSFUL

## 真机验证(5NC0225B27004718)
| 验证项 | 实测 |
|---|---|
| 独立页面 | 日志 `pages/TodoEditPage finish inPage transition` ✅ |
| 顶部栏布局 | 取消 x=50、标题 x=427(居中)、保存 x=906 ✅ |
| 表单字段 | 日历本4个/截止/重复/优先级/提醒 全部渲染 |
| 滚动 | 上滑后 附件 y=1668、添加文件 y=1668、备注 y=1760 全部出现 ✅ |
| 顶部栏固定 | 滚动前后 取消/标题/保存 的 y 完全不变(122/143/122)✅ |
This commit is contained in:
2026-11-04 19:26:11 +08:00
parent 22d67e8a9d
commit fe7945d300
3 changed files with 868 additions and 96 deletions
+10 -96
View File
@@ -3616,105 +3616,19 @@ struct Index {
* 找不到就选第一个可写的本 —— 让用户少点一步,但仍可改。 * 找不到就选第一个可写的本 —— 让用户少点一步,但仍可改。
*/ */
private async openTodoCreator(): Promise<void> { private async openTodoCreator(): Promise<void> {
const ctx = this.getUIContext().getHostContext(); // ⭐ 改为独立页面(与新建日程一致):预置参数后跳转
if (ctx === undefined) { AppStorage.setOrCreate<number>('pendingTodoId', 0);
return; AppStorage.setOrCreate<number>('pendingTodoDate', this.selectedDate);
} router.pushUrl({ url: 'pages/TodoEditPage' });
// 只取可写的 CalDAV 本('dav' 来源);只读本推上去必 403,不该出现在选项里
// 注意:CalSource 没有 href 字段,需从账号里按 calKey 反解
const accounts: DavAccount[] = await AccountStore.loadAll(ctx);
this.todoBookHrefs.clear();
const writable: CalSource[] = [];
for (const s of this.sources) {
if (!s.writable || s.source !== 'dav') {
continue;
}
// ⭐ 只列**能放待办**的本:纯日历本(COMP_SET 只有 VEVENT)写不进 VTODO。
// 'both' / 'unknown' 都保留 —— unknown 是服务器未声明或旧数据,不能武断排除。
if (s.comps === 'event') {
continue;
}
// calKey 形如 "<accountId>_<index>"
const us: number = s.calKey.lastIndexOf('_');
if (us <= 0) {
continue;
}
const accId: string = s.calKey.substring(0, us);
const idx: number = parseInt(s.calKey.substring(us + 1), 10);
if (isNaN(idx)) {
continue;
}
const acc: DavAccount | undefined = accounts.find(
(a: DavAccount): boolean => String(a.id) === accId);
if (acc === undefined || idx < 0 || idx >= acc.calendarHrefs.length) {
continue;
}
this.todoBookHrefs.set(s.calKey, acc.calendarHrefs[idx]);
writable.push(s);
}
if (writable.length === 0) {
this.getUIContext().getPromptAction().showToast({
message: '还没有可写入的 CalDAV 日历本,请先添加账号'
});
return;
}
this.todoBooks = writable;
// 默认选中:名字里含 inbox/收件箱 的本优先,否则第一个
let pick: string = writable[0].calKey;
for (const s of writable) {
const n: string = s.name.toLowerCase();
if (n.indexOf('inbox') >= 0 || s.name.indexOf('收件箱') >= 0) {
pick = s.calKey;
break;
}
}
this.todoCreateBookKey = pick;
// 清空编辑态数据,进入"新建"
this.todoCreating = true;
this.todoEditId = 0;
this.todoEditTitle = '';
this.todoEditDesc = '';
this.todoEditDueMs = 0;
this.todoEditHasDue = false;
this.todoEditWritable = true;
this.todoEditBook = '';
this.todoEditRecur = '';
this.todoEditPriority = 0;
this.todoEditCategories = '';
this.todoEditLocation = '';
this.todoEditReminders = [];
this.todoEditParentUid = '';
this.todoEditAttachments = '';
this.todoEditPickShow = false;
this.todoEditShow = true;
} }
/** 打开待办编辑弹框(点内容区触发;不会改变完成状态) */ /** 打开待办编辑页(点内容区触发;不会改变完成状态) */
private openTodoEditor(t: DisplayEvent): void { private openTodoEditor(t: DisplayEvent): void {
this.todoCreating = false; // ⭐ 改为独立页面(与编辑日程一致):把 id 预置后跳转
this.todoEditId = t.id; AppStorage.setOrCreate<number>('pendingTodoId', t.id);
this.todoEditTitle = t.title; AppStorage.setOrCreate<number>('pendingTodoDate', 0);
this.todoEditDesc = t.description; router.pushUrl({ url: 'pages/TodoEditPage' });
this.todoEditDueMs = t.startTime > 0 ? t.startTime : 0; return;
this.todoEditHasDue = t.startTime > 0;
this.todoEditBook = t.calName;
this.todoEditWritable = t.writable;
this.todoEditRecur = t.recurRule !== '' ? IcsUtil.freqOfRrule(t.recurRule) : '';
// 优先级 / 标签 / 地点(DisplayEvent 已带这些字段)
this.todoEditPriority = t.priority > 0 ? t.priority : 0;
this.todoEditCategories = t.categories;
this.todoEditLocation = t.location;
this.todoEditReminders = t.reminders.slice();
// 子任务与附件
this.todoEditParentUid = t.parentUid;
this.todoEditAttachments = t.attachments;
// 默认值给"明天 09:00",与新建日程的口径一致(非今天用 9 点)
const base: Date = new Date();
this.todoEditPickDate = new Date(base.getFullYear(), base.getMonth(), base.getDate() + 1);
this.todoEditPickHour = 9;
this.todoEditPickMin = 0;
this.todoEditPickShow = false;
this.todoEditShow = true;
} }
/** 打开截止时间选择器(把当前值灌进滚轮初值) */ /** 打开截止时间选择器(把当前值灌进滚轮初值) */
+857
View File
@@ -0,0 +1,857 @@
// entry/src/main/ets/pages/TodoEditPage.ets
// 待办编辑页:新建 / 修改 / 删除待办(VTODO)
//
// ⭐ 为什么做成独立页面而不是弹层(用户明确要求):
// 原来待办编辑是 Index 里的一个底部弹层(todoEditorOverlay)。字段越加越多
// (日历本/标题/截止/循环/优先级/提醒/标签/地点/父任务/附件/备注),
// 弹层空间不够、要自己处理滚动和高度约束,与新建日程页(EventEditPage)
// 的交互形态也不一致 —— 用户要求「跟新建日程一样」。
// 所以整体迁移成独立页面:顶部「取消 · 标题 · 保存」,中间一整页可滚动表单。
//
// 传参:与 EventEditPage 一致,用 AppStorage 预置,页面自己读。
// 新建:pendingTodoId = 0,pendingTodoDate = 预填截止日(毫秒,0 = 不预填)
// 编辑:pendingTodoId = 本地库 id
import { router, KeyboardAvoidMode } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { AccountStore, CalSource, DavAccount } from '../common/AccountStore';
import { EventDb, LocalEvent } from '../common/EventDb';
import { TodoService } from '../common/TodoService';
import { CalendarDataService, DisplayEvent } from '../common/CalendarDataService';
import { CardDataService } from '../common/CardDataService';
import { ReminderService } from '../common/ReminderService';
import { LogUtil } from '../common/LogUtil';
import { IcsUtil, AttachMeta } from '../common/IcsUtil';
/** 提醒选项(分钟) */
class ReminderOpt {
v: number = 0;
label: string = '';
}
/** 优先级选项 */
class PriorityOpt {
v: number = 0;
label: string = '';
color: string = '';
}
/** 循环选项 */
class RecurOpt {
v: string = '';
label: string = '';
}
@Entry
@Component
struct TodoEditPage {
// ---------- 基础 ----------
@State loading: boolean = true;
@State busy: boolean = false;
@State creating: boolean = true; // true=新建 false=编辑
@State editId: number = 0;
@State writable: boolean = true;
@State books: CalSource[] = [];
@State bookKey: string = ''; // 新建时选中的本
/** calKey → 集合 href(CalSource 没有 href 字段,需从账号里按 calKey 反解) */
private bookHrefs: Map<string, string> = new Map<string, string>();
@State bookName: string = ''; // 编辑时只读显示的本名
// ---------- 表单字段 ----------
@State title: string = '';
@State desc: string = '';
@State hasDue: boolean = false;
@State dueMs: number = 0;
@State recur: string = ''; // '' = 不循环
@State priority: number = 0;
@State categories: string = '';
@State location: string = '';
@State reminders: number[] = [];
@State parentUid: string = '';
@State attachments: string = ''; // 附件 JSON
// ---------- 截止时间选择器 ----------
@State pickShow: boolean = false;
@State pickDate: Date = new Date();
@State pickHour: number = 9;
@State pickMin: number = 0;
// ---------- 父任务选择 ----------
@State parentPickShow: boolean = false;
@State allTodos: DisplayEvent[] = []; // 用于父任务候选 + 显示名
// ---------- 附件 ----------
@State attachBusy: boolean = false;
private formScroller: Scroller = new Scroller();
// 选项表
private reminderOpts: ReminderOpt[] = [
{ v: 0, label: '不提醒' },
{ v: 5, label: '提前5分钟' },
{ v: 15, label: '提前15分钟' },
{ v: 60, label: '提前1小时' },
{ v: 180, label: '提前3小时' },
{ v: 1440, label: '提前1天' }
];
private priorityOpts: PriorityOpt[] = [
{ v: 0, label: '无', color: '#9AA0A6' },
{ v: 1, label: '最高', color: '#FF3B30' },
{ v: 5, label: '中', color: '#FF9500' },
{ v: 9, label: '低', color: '#34C759' }
];
private recurOpts: RecurOpt[] = [
{ v: '', label: '不循环' },
{ v: 'DAILY', label: '每天' },
{ v: 'WEEKLY', label: '每周' },
{ v: 'MONTHLY', label: '每月' }
];
/** 键盘避让(与日程页一致):RESIZE 模式,避免键盘遮挡底部字段 */
private setupKeyboardAvoid(): void {
try {
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
} catch (err) {
// 低版本不支持,忽略(只是少一层校正)
}
}
aboutToAppear(): void {
this.setupKeyboardAvoid();
this.initPage();
}
/** 读 AppStorage 参数并初始化表单 */
private async initPage(): Promise<void> {
const ctx = this.getUIContext().getHostContext();
if (ctx === undefined) {
this.loading = false;
return;
}
const pendingId: number = AppStorage.get<number>('pendingTodoId') ?? 0;
// 清理,避免下次误读
AppStorage.setOrCreate<number>('pendingTodoId', 0);
try {
// 可写日历本(新建时供选择)
// 只取可写 + dav 来源 + 能放待办的本(comps 不是 event)
const sources: CalSource[] = await CalendarDataService.loadSources(ctx);
const accounts: DavAccount[] = await AccountStore.loadAll(ctx);
this.bookHrefs.clear();
const writable: CalSource[] = [];
for (const s of sources) {
if (!s.writable || s.source !== 'dav') {
continue;
}
// ⭐ 纯日历本(COMP_SET 只有 VEVENT)写不进 VTODO,排除。
// 'both' / 'unknown' 保留(unknown 是服务器未声明或旧数据,不能武断排除)。
if (s.comps === 'event') {
continue;
}
const us: number = s.calKey.lastIndexOf('_');
if (us <= 0) {
continue;
}
const accId: string = s.calKey.substring(0, us);
const idx: number = parseInt(s.calKey.substring(us + 1), 10);
if (isNaN(idx)) {
continue;
}
const acc: DavAccount | undefined = accounts.find(
(a: DavAccount): boolean => String(a.id) === accId);
if (acc === undefined || idx < 0 || idx >= acc.calendarHrefs.length) {
continue;
}
this.bookHrefs.set(s.calKey, acc.calendarHrefs[idx]);
writable.push(s);
}
this.books = writable;
// 全部待办(父任务候选 + 名称显示)
this.allTodos = await CalendarDataService.loadTodos(ctx, sources);
if (pendingId > 0) {
this.creating = false;
this.editId = pendingId;
await this.fillFromRow(ctx, pendingId);
} else {
this.creating = true;
// 默认选第一个可写本
if (this.books.length > 0) {
this.bookKey = this.books[0].calKey;
}
// 预填截止日(从月视图某天点进来时带过来)
const preDate: number = AppStorage.get<number>('pendingTodoDate') ?? 0;
AppStorage.setOrCreate<number>('pendingTodoDate', 0);
if (preDate > 0) {
const d = new Date(preDate);
this.pickDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
this.hasDue = true;
this.dueMs = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 9, 0).getTime();
} else {
// 默认"明天 09:00",与新建日程口径一致
const base = new Date();
this.pickDate = new Date(base.getFullYear(), base.getMonth(), base.getDate() + 1);
}
}
} catch (err) {
LogUtil.write(`待办编辑页初始化失败: ${(err as BusinessError).message ?? ''}`);
} finally {
this.loading = false;
}
}
/** 从本地库读一条待办填充表单 */
private async fillFromRow(ctx: common.Context, id: number): Promise<void> {
const row: LocalEvent | null = await EventDb.queryById(ctx, id);
if (row === null) {
return;
}
this.title = row.title;
this.desc = row.description;
this.hasDue = row.startTime > 0;
this.dueMs = row.startTime > 0 ? row.startTime : 0;
this.bookName = this.bookNameOf(row.calKey);
this.writable = true;
this.recur = row.rrule !== '' ? IcsUtil.freqOfRrule(row.rrule) : '';
this.priority = row.priority > 0 ? row.priority : 0;
this.categories = row.categories;
this.location = row.location;
this.reminders = row.reminders.slice();
this.parentUid = row.parentUid;
this.attachments = row.attachments;
// 选择器初值
const d = new Date(row.startTime > 0 ? row.startTime : Date.now());
this.pickDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
this.pickHour = d.getHours();
this.pickMin = d.getMinutes();
}
private bookNameOf(calKey: string): string {
for (const b of this.books) {
if (b.calKey === calKey) {
return b.name;
}
}
return '';
}
// ---------- 保存 ----------
private async save(): Promise<void> {
const ctx = this.getUIContext().getHostContext();
if (ctx === undefined || this.busy) {
return;
}
const t: string = this.title.trim();
if (t === '') {
this.getUIContext().getPromptAction().showToast({ message: '请填写待办标题' });
return;
}
this.busy = true;
try {
const due: number = this.hasDue ? this.dueMs : 0;
const dateOnly: boolean = due > 0 && due % 86400000 === 0;
if (this.creating) {
const href: string = this.hrefOf(this.bookKey);
if (href === '') {
this.getUIContext().getPromptAction().showToast({ message: '请选择日历本' });
this.busy = false;
return;
}
const newId: number = await TodoService.createTodo(ctx, href, t, this.desc,
due, dateOnly, this.priority, this.categories, this.location,
this.reminders, this.parentUid, this.attachments);
if (newId > 0) {
this.getUIContext().getPromptAction().showToast({ message: '待办已创建' });
router.back();
return;
}
this.getUIContext().getPromptAction().showToast({
message: '创建失败:需联网,且该日历本需可写'
});
} else {
const ok: boolean = await TodoService.updateTodo(ctx, this.editId, t, this.desc,
due, this.priority, this.categories, this.location, this.reminders,
this.parentUid, this.attachments, true, true);
if (ok) {
// 循环规则单独写(与弹层时代一致:要先有截止时间才有意义)
const wantRule: string = this.recur !== '' ? `FREQ=${this.recur}` : '';
if (due > 0) {
await TodoService.setRecurrence(ctx, this.editId, wantRule, due);
}
this.getUIContext().getPromptAction().showToast({ message: '已保存' });
router.back();
return;
}
this.getUIContext().getPromptAction().showToast({
message: '保存失败:需联网,且该日历本需可写'
});
}
} catch (err) {
this.getUIContext().getPromptAction().showToast({ message: '保存失败,请稍后重试' });
} finally {
this.busy = false;
}
}
private hrefOf(calKey: string): string {
return this.bookHrefs.get(calKey) ?? '';
}
// ---------- 删除 ----------
private askDelete(): void {
this.getUIContext().showAlertDialog({
title: '删除待办',
message: '将从服务器删除这条待办,删除后无法撤销。',
autoCancel: true,
alignment: DialogAlignment.Center,
primaryButton: {
value: '取消',
action: (): void => {
}
},
secondaryButton: {
value: '删除',
action: (): void => {
this.doDelete();
}
}
});
}
private async doDelete(): Promise<void> {
const ctx = this.getUIContext().getHostContext();
if (ctx === undefined || this.busy) {
return;
}
this.busy = true;
try {
const ok: boolean = await TodoService.deleteTodo(ctx, this.editId);
if (ok) {
this.getUIContext().getPromptAction().showToast({ message: '已删除' });
router.back();
} else {
this.getUIContext().getPromptAction().showToast({ message: '删除失败,请稍后重试' });
}
} finally {
this.busy = false;
}
}
// ---------- 附件 ----------
private attachList(): AttachMeta[] {
return IcsUtil.jsonToAttachMeta(this.attachments);
}
private removeAttach(idx: number): void {
const list: AttachMeta[] = this.attachList();
if (idx < 0 || idx >= list.length) {
return;
}
list.splice(idx, 1);
this.attachments = IcsUtil.attachMetaToJson(list);
}
private async pickAttachment(ctx: common.Context): Promise<void> {
if (this.attachBusy) {
return;
}
this.attachBusy = true;
try {
const pw = new picker.DocumentViewPicker(ctx);
const uris: string[] = await pw.select({ maxSelectNumber: 3 });
if (uris.length === 0) {
return;
}
const existing: AttachMeta[] = this.attachList();
if (existing.length + uris.length > 3) {
this.getUIContext().getPromptAction().showToast({ message: '最多 3 个附件' });
return;
}
const merged: AttachMeta[] = existing.slice();
for (const uri of uris) {
const m: AttachMeta | null = this.readAttach(uri);
if (m !== null) {
merged.push(m);
}
}
this.attachments = IcsUtil.attachMetaToJson(merged);
} catch (err) {
// 用户取消也会走这里
} finally {
this.attachBusy = false;
}
}
private readAttach(uri: string): AttachMeta | null {
try {
const f = fs.openSync(uri, fs.OpenMode.READ_ONLY);
const st = fs.statSync(f.fd);
if (st.size <= 0 || st.size > 5 * 1024 * 1024) {
fs.closeSync(f.fd);
return null;
}
const buf: ArrayBuffer = new ArrayBuffer(st.size);
const n: number = fs.readSync(f.fd, buf);
fs.closeSync(f.fd);
if (n <= 0) {
return null;
}
const base64: string = new util.Base64Helper().encodeToStringSync(new Uint8Array(buf));
let name: string = uri.substring(uri.lastIndexOf('/') + 1);
const q: number = name.indexOf('?');
if (q >= 0) {
name = name.substring(0, q);
}
if (name === '') {
name = '附件';
}
const m = new AttachMeta();
m.name = name;
m.mime = this.mimeOf(name);
m.base64 = base64;
return m;
} catch (err) {
return null;
}
}
private mimeOf(name: string): string {
const l: string = name.toLowerCase();
if (l.endsWith('.pdf')) { return 'application/pdf'; }
if (l.endsWith('.png')) { return 'image/png'; }
if (l.endsWith('.jpg') || l.endsWith('.jpeg')) { return 'image/jpeg'; }
if (l.endsWith('.txt')) { return 'text/plain'; }
if (l.endsWith('.csv')) { return 'text/csv'; }
if (l.endsWith('.zip')) { return 'application/zip'; }
return 'application/octet-stream';
}
// ---------- 父任务 ----------
private currentUid(): string {
for (const t of this.allTodos) {
if (t.id === this.editId) {
return t.uid;
}
}
return '';
}
private parentCandidates(): DisplayEvent[] {
const self: string = this.currentUid();
const exclude: Set<string> = new Set<string>();
if (self !== '') {
exclude.add(self);
const q: string[] = [self];
let guard = 0;
while (q.length > 0 && guard < 200) {
const cur: string = q.shift() as string;
for (const t of this.allTodos) {
if (t.parentUid === cur && !exclude.has(t.uid)) {
exclude.add(t.uid);
q.push(t.uid);
}
}
guard++;
}
}
const out: DisplayEvent[] = [];
for (const t of this.allTodos) {
if (t.completed || t.parentUid !== '') {
continue;
}
if (exclude.has(t.uid)) {
continue;
}
out.push(t);
}
return out;
}
private parentName(): string {
if (this.parentUid === '') {
return '无';
}
for (const t of this.allTodos) {
if (t.uid === this.parentUid) {
return t.title === '' ? '(无标题)' : t.title;
}
}
return '(已不存在)';
}
// ---------- 显示 ----------
private fmtDue(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())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
private reminderActive(v: number): boolean {
if (v === 0) {
return this.reminders.length === 0;
}
return this.reminders.indexOf(v) >= 0;
}
private toggleReminder(v: number): void {
if (v === 0) {
this.reminders = [];
return;
}
const idx: number = this.reminders.indexOf(v);
if (idx >= 0) {
this.reminders = this.reminders.filter((n: number): boolean => n !== v);
} else {
this.reminders = this.reminders.concat([v]);
}
}
// ---------- UI ----------
build() {
Column({ space: 14 }) {
// ===== 固定顶部栏:取消 · 标题 · 保存 =====
Row({ space: 6 }) {
Text('取消')
.fontSize(16)
.fontColor($r('app.color.brand'))
.padding({ left: 4, right: 4, top: 8, bottom: 8 })
.onClick(() => {
router.back();
})
Blank()
Text(this.creating ? '新建待办' : '编辑待办')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Blank()
Text(this.busy ? '保存中…' : '保存')
.fontSize(16)
.fontColor(this.busy ? $r('app.color.text_hint') : $r('app.color.brand'))
.fontWeight(FontWeight.Medium)
.padding({ left: 4, right: 4, top: 8, bottom: 8 })
.onClick(() => {
this.save();
})
}
.width('100%')
.padding({ left: 16, right: 16 })
// ===== 中间表单:整页可滚动 =====
Scroll(this.formScroller) {
Column({ space: 14 }) {
// 标题
TextInput({ text: this.title, placeholder: '待办标题(必填)' })
.height(46)
.fontSize(16)
.backgroundColor($r('app.color.card_bg'))
.borderRadius(12)
.fontColor($r('app.color.text_primary'))
.placeholderColor($r('app.color.text_hint'))
.caretColor($r('app.color.brand'))
.onChange((v: string) => {
this.title = v;
})
// 日历本
if (this.creating) {
this.formLabel('日历本')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(this.books, (b: CalSource) => {
Row({ space: 6 }) {
Circle().width(8).height(8).fill(b.color)
Text(b.name)
.fontSize(13)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontColor(this.bookKey === b.calKey
? $r('app.color.button_text') : $r('app.color.text_primary'))
}
.width('48%')
.padding({ left: 10, right: 10, top: 9, bottom: 9 })
.borderRadius(14)
.margin({ bottom: 8 })
.backgroundColor(this.bookKey === b.calKey ? b.color : $r('app.color.chip_off_bg'))
.onClick(() => {
this.bookKey = b.calKey;
})
}, (b: CalSource) => `bk_${b.calKey}`)
}
.width('100%')
} else if (this.bookName !== '') {
this.formLabel('日历本')
Row({ space: 6 }) {
Text(this.bookName)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
Blank()
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor($r('app.color.card_bg'))
}
// 截止时间
this.formLabel('截止时间')
Row({ space: 8 }) {
Row({ space: 6 }) {
Text(this.hasDue ? this.fmtDue(this.dueMs) : '无截止时间')
.fontSize(14)
.fontColor(this.hasDue
? $r('app.color.text_primary') : $r('app.color.text_hint'))
Blank()
Text('修改')
.fontSize(13)
.fontColor($r('app.color.brand'))
}
.layoutWeight(1)
.padding({ left: 12, right: 12, top: 12, bottom: 12 })
.borderRadius(10)
.backgroundColor($r('app.color.card_bg'))
.onClick(() => {
this.pickShow = true;
})
if (this.hasDue) {
Text('清除')
.fontSize(13)
.fontColor($r('app.color.brand_text'))
.padding({ left: 8, right: 8, top: 12, bottom: 12 })
.onClick(() => {
this.hasDue = false;
this.dueMs = 0;
})
}
}
.width('100%')
// 循环
this.formLabel('重复')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(this.recurOpts, (o: RecurOpt) => {
Text(o.label)
.fontSize(13)
.fontColor(this.recur === o.v
? $r('app.color.button_text') : $r('app.color.text_primary'))
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(14)
.margin({ right: 8, bottom: 8 })
.backgroundColor(this.recur === o.v
? $r('app.color.brand') : $r('app.color.chip_off_bg'))
.onClick(() => {
this.recur = o.v;
})
}, (o: RecurOpt) => `rc_${o.v}`)
}
.width('100%')
if (this.recur !== '' && !this.hasDue) {
Text('提示:循环任务需要有截止时间,才能算出下一次')
.fontSize(11)
.fontColor($r('app.color.error'))
.width('100%')
}
// 优先级
this.formLabel('优先级')
Row({ space: 8 }) {
ForEach(this.priorityOpts, (o: PriorityOpt) => {
Row({ space: 4 }) {
Circle().width(7).height(7).fill(o.color)
Text(o.label)
.fontSize(13)
.fontColor(this.priority === o.v
? $r('app.color.button_text') : $r('app.color.text_primary'))
}
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.borderRadius(14)
.backgroundColor(this.priority === o.v
? $r('app.color.brand') : $r('app.color.chip_off_bg'))
.onClick(() => {
this.priority = o.v;
})
}, (o: PriorityOpt) => `pr_${o.v}`)
Blank()
}
.width('100%')
// 提醒
this.formLabel('提醒')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(this.reminderOpts, (o: ReminderOpt) => {
Text(o.label)
.fontSize(13)
.fontColor(this.reminderActive(o.v)
? $r('app.color.button_text') : $r('app.color.text_primary'))
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(14)
.margin({ right: 8, bottom: 8 })
.backgroundColor(this.reminderActive(o.v)
? $r('app.color.brand') : $r('app.color.chip_off_bg'))
.onClick(() => {
this.toggleReminder(o.v);
})
}, (o: ReminderOpt) => `rm_${o.v}`)
}
.width('100%')
// 标签
this.formLabel('标签')
TextInput({ text: this.categories, placeholder: '多个用逗号分隔' })
.height(46)
.fontSize(14)
.backgroundColor($r('app.color.card_bg'))
.borderRadius(12)
.fontColor($r('app.color.text_primary'))
.placeholderColor($r('app.color.text_hint'))
.onChange((v: string) => {
this.categories = v;
})
// 地点
this.formLabel('地点')
TextInput({ text: this.location, placeholder: '可选' })
.height(46)
.fontSize(14)
.backgroundColor($r('app.color.card_bg'))
.borderRadius(12)
.fontColor($r('app.color.text_primary'))
.placeholderColor($r('app.color.text_hint'))
.onChange((v: string) => {
this.location = v;
})
// 父任务
this.formLabel('父任务')
Row({ space: 8 }) {
Text(this.parentName())
.fontSize(14)
.fontColor(this.parentUid === ''
? $r('app.color.text_hint') : $r('app.color.text_primary'))
Blank()
if (this.parentUid !== '') {
Text('清除')
.fontSize(13)
.fontColor($r('app.color.brand_text'))
.onClick(() => {
this.parentUid = '';
})
} else {
Text('选择')
.fontSize(13)
.fontColor($r('app.color.brand'))
.onClick(() => {
this.parentPickShow = true;
})
}
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor($r('app.color.card_bg'))
.onClick(() => {
this.parentPickShow = true;
})
// 附件
Column({ space: 8 }) {
Row({ space: 8 }) {
Text('附件')
.fontSize(13)
.fontColor($r('app.color.text_secondary'))
Blank()
if (this.attachList().length < 3) {
Text('添加文件')
.fontSize(13)
.fontColor($r('app.color.brand'))
.onClick(() => {
const c = this.getUIContext().getHostContext();
if (c !== undefined) {
this.pickAttachment(c);
}
})
}
}
.width('100%')
ForEach(this.attachList(), (m: AttachMeta, idx: number) => {
Row({ space: 8 }) {
Text(m.name)
.fontSize(13)
.fontColor($r('app.color.text_primary'))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text('✕')
.fontSize(13)
.fontColor($r('app.color.text_hint'))
.onClick(() => {
this.removeAttach(idx);
})
}
.width('100%')
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
.borderRadius(8)
.backgroundColor($r('app.color.card_bg'))
}, (m: AttachMeta, idx: number) => `at_${idx}_${m.name}`)
}
.width('100%')
// 备注
this.formLabel('备注')
TextArea({ text: this.desc, placeholder: '备注(可选)' })
.fontSize(14)
.height(100)
.borderRadius(12)
.backgroundColor($r('app.color.card_bg'))
.fontColor($r('app.color.text_primary'))
.placeholderColor($r('app.color.text_hint'))
.onChange((v: string) => {
this.desc = v;
})
// 删除(编辑态,放表单最底部,与日程页一致)
if (!this.creating && this.writable) {
Button('删除待办')
.width('100%')
.height(46)
.borderRadius(12)
.fontSize(15)
.fontColor($r('app.color.error'))
.backgroundColor($r('app.color.chip_off_bg'))
.margin({ top: 8 })
.onClick(() => {
this.askDelete();
})
}
// 底部留白,避免最后一个字段贴屏幕边缘
Blank().height(24)
}
.width('100%')
.padding({ left: 16, right: 16, top: 4, bottom: 24 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
.align(Alignment.Top)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.page_bg'))
}
/** 字段小标题 */
@Builder
formLabel(txt: string) {
Text(txt)
.fontSize(13)
.fontColor($r('app.color.text_secondary'))
.width('100%')
.textAlign(TextAlign.Start)
}
}
@@ -6,6 +6,7 @@
"pages/CalendarListPage", "pages/CalendarListPage",
"pages/EditAccountPage", "pages/EditAccountPage",
"pages/EventEditPage", "pages/EventEditPage",
"pages/TodoEditPage",
"pages/SettingsPage" "pages/SettingsPage"
] ]
} }