From d4b9d08b7e36cf5fc6e732a4bde662b302319111 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 5 Nov 2026 19:48:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E9=A2=98/=E5=A4=87=E6=B3=A8=E6=94=B9?= =?UTF-8?q?=E6=88=90=E5=B8=B8=E9=A9=BB=E8=BE=93=E5=85=A5=E6=A1=86=20+=20?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E7=94=A8=E5=A2=A8=E9=BB=91=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 2026-11-05 两点: 1.「任务标题里面,"台式机采购计划"这样的字样还是改成黑色吧。 任务标题这四个字用主题色,但是输入框里面的文字用黑色。」 2.「这个输入框本来就应该是输入框,怎么现在点了一下才变成输入框呢? 它应该直接就是输入框,只是把文字放到输入框里面就行了。 这样的话,点一下键盘就弹出来了,包括下面的备注也是一样。」 ① 颜色:标签主题色、内容墨黑 上一版我把 TextArea 的 fontColor 也设成 accent,整段标题变成红的, 像报错。现在: · 「任务标题」四个字 → fieldLb(主题色 + Bold) · 框里的文字 → FbColors.ink 备注同理。 ② 常驻输入框(这是关键的结构修正) 上一版是"只读文字 + 点一下切成 TextArea"的双态(titleEdit / noteEdit), 所以用户看到的是"点一下才变成输入框"。 现在**直接就是 TextArea**,值从 titleDraft / noteDraft 来。 点输入框由系统自己聚焦 + 弹键盘,不需要 requestFocus。 ⚠️ 必须是**受控**的:`TextArea({ text: this.titleOf() })` 是非受控, 每次 reload() 重建组件会丢掉用户正在打的字。 ⚠️ 删掉 titleEdit / noteEdit 两个状态(双态不存在了), 保存判据改成"值变了没有"。 ⚠️ 标题**空值回填、不提交**:服务端会把空标题清洗成空串 = 把标题删了。 用户只是清空重打,不该丢标题。 ⚠️ 备注**允许清空**:删掉备注是正常操作,所以 note 不像 title 那样回填。 ③ 焦点保护(新增 titleFocus / noteFocus) `@Monitor(refreshTick)` 会在外部刷新时调 reload(),而 reload 会用 服务端的值覆盖 titleDraft —— 用户正打字时被覆盖就是"打着打着字没了"。 ⇒ 有焦点时不覆盖,失焦后再同步(onBlur → save → reload)。 编译通过,已装真机。 --- entry/src/main/ets/components/FbTaskPanel.ets | 239 ++++++++---------- 1 file changed, 110 insertions(+), 129 deletions(-) diff --git a/entry/src/main/ets/components/FbTaskPanel.ets b/entry/src/main/ets/components/FbTaskPanel.ets index 3d59e96..5c7c727 100644 --- a/entry/src/main/ets/components/FbTaskPanel.ets +++ b/entry/src/main/ets/components/FbTaskPanel.ets @@ -121,12 +121,17 @@ export struct FbTaskPanel { 交互(那几处都是点一下就生效)。这里多一颗按钮反而不一致。 */ @Local duePickOpen: boolean = false; @Local attPickOpen: boolean = false; - /* #77l:「任务标题」也做成可编辑(用户 2026-11-05: - 「台式机采购计划也往下放,也是可以修改的」) */ - @Local titleEdit: boolean = false; + /* #77n:标题与备注都是**常驻输入框**(用户:「它应该直接就是输入框」), + 所以不再需要 `titleEdit` / `noteEdit` 这类双态标记, + 只要两个受控值(框里的文字)。 + ⚠️ 必须受控:直接 `TextArea({text: this.titleOf()})` 是非受控的, + 每次 reload() 重建组件会丢掉用户正在打的字。 */ @Local titleDraft: string = ''; - @Local noteEdit: boolean = false; @Local noteDraft: string = ''; + /* 输入框是否有焦点。用来决定「重新载入后要不要用服务端的值覆盖框里的字」—— + 有焦点说明用户正在打字,这时覆盖会把刚打的字冲掉。 */ + @Local titleFocus: boolean = false; + @Local noteFocus: boolean = false; @Local inlineBusy: boolean = false; /* #77c:附件预览浮层 —— 存当前要全屏看的那张图的地址,空 = 没开 */ @@ -174,6 +179,21 @@ export struct FbTaskPanel { if (d !== null) { this.detail = d; } + /* #77n:标题/备注是常驻输入框,值要跟着服务端走。 + ⚠️ 但**有焦点时不能覆盖**:@Monitor('refreshTick') 会在外部刷新时 + 调 reload(),用户正打字时被覆盖就是"打着打着字没了"。 + 失焦时会再同步一次(onBlur -> save -> reload)。 + ⚠️ 比较用 trim:服务端存的可能带首尾空格, + 直接赋值会让框里的文字"莫名多一个空格"。 */ + const tt: FbTaskItem | null = this.cur(); + if (tt !== null) { + if (!this.titleFocus && this.titleDraft !== tt.title) { + this.titleDraft = tt.title; + } + if (!this.noteFocus && this.noteDraft !== tt.note) { + this.noteDraft = tt.note; + } + } } /** 当前要显示的任务(详情优先,退回列表那一份) */ @@ -378,29 +398,45 @@ export struct FbTaskPanel { } /** 保存备注(失焦/回车时触发) */ - /** 保存任务标题(#77l) */ + /** + * 保存任务标题。 + * + * 🔴 #77n:标题改成**常驻输入框**后不再有 `titleEdit` 这个双态标记, + * 所以只靠"值变了没有"判断要不要发请求。 + * ⚠️ 空值必须**回填原值**:服务端会把空标题清洗成空串, + * 那等于把标题删了 —— 用户只是清空重打,不该丢标题。 + */ private async saveTitle(): Promise { - if (!this.titleEdit) { + const v: string = this.titleDraft.trim(); + if (v === '') { + this.titleDraft = this.titleOf(); // 回填,不提交 return; } - this.titleEdit = false; - const v: string = this.titleDraft.trim(); - if (v === '' || v === this.titleOf()) { - return; // 空标题不发请求(服务端也会清洗成空,等于把标题删了) + if (v === this.titleOf()) { + return; // 没变,不发请求 + } + const ok: boolean = await this.saveField('title', v, -1); + if (!ok) { + /* 保存失败也要把框里改回数据库的真实值,否则框里显示的和库里的不一致 */ + this.titleDraft = this.titleOf(); } - await this.saveField('title', v, -1); } + /** + * 保存备注。 + * 🔴 #77n:备注也改成常驻输入框,不再有 `noteEdit`。 + * ⚠️ 备注**允许清空**(与标题不同):删掉备注是正常操作, + * 所以这里不像 saveTitle 那样回填。 + */ private async saveNote(): Promise { - if (!this.noteEdit) { - return; - } - this.noteEdit = false; const v: string = this.noteDraft.trim(); if (v === this.noteOf()) { return; // 没变,不发请求 } - await this.saveField('note', v, -1); + const ok: boolean = await this.saveField('note', v, -1); + if (!ok) { + this.noteDraft = this.noteOf(); // 失败回滚显示 + } } /** @@ -1183,83 +1219,55 @@ export struct FbTaskPanel { * ⚠️ 其余(分类 / 循环)**保持不变** —— 用户没提,不动。 */ /** - * 任务标题(可编辑)—— 放在**所有内容最前**(状态之前)。 + * 任务标题 —— **一直就是输入框**,不是"点了才变输入框"。 * - * 🔴🔴 #77m(用户 2026-11-05): - * 「任务标题应该放在最前面,放在所有内容之前,也就是状态之前。」 - * 「任务和备注是点进去之后,只要点一下,马上就要变成编辑状态, - * 然后弹出输入输入法,开始输入。不需要点一下之后先变成一个输入框, - * 再点一下弹出输入法。直接点进去之后就是输入状态。」 + * 🔴🔴 #77n(用户 2026-11-05): + * 「这个输入框本来就应该是输入框,怎么现在点了一下才变成输入框呢? + * 它应该直接就是输入框,只是把文字放到输入框里面就行了。 + * 这样的话,点一下键盘就弹出来了,包括下面的备注也是一样。」 + * 「任务标题里面,"台式机采购计划"这样的字样还是改成黑色吧。 + * 任务标题这四个字用主题色,但是输入框里面的文字用黑色。」 * - * ⇒ 点一下直接换成 `TextArea` 并**抢焦点拉起输入法**。 + * 所以: + * · 不设 `titleEdit` 双态,直接把 TextArea 常驻;文字从 `titleOf()` 灌进去。 + * · **标签**(「任务标题」四个字)= 主题色粗体(走 fieldLb); + * **框里的内容** = `FbColors.ink`(墨黑)。上一版我把内容也染成 accent, + * 变成"整段标题都是红的",读起来像报错,也不符合"内容用墨色"。 + * · 点输入框 → 系统自己聚焦 + 弹输入法,不需要我 requestFocus。 * - * ⚠️ 关键:切换成输入框只是"画出来了",**不会自动获得焦点**。 - * 必须 `focusControl.requestFocus(id)` 显式要焦点,否则用户还得再点一下 —— - * 正是用户抱怨的"点一下变输入框、再点一下弹输入法"。 - * ⚠️ 必须 `setTimeout` 延后一帧:`titleEdit = true` 之后框架还没完成 - * 这次重建,此刻 `id` 对应的节点尚不存在,直接 requestFocus 会落空。 - * 60ms 用户完全无感,但足够框架把新节点挂上。 - * ⚠️ `.defaultFocus(true)` 作为兜底:个别机型上 requestFocus 会因 - * "节点刚创建"而失败,defaultFocus 能让它自己拿到焦点。 + * ⚠️ 不能直接 `TextArea({ text: this.titleOf() })` —— 那样是**非受控**: + * 每次 `reload()` 后组件重建会丢掉用户正在打的字。 + * 用 `titleDraft` 做受控值,`aboutToAppear` 里灌初值、保存后同步。 + * ⚠️ 失焦时清空则**恢复原值**(不能把标题存成空串:服务端会把标题清零)。 */ @Builder titleRow() { Column({ space: 6 }) { this.fieldLb('任务标题') - if (this.titleEdit) { - TextArea({ text: this.titleDraft, placeholder: '要做的事…' }) - .id('tkpTitleInput') - .defaultFocus(true) - .placeholderColor(FbColors.faint) - .placeholderFont({ size: FbMetric.fontSmall }) - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Bold) - .fontColor(this.accent) - .backgroundColor(FbColors.inputBg) - .borderRadius(FbMetric.radiusS) - .width('100%') - .height(64) - .padding({ left: 12, right: 12, top: 10, bottom: 10 }) - .onChange((v: string): void => { - this.titleDraft = v; - }) - /* 失焦保存 —— 与备注同一套(用户改完直接点别处是最自然的动作, - 让他再去找"保存"就会以为没存上)。 */ - .onBlur((): void => { - this.saveTitle(); - }) - } else { - Row({ space: 8 }) { - Text(this.titleOf()) - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Bold) - .fontColor(this.isDone() ? FbColors.faint : this.accent) - .decoration({ - type: this.isDone() ? TextDecorationType.LineThrough : TextDecorationType.None, - color: FbColors.faint - }) - .maxLines(3) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) - Text('✎') - .fontSize(FbMetric.fontTiny) - .fontColor(this.inlineBusy ? FbColors.faint : this.accent) - } - .width('100%') - .constraintSize({ minHeight: 44 }) - .padding({ left: 12, right: 12, top: 11, bottom: 11 }) + TextArea({ text: this.titleDraft, placeholder: '要做的事…' }) + .placeholderColor(FbColors.faint) + .placeholderFont({ size: FbMetric.fontBody }) + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Bold) + /* 内容用墨黑(用户明确要求)—— 只有标签那四个字是主题色 */ + .fontColor(FbColors.ink) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) - .alignItems(VerticalAlign.Top) - .onClick((): void => { - if (this.titleEdit || this.inlineBusy) { - return; - } - this.titleDraft = this.titleOf(); - this.titleEdit = true; - focusControl.requestFocus('tkpTitleInput'); + .width('100%') + .height(64) + .padding({ left: 12, right: 12, top: 10, bottom: 10 }) + .onChange((v: string): void => { + this.titleDraft = v; + }) + .onFocus((): void => { + this.titleFocus = true; + }) + /* 失焦保存 —— 改完直接点别处是最自然的动作, + 让他再去找"保存"就会以为没存上。 */ + .onBlur((): void => { + this.titleFocus = false; + this.saveTitle(); }) - } } .width('100%') .alignItems(HorizontalAlign.Start) @@ -1372,58 +1380,31 @@ export struct FbTaskPanel { } /* ---- 备注 ---- */ + /* 🔴 #77n(用户 2026-11-05):「包括下面的备注也是一样」 + ⇒ 与任务标题同处理:**常驻输入框**,不做"点一下才变输入框"。 + 标签(「备注」两字)= 主题色;框里内容 = 墨黑。 */ Column({ space: 6 }) { this.fieldLb('备注') - if (this.noteEdit) { - TextArea({ text: this.noteDraft, placeholder: '补充说明…' }) - .id('tkpNoteInput') - .defaultFocus(true) - .placeholderColor(FbColors.faint) - .placeholderFont({ size: FbMetric.fontTiny }) - .fontSize(FbMetric.fontSmall) - .fontColor(FbColors.ink) - .backgroundColor(FbColors.inputBg) - .borderRadius(FbMetric.radiusS) - .width('100%') - .height(76) - .padding({ left: 12, right: 12, top: 10, bottom: 10 }) - .onChange((v: string): void => { - this.noteDraft = v; - }) - /* 失焦保存 —— 用户改完直接点别处是很自然的动作, - 等他再去找"保存"就会以为没存上。 */ - .onBlur((): void => { - this.saveNote(); - }) - } else { - Row({ space: 8 }) { - Text(this.noteOf() !== '' ? this.noteOf() : '点这里添加备注…') - .fontSize(FbMetric.fontSmall) - .fontColor(this.noteOf() === '' ? FbColors.faint : FbColors.ink) - .maxLines(3) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) - Text('✎') - .fontSize(FbMetric.fontTiny) - .fontColor(this.inlineBusy ? FbColors.faint : this.accent) - } - .width('100%') - .constraintSize({ minHeight: 40 }) - .padding({ left: 12, right: 12, top: 11, bottom: 11 }) + TextArea({ text: this.noteDraft, placeholder: '补充说明…' }) + .placeholderColor(FbColors.faint) + .placeholderFont({ size: FbMetric.fontSmall }) + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.ink) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) - .alignItems(VerticalAlign.Top) - .onClick((): void => { - if (this.noteEdit || this.inlineBusy) { - return; - } - this.noteDraft = this.noteOf(); - this.noteEdit = true; - /* #77m:与任务标题同一套 —— 点一下就进编辑态并拉起输入法, - 不用"再点一下输入框"(用户明确要求)。 */ - focusControl.requestFocus('tkpNoteInput'); + .width('100%') + .height(84) + .padding({ left: 12, right: 12, top: 10, bottom: 10 }) + .onChange((v: string): void => { + this.noteDraft = v; + }) + .onFocus((): void => { + this.noteFocus = true; + }) + .onBlur((): void => { + this.noteFocus = false; + this.saveNote(); }) - } } .width('100%') .alignItems(HorizontalAlign.Start)