标题/备注改成常驻输入框 + 内容用墨黑 (#77)
用户 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)。
编译通过,已装真机。
This commit is contained in:
@@ -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<void> {
|
||||
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<void> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user