在「待办」视图下点 + 新建待办,其余视图仍新建日程。 新建时把全部可写 CalDAV 日历本铺开供选择,交互与新建日程一致。 ## 原状 `TodoService` 只有 改/删/设重复/打勾,**没有新建方法**; `saveTodoEdit` 开头 `if (... || this.todoEditId <= 0) return;` 直接挡掉新建。 +号固定 `addEvent()`,在待办页点它却跳到日程编辑页,与当前语境不符。 ## 改动 ### IcsUtil.buildTodo(新增) 从零构造 VTODO 资源。不能复用 `rewriteTodoCompletion`——后者是在服务器原文上 打补丁(保留未知字段),前提是原文已存在;新建时没有原文。 字段取舍: - UID / DTSTAMP / SUMMARY 必写 - DESCRIPTION 为空则不写 - DUE 可选;只有日期时写 `DUE;VALUE=DATE` - STATUS 固定 NEEDS-ACTION(完成态由 setCompleted 切换) - 不写 PRIORITY,避免凭空替用户设优先级 ⚠️ 时间格式必须与现有待办写入保持一致:本工程 `rewriteTodoSchedule` 用的是 `DUE:` + `fmtIcsTime(dueMs, false)`,即**浮动本地时间、不带 Z**。 若这里写成 UTC,同一字段出现两种格式,服务器上会显示成早 8 小时的待办。 ### TodoService.createTodo(新增) **先 PUT 服务器、成功后才落库**——与 updateTodo 的顺序相反。 更新时本地已有行可回滚;新建若先落库再 PUT 失败,会留下"本地有、服务器没有" 的孤儿待办,用户看着它存在却永远同步不出去。 同时校验:标题非空、href 非空、账号可定位、日历本可写(只读本提前拦下,避免 403)。 ### Index.ets - `todoCreating` 状态区分新建/编辑 - `openTodoCreator()`:收集全部可写 dav 本;默认选中 Inbox(名字含 inbox/收件箱 者优先),找不到用第一个 —— 少点一步但仍可改 - `saveNewTodo()`:走 createTodo - +号分流:`mode === 'todo'` → 新建待办,否则新建日程 - 弹层标题「新建待办」/按钮「创建」,新建时隐藏"删除"入口 - 日历本区:新建时 Flex 铺开色块胶囊(48% 宽,选中染本色调), 编辑时保持原来只读显示 `CalSource` 没有 href 字段,`createTodo` 需要的 URL 由 calKey(`<accountId>_<index>`) 反解出账号与索引后从 `account.calendarHrefs[index]` 取,映射存在 `todoBookHrefs`。 ## 真机验证(5NC0225B27004718) 待办视图点 + → 弹层标题「新建待办」、8 个日历本全部铺开、 按钮显示「创建」。填标题后点创建: ``` PUT .../synccalendartest/home_todo/todo-1793717941310-bl6ffh.ics → 201 待办新建成功 id=749 → todo-1793717941310-bl6ffh.ics ``` - 默认落到 `home_todo`(Inbox)✅ Inbox 智能匹配生效 - 服务器端复查该资源返回 **200** ✅ 真的创建了 - App 列表即时刷新:未完成 5→6 项,出现「群晖:Inbox(1)」✅ 测试数据已删除(DELETE → 204),未在服务器留残留。 ## 编译 `BUILD SUCCESSFUL`,本次改动无新增警告。
198 lines
5.2 KiB
Plaintext
198 lines
5.2 KiB
Plaintext
// entry/src/main/ets/pages/widget/TodoWidget6x4.ets
|
||
// 6x4 待办服务卡片:4x4 的横向扩展。左侧 = 统计概览,右侧 = 可滚动的待办列表(更多条目)。
|
||
// 每条待办左侧圆形勾选框,点击即完成并推送回服务器。
|
||
let storageTodo6x4 = new LocalStorage();
|
||
|
||
/** 待办卡片单条(解析 todoListJson) */
|
||
class TodoItem6x4 {
|
||
id: number = 0;
|
||
title: string = '';
|
||
dueText: string = '';
|
||
color: string = '#007DFF';
|
||
calName: string = '';
|
||
completed: boolean = false;
|
||
}
|
||
|
||
@Entry(storageTodo6x4)
|
||
@Component
|
||
struct TodoWidget6x4Card {
|
||
@LocalStorageProp('dateMd') dateMd: string = '';
|
||
@LocalStorageProp('weekday') weekday: string = '';
|
||
@LocalStorageProp('todoCount') todoCount: number = 0;
|
||
@LocalStorageProp('todoTodayCount') todoTodayCount: number = 0;
|
||
@LocalStorageProp('todoListJson') todoListJson: string = '[]';
|
||
|
||
private parseTodos(): TodoItem6x4[] {
|
||
try {
|
||
return JSON.parse(this.todoListJson) as TodoItem6x4[];
|
||
} catch (err) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
@Builder
|
||
addButton() {
|
||
Button() {
|
||
Text('+')
|
||
.fontSize(20)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor('#FFFFFF')
|
||
}
|
||
.width(30)
|
||
.height(30)
|
||
.borderRadius(15)
|
||
.padding(0)
|
||
.backgroundColor('#6A4C93')
|
||
.onClick(() => {
|
||
postCardAction(this, {
|
||
action: 'router',
|
||
abilityName: 'EntryAbility',
|
||
params: { widgetAction: 'addTodo' }
|
||
});
|
||
})
|
||
}
|
||
|
||
private openApp(): void {
|
||
postCardAction(this, {
|
||
action: 'router',
|
||
abilityName: 'EntryAbility',
|
||
params: { widgetAction: 'openTodo' }
|
||
});
|
||
}
|
||
|
||
private checkTodo(id: number): void {
|
||
postCardAction(this, {
|
||
action: 'message',
|
||
params: { todoCheck: `${id}` }
|
||
});
|
||
}
|
||
|
||
@Builder
|
||
checkbox(id: number) {
|
||
// ⚠️ 不用 Circle().stroke() —— 那是 SDK 26 的 API,本工程 compatibleSdkVersion 为
|
||
// 6.1.1(24),真机上 stroke 不生效会导致圆圈不可见。
|
||
// 改用 border + 50% 圆角,视觉一致且从 API 7 起就支持。
|
||
Column()
|
||
.width(20)
|
||
.height(20)
|
||
.borderRadius(10)
|
||
.border({ width: 1.5, color: '#B0B0B0' })
|
||
.backgroundColor(Color.Transparent)
|
||
.width(20)
|
||
.height(20)
|
||
.onClick(() => this.checkTodo(id))
|
||
}
|
||
|
||
/** 单条待办(宽版):勾选框 + 色条 + 标题 + 截止时间列 + 日历本名列 */
|
||
@Builder
|
||
todoRow(item: TodoItem6x4) {
|
||
Row({ space: 8 }) {
|
||
this.checkbox(item.id)
|
||
Column()
|
||
.width(3)
|
||
.height(34)
|
||
.borderRadius(2)
|
||
.backgroundColor(item.color)
|
||
Text(item.title)
|
||
.fontSize(13)
|
||
.fontColor('#1A1A1A')
|
||
.maxLines(2)
|
||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
.layoutWeight(1)
|
||
Text(item.dueText)
|
||
.fontSize(11)
|
||
.fontColor(item.dueText.indexOf('逾期') >= 0 ? '#FF3B30' : '#8A8A8A')
|
||
.maxLines(1)
|
||
.width(88)
|
||
.textAlign(TextAlign.End)
|
||
Text(item.calName)
|
||
.fontSize(10)
|
||
.fontColor(item.color)
|
||
.maxLines(1)
|
||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
.width(80)
|
||
.textAlign(TextAlign.End)
|
||
}
|
||
.alignItems(VerticalAlign.Center)
|
||
.width('100%')
|
||
.padding({ left: 4, right: 12, top: 6, bottom: 6 })
|
||
.borderRadius(10)
|
||
.backgroundColor('#F5F7FA')
|
||
}
|
||
|
||
@Builder
|
||
headerBar() {
|
||
Row({ space: 8 }) {
|
||
Row({ space: 6 }) {
|
||
Text('待办')
|
||
.fontSize(16)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#1A1A1A')
|
||
Text(`${this.todoCount}`)
|
||
.fontSize(13)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#FFFFFF')
|
||
.backgroundColor('#6A4C93')
|
||
.borderRadius(9)
|
||
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
|
||
}
|
||
Text(this.todoTodayCount > 0
|
||
? `${this.dateMd} ${this.weekday} · 今日到期 ${this.todoTodayCount} 项`
|
||
: `${this.dateMd} ${this.weekday} · 今日无到期`)
|
||
.fontSize(10)
|
||
.fontColor(this.todoTodayCount > 0 ? '#FF9500' : '#8A8A8A')
|
||
.maxLines(1)
|
||
.layoutWeight(1)
|
||
this.addButton()
|
||
}
|
||
.width('100%')
|
||
}
|
||
|
||
@Builder
|
||
emptyState() {
|
||
Column({ space: 8 }) {
|
||
Text('✓')
|
||
.fontSize(30)
|
||
.fontColor('#C7C7CC')
|
||
Text(this.todoCount === 0 ? '所有待办都完成了' : '暂无待办')
|
||
.fontSize(13)
|
||
.fontColor('#8A8A8A')
|
||
Text('点右上角 + 新建')
|
||
.fontSize(10)
|
||
.fontColor('#B0B0B0')
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
.justifyContent(FlexAlign.Center)
|
||
}
|
||
|
||
build() {
|
||
Column({ space: 8 }) {
|
||
this.headerBar()
|
||
|
||
Divider()
|
||
.strokeWidth(0.5)
|
||
.color('#E5E5E5')
|
||
|
||
if (this.parseTodos().length === 0) {
|
||
this.emptyState()
|
||
} else {
|
||
List({ space: 4 }) {
|
||
ForEach(this.parseTodos(), (item: TodoItem6x4, idx: number) => {
|
||
ListItem() {
|
||
this.todoRow(item)
|
||
}
|
||
}, (item: TodoItem6x4, idx: number) => `t${item.id}_${idx}`)
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
.scrollBar(BarState.Off)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
.padding(12)
|
||
.backgroundColor('#FFFFFF')
|
||
.borderRadius(16)
|
||
}
|
||
} |