fix(卡片/粘贴): 5 项反馈修复 —— 粘贴按钮、卡片数据、卡片加号、卡片滚动、列表统一
## 1. 粘贴按钮文字看不清(EventEditPage)
根因不是"忘了设文字"——空参时系统本就给默认样式(LINES 图标 + PASTE 文案)。
真正原因是我**给了一个和系统文字颜色太接近的背景色**(card_bg 近乎白)。
查 SDK 类型定义 `paste_button.d.ts`,错误码 2 明确列出:
「字体或图标颜色与组件背景颜色相近」「文字超出组件背景区域」等
安全组件创建时会做样式校验,对比度不足会让文字灰淡难辨、甚至授权失败。
同时发现 icon/text/buttonType **只能走构造参数**,不是链式属性
(`.text()` 会编译报 "Property 'text' does not exist")。
改为:
```
PasteButton({
icon: PasteIconStyle.LINES, // 该枚举只有 LINES
text: PasteDescription.PASTE, // 该枚举只有 PASTE
buttonType: ButtonType.Capsule
})
```
并**去掉自定义背景色**,交给系统默认样式,只保留尺寸与圆角。
## 2. 待办卡片显示 0 条(CardDataService)
`pushToAllForms()` 是同步完成后刷新所有卡片的主路径,
但它只调 `buildCardData()`(日程),**从未调 `buildTodoCardData()`** ⇒
推给卡片的 binding 里 todoCount / todoListJson 全是默认值。
而 EntryFormAbility.pushData() 里合并了待办 —— 两条路径不一致,
所以表现成"有时对、有时 0"。
修:抽出 `mergeTodoInto()` 供两处共用;待办数据与 dayOffset 无关,
整个 pushToAllForms 只构建一次。
真机日志:`待办卡片数据刷新:未完成 5 条…列表输出 5 条`(原为 0)。
## 3. 待办卡片右上角 + 点了没反应(EntryAbility + Index)
待办卡片发的是 `widgetAction: 'addTodo'` / `'openTodo'`,
但 EntryAbility 只判断 `raw === 'add'`,Index 也只处理 `'add'`
⇒ 参数被静默丢弃。
修:两端都补上 addTodo / openTodo。
addTodo → 切待办 Tab + 打开新建待办
openTodo → 切待办 Tab
## 4. 日程 2×4 卡片右侧可滚动,15 条(CardDataService + Widget4x2)
数据侧 `slice(0, 2)` → `slice(0, 15)`
视图侧 `Column + ForEach` → `List + ListItem`(虚拟化 + 滚动惯性 + 回弹)
真机日志:`进行中 15 条`,确认上限生效。
## 5. 待办 4×4 / 6×4 列表样式统一,全部可滚动
按用户选择,**统一成 4×4 样式**(勾选框 + 色条 + 标题 + 副行「截止时间 · 日历本」)。
6×4 原来是「标题 + 独立截止时间列 + 独立日历本列」,窄卡片里会把标题挤没,
改为与 4×4 完全相同的结构。已用逐行 diff 校验:两卡 todoRow **36 行完全一致**。
`MAX_TODO_ITEMS` 20 → 15,与日程卡片口径一致(所有待办卡片都已可滚动,
不必再按卡片尺寸给不同上限)。
## 编译
`BUILD SUCCESSFUL`
## 未验证
真机仅验证了「待办卡片数据不再为 0」与「日程 15 条上限」两项。
粘贴按钮视觉、卡片内滚动、卡片 + 跳转、两卡样式一致性需上机目视确认。
This commit is contained in:
@@ -149,25 +149,53 @@ export class CardDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** App 内同步完成后调用:刷新所有已添加的卡片 */
|
/** App 内同步完成后调用:刷新所有已添加的卡片 */
|
||||||
|
/**
|
||||||
|
* 把待办数据合并进日程数据对象。
|
||||||
|
*
|
||||||
|
* ⭐ 抽出来的原因: EntryFormAbility.pushData 与 CardDataService.pushToAllForms
|
||||||
|
* 是两条独立的推送路径,之前只有前者做了合并,后者漏了 ⇒
|
||||||
|
* 待办卡片永远显示 0 条(todoCount 停在默认值)。
|
||||||
|
* 两处共用同一段逻辑,避免再出现"改一处忘一处"。
|
||||||
|
*/
|
||||||
|
private static mergeTodoInto(ev: CardData, td: CardData): void {
|
||||||
|
ev.todoListJson = td.todoListJson;
|
||||||
|
ev.todoCount = td.todoCount;
|
||||||
|
ev.todoTodayCount = td.todoTodayCount;
|
||||||
|
}
|
||||||
|
|
||||||
static async pushToAllForms(context: common.Context): Promise<void> {
|
static async pushToAllForms(context: common.Context): Promise<void> {
|
||||||
// 先恢复持久化的 formId(App 重启后内存注册表为空)
|
// 先恢复持久化的 formId(App 重启后内存注册表为空)
|
||||||
await CardDataService.loadPersistedFormIds(context);
|
await CardDataService.loadPersistedFormIds(context);
|
||||||
if (CardDataService.formIds.length === 0) {
|
if (CardDataService.formIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ⭐ 待办数据与 dayOffset 无关(待办不翻页),只构建一次
|
||||||
|
let todoData: CardData | null = null;
|
||||||
|
try {
|
||||||
|
todoData = await CardDataService.buildTodoCardData(context);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`待办卡片数据构建失败: ${(err as BusinessError).message}`);
|
||||||
|
}
|
||||||
// 各卡片可能翻到了不同日期 → 按偏移分组构建,避免把用户翻走的日期拽回今天
|
// 各卡片可能翻到了不同日期 → 按偏移分组构建,避免把用户翻走的日期拽回今天
|
||||||
const cache: Map<number, formBindingData.FormBindingData> =
|
const cache: Map<number, formBindingData.FormBindingData> =
|
||||||
new Map<number, formBindingData.FormBindingData>();
|
new Map<number, formBindingData.FormBindingData>();
|
||||||
|
const fallbackData: CardData = await CardDataService.buildCardData(context, 0);
|
||||||
|
if (todoData !== null) {
|
||||||
|
CardDataService.mergeTodoInto(fallbackData, todoData);
|
||||||
|
}
|
||||||
const fallback: formBindingData.FormBindingData =
|
const fallback: formBindingData.FormBindingData =
|
||||||
formBindingData.createFormBindingData(await CardDataService.buildCardData(context, 0));
|
formBindingData.createFormBindingData(fallbackData);
|
||||||
cache.set(0, fallback);
|
cache.set(0, fallback);
|
||||||
const stale: string[] = [];
|
const stale: string[] = [];
|
||||||
for (const formId of CardDataService.formIds) {
|
for (const formId of CardDataService.formIds) {
|
||||||
const off: number = CardDataService.getDayOffset(formId);
|
const off: number = CardDataService.getDayOffset(formId);
|
||||||
let binding: formBindingData.FormBindingData | undefined = cache.get(off);
|
let binding: formBindingData.FormBindingData | undefined = cache.get(off);
|
||||||
if (binding === undefined) {
|
if (binding === undefined) {
|
||||||
binding = formBindingData.createFormBindingData(
|
const d: CardData = await CardDataService.buildCardData(context, off);
|
||||||
await CardDataService.buildCardData(context, off));
|
if (todoData !== null) {
|
||||||
|
CardDataService.mergeTodoInto(d, todoData);
|
||||||
|
}
|
||||||
|
binding = formBindingData.createFormBindingData(d);
|
||||||
cache.set(off, binding);
|
cache.set(off, binding);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -327,14 +355,16 @@ export class CardDataService {
|
|||||||
// 今日日程条数:item.date === '今天' 的均为今天分组(含全天/跨天进行中)
|
// 今日日程条数:item.date === '今天' 的均为今天分组(含全天/跨天进行中)
|
||||||
data.todayCount = items.filter((i: CardItem): boolean => i.date === '今天').length;
|
data.todayCount = items.filter((i: CardItem): boolean => i.date === '今天').length;
|
||||||
|
|
||||||
// ===== 2x4 卡片右侧:**当前时间之后最近的 2 条**日程 =====
|
// ===== 2x4 卡片右侧:**当前时间之后最近的若干条**日程(卡片可滚动,最多 15 条)=====
|
||||||
// 规则:只取"还没开始"的有时间日程(startTime >= now),
|
// 规则:只取"还没开始"的有时间日程(startTime >= now),
|
||||||
// **排除全天 / 跨天**,按开始时间升序取前 2 条。
|
// **排除全天 / 跨天**,按开始时间升序取前 2 条。
|
||||||
// 不再区分"进行中 / 即将开始" —— 卡片上不再显示这些字样。
|
// 不再区分"进行中 / 即将开始" —— 卡片上不再显示这些字样。
|
||||||
const upcomingNext: DisplayEvent[] = upcoming
|
const upcomingNext: DisplayEvent[] = upcoming
|
||||||
.filter((e: DisplayEvent): boolean => !isDayLong(e) && e.startTime >= nowMs)
|
.filter((e: DisplayEvent): boolean => !isDayLong(e) && e.startTime >= nowMs)
|
||||||
.sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime);
|
.sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime);
|
||||||
const ongoingPick: DisplayEvent[] = upcomingNext.slice(0, 2);
|
// ⭐ 上限 15 条:卡片可滚动,不必再为了"放得下"而截断到 2 条。
|
||||||
|
// 15 是经验值 —— 再多则用户更该进 App 看,而不是在卡片里翻。
|
||||||
|
const ongoingPick: DisplayEvent[] = upcomingNext.slice(0, 15);
|
||||||
const ongoingItems: CardItem[] = [];
|
const ongoingItems: CardItem[] = [];
|
||||||
for (const e of ongoingPick) {
|
for (const e of ongoingPick) {
|
||||||
const dayLong: boolean = isDayLong(e);
|
const dayLong: boolean = isDayLong(e);
|
||||||
@@ -404,8 +434,12 @@ export class CardDataService {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卡片列表最多渲染多少条待办(4x4 用前 6 条、6x4 用前 10 条,卡片侧自行截断) */
|
/**
|
||||||
private static readonly MAX_TODO_ITEMS: number = 20;
|
* 卡片列表最多输出多少条待办。
|
||||||
|
* ⭐ 统一为 15:所有待办卡片都已改为可滚动(List + 虚拟化),
|
||||||
|
* 不必再为了"一屏放得下"而按卡片尺寸给不同上限。
|
||||||
|
*/
|
||||||
|
private static readonly MAX_TODO_ITEMS: number = 15;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建**待办卡片**数据。
|
* 构建**待办卡片**数据。
|
||||||
|
|||||||
@@ -42,10 +42,12 @@ export default class EntryAbility extends UIAbility {
|
|||||||
private handleWidgetParam(want: Want): void {
|
private handleWidgetParam(want: Want): void {
|
||||||
const params = want.parameters;
|
const params = want.parameters;
|
||||||
if (params !== undefined) {
|
if (params !== undefined) {
|
||||||
|
// ⭐ 白名单:日程卡片发 'add',待办卡片发 'addTodo'(新建待办)/ 'openTodo'(进待办页)。
|
||||||
|
// 之前只认 'add',待办卡片点 + 毫无反应 —— 参数被静默丢弃。
|
||||||
const raw = params['widgetAction'];
|
const raw = params['widgetAction'];
|
||||||
if (typeof raw === 'string' && raw === 'add') {
|
if (typeof raw === 'string' && (raw === 'add' || raw === 'addTodo' || raw === 'openTodo')) {
|
||||||
AppStorage.setOrCreate<string>('widgetAction', 'add');
|
AppStorage.setOrCreate<string>('widgetAction', raw);
|
||||||
LogUtil.write('卡片添加入口:已设置 widgetAction=add');
|
LogUtil.write(`卡片入口:已设置 widgetAction=${raw}`);
|
||||||
}
|
}
|
||||||
// 卡片点击进入日历时,定位到卡片当前显示的那一天(用户可能已翻到其他日期)
|
// 卡片点击进入日历时,定位到卡片当前显示的那一天(用户可能已翻到其他日期)
|
||||||
const od = params['openDate'];
|
const od = params['openDate'];
|
||||||
|
|||||||
@@ -1746,11 +1746,21 @@ struct EventEditPage {
|
|||||||
// 用它而不是自绘按钮 + pasteboard.getData(),原因见 pastePlace() 注释:
|
// 用它而不是自绘按钮 + pasteboard.getData(),原因见 pastePlace() 注释:
|
||||||
// READ_PASTEBOARD 是受限权限,普通应用申请会被拒绝。
|
// READ_PASTEBOARD 是受限权限,普通应用申请会被拒绝。
|
||||||
// PasteButton 无需声明权限、无需弹窗,权限仅在 onClick 内有效。
|
// PasteButton 无需声明权限、无需弹窗,权限仅在 onClick 内有效。
|
||||||
PasteButton()
|
// ⭐ PasteButton:安全控件,点击时临时获得剪贴板读权限
|
||||||
|
// (无需申请 READ_PASTEBOARD —— 那是受限权限,普通应用申请会被拒)
|
||||||
|
//
|
||||||
|
// ⚠️ icon/text/buttonType **只能走构造参数**,写成链式属性会编译报错。
|
||||||
|
// ⚠️ 不要自定义背景色:SDK 明确把"字体颜色与背景相近"列为样式违规
|
||||||
|
// (错误码 2),会让人字形灰淡难辨、甚至授权失败。
|
||||||
|
// 交给系统默认样式即可,只调尺寸与圆角。
|
||||||
|
PasteButton({
|
||||||
|
icon: PasteIconStyle.LINES,
|
||||||
|
text: PasteDescription.PASTE,
|
||||||
|
buttonType: ButtonType.Capsule
|
||||||
|
})
|
||||||
.height(44)
|
.height(44)
|
||||||
.padding({ left: 8, right: 8 })
|
.padding({ left: 10, right: 14 })
|
||||||
.borderRadius(12)
|
.borderRadius(12)
|
||||||
.backgroundColor($r('app.color.card_bg'))
|
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.pastePlace();
|
this.pastePlace();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -291,12 +291,28 @@ struct Index {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消费卡片"添加"深链:拉起新建日程页并立即清除标记,避免重复触发 */
|
/**
|
||||||
|
* 消费卡片"添加"深链。
|
||||||
|
*
|
||||||
|
* 三种来源:
|
||||||
|
* add 日程卡片 + → 新建日程
|
||||||
|
* addTodo 待办卡片 + → 新建待办
|
||||||
|
* openTodo 待办卡片整卡点击 → 切到待办 Tab
|
||||||
|
* 处理完立刻清标记,否则页面重建会重复触发。
|
||||||
|
*/
|
||||||
private consumeWidgetAction(): void {
|
private consumeWidgetAction(): void {
|
||||||
const action: string = AppStorage.get<string>('widgetAction') ?? '';
|
const action: string = AppStorage.get<string>('widgetAction') ?? '';
|
||||||
|
if (action === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AppStorage.setOrCreate<string>('widgetAction', '');
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
AppStorage.setOrCreate<string>('widgetAction', '');
|
|
||||||
this.addEvent();
|
this.addEvent();
|
||||||
|
} else if (action === 'addTodo') {
|
||||||
|
this.mode = 'todo';
|
||||||
|
this.openTodoCreator();
|
||||||
|
} else if (action === 'openTodo') {
|
||||||
|
this.mode = 'todo';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,39 +83,50 @@ struct TodoWidget6x4Card {
|
|||||||
.onClick(() => this.checkTodo(id))
|
.onClick(() => this.checkTodo(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单条待办(宽版):勾选框 + 色条 + 标题 + 截止时间列 + 日历本名列 */
|
/**
|
||||||
|
* 单条待办:勾选框 + 色条 + 标题 + 副行(截止时间 · 日历本)。
|
||||||
|
*
|
||||||
|
* ⚠️ 与 TodoWidget4x4 的 todoRow **保持完全一致**(用户要求两个卡片显示效果一模一样)。
|
||||||
|
* 改这里必须同步改那边,否则又会不一致。
|
||||||
|
* 选这个结构而不是"独立列 + 固定宽度":标题 layoutWeight(1) 能吃满剩余宽度,
|
||||||
|
* 在窄卡片里也不会被挤出屏幕。
|
||||||
|
*/
|
||||||
@Builder
|
@Builder
|
||||||
todoRow(item: TodoItem6x4) {
|
todoRow(item: TodoItem6x4) {
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
this.checkbox(item.id)
|
this.checkbox(item.id)
|
||||||
Column()
|
Column()
|
||||||
.width(3)
|
.width(3)
|
||||||
.height(34)
|
.height(32)
|
||||||
.borderRadius(2)
|
.borderRadius(2)
|
||||||
.backgroundColor(item.color)
|
.backgroundColor(item.color)
|
||||||
Text(item.title)
|
Column({ space: 3 }) {
|
||||||
.fontSize(13)
|
Text(item.title)
|
||||||
.fontColor('#1A1A1A')
|
.fontSize(13)
|
||||||
.maxLines(2)
|
.fontColor('#1A1A1A')
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.maxLines(2)
|
||||||
.layoutWeight(1)
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
Text(item.dueText)
|
Row({ space: 6 }) {
|
||||||
.fontSize(11)
|
Text(item.dueText)
|
||||||
.fontColor(item.dueText.indexOf('逾期') >= 0 ? '#FF3B30' : '#8A8A8A')
|
.fontSize(10)
|
||||||
.maxLines(1)
|
.fontColor(item.dueText.indexOf('逾期') >= 0 ? '#FF3B30' : '#8A8A8A')
|
||||||
.width(88)
|
.maxLines(1)
|
||||||
.textAlign(TextAlign.End)
|
if (item.calName !== '') {
|
||||||
Text(item.calName)
|
Text(item.calName)
|
||||||
.fontSize(10)
|
.fontSize(10)
|
||||||
.fontColor(item.color)
|
.fontColor(item.color)
|
||||||
.maxLines(1)
|
.maxLines(1)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.width(80)
|
.constraintSize({ maxWidth: '55%' })
|
||||||
.textAlign(TextAlign.End)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Center)
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 4, right: 12, top: 6, bottom: 6 })
|
.padding({ left: 4, right: 10, top: 6, bottom: 6 })
|
||||||
.borderRadius(10)
|
.borderRadius(10)
|
||||||
.backgroundColor('#F5F7FA')
|
.backgroundColor('#F5F7FA')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ struct Widget4x2Card {
|
|||||||
/** 卡片当前显示的那一天 0 点毫秒 —— 点卡片进 App 时定位到这一天 */
|
/** 卡片当前显示的那一天 0 点毫秒 —— 点卡片进 App 时定位到这一天 */
|
||||||
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
||||||
|
|
||||||
/** 右侧最多渲染 2 条(数据侧已裁剪,这里再兜底截断) */
|
/** 右侧列表数据(可滚动;数据侧已按时间升序并限制在 15 条内) */
|
||||||
private parseOngoing(): CardItem2x4[] {
|
private parseOngoing(): CardItem2x4[] {
|
||||||
try {
|
try {
|
||||||
const all: CardItem2x4[] = JSON.parse(this.ongoingJson) as CardItem2x4[];
|
return JSON.parse(this.ongoingJson) as CardItem2x4[];
|
||||||
return all.slice(0, 2);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -102,7 +101,7 @@ struct Widget4x2Card {
|
|||||||
}
|
}
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Center)
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
.padding({ left: 6, right: 8, top: 4, bottom: 4 })
|
||||||
.borderRadius(8)
|
.borderRadius(8)
|
||||||
.backgroundColor('#F5F7FA')
|
.backgroundColor('#F5F7FA')
|
||||||
}
|
}
|
||||||
@@ -183,19 +182,24 @@ struct Widget4x2Card {
|
|||||||
.strokeWidth(0.5)
|
.strokeWidth(0.5)
|
||||||
.color('#E5E5E5')
|
.color('#E5E5E5')
|
||||||
|
|
||||||
// ===== 右半:当前时间之后最近的两条日程(不滚动)=====
|
// ===== 右半:未来日程列表(可滚动,最多 15 条)=====
|
||||||
|
// ⭐ 原来固定 Column + 只取 2 条,用户想看后天的安排就看不到。
|
||||||
|
// 改为 List:自带虚拟化与滚动惯性,条目多也不占内存。
|
||||||
Column() {
|
Column() {
|
||||||
if (this.parseOngoing().length === 0) {
|
if (this.parseOngoing().length === 0) {
|
||||||
this.emptyHint()
|
this.emptyHint()
|
||||||
} else {
|
} else {
|
||||||
Column({ space: 6 }) {
|
List({ space: 4 }) {
|
||||||
ForEach(this.parseOngoing(), (item: CardItem2x4, idx: number) => {
|
ForEach(this.parseOngoing(), (item: CardItem2x4, idx: number) => {
|
||||||
this.ongoingRow(item)
|
ListItem() {
|
||||||
}, (item: CardItem2x4, idx: number) => `${idx}_${item.title}_${item.time}`)
|
this.ongoingRow(item)
|
||||||
|
}
|
||||||
|
}, (item: CardItem2x4, idx: number) => `o${idx}_${item.time}_${item.title}`)
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
.justifyContent(FlexAlign.Center)
|
.scrollBar(BarState.Off)
|
||||||
|
.edgeEffect(EdgeEffect.Spring)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user