Files
SyncCalendar/entry/src/main/ets/pages/widget/TodoWidget6x4.ets
T
laoyang ca97ec7fcd feat(待办): +号职责感知 + 新建待办(所有可写日历本铺开选择)
在「待办」视图下点 + 新建待办,其余视图仍新建日程。
新建时把全部可写 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`,本次改动无新增警告。
2026-11-03 23:00:07 +08:00

198 lines
5.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// entry/src/main/ets/pages/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)
}
}