选人改「只留搜索框」+ 附件用系统打开 + 留言头像与主题色 (#77)

用户 2026-11-05 五项。

① 选人慢 → 未输入关键词时不渲染任何人
   用户:「实在不行,我们就不把人列出来了,只留搜索框…输入拼音首字母就可以找到这个人」

   慢的根因(不只是节点多):
     · 名册 **800 人**(不是 8 个);
     · 每人一个 FbAvatar,而它内部调 fbImageSrc(avatar, token) ——
       那是**同步**查本地缓存 + 拼 URL,800 次全在打开弹窗那一帧跑完;
     · 再加 800 个 Column/Text 节点参与布局。
   ⇒ 未输入时一个候选都不渲染,只显示"输入名字或拼音首字母找人"。
     只有真打了字才过滤、才渲染(结果通常 1~20 个,瞬间出来)。

   ⚠️ 没改成"只显示前 N 个":800 人里翻前 20 个没用,用户还是得搜。
   ⚠️ 暂存区(已选那几个)不受影响:数量极少且必须一直可见
      (否则用户忘记选了谁)。多选 + 多次搜索的流程因此保持完整:
      搜一个 → 点选进暂存 → 改关键词再搜 → 再点选 → 确定。

② 附件:Word 等非图片文件交给系统应用打开
   用户:「应该打开本机上默认能够打开这个文件的应用,让它把文件打开才对。」

   项目里**已经有** common/FbAttachOpen.ets(下载明文到 cacheDir +
   隐式 Want + action.viewData 交给系统「打开方式」),只是任务面板没调用。
   ⇒ 附件缩略图挂上 onClick:
       · 图片 → 就地全屏预览(新加浮层,ImageFit.Contain 看全图不裁边)
       · 其它 → fbAttachOpen(),系统自己找能打开 .docx 的应用
   ⚠️ 不能用「显式 Want + 包名」:不知道用户装的是 WPS 还是 Office。
   ⚠️ 预览用 Contain 而非 Cover —— 缩略图用 Cover 对,预览裁边就错了。

③ 留言人头像
   服务端 Task::comments() 的 SQL 里**一直有** `p.avatar`,
   端上 parseTaskComment 只取了 name/color ⇒ 头像从没显示过。
   补 RawTaskComment.avatar + FbTaskComment.avatar + 解析,
   UI 换成公共 FbAvatar(有图用图、没图退回首字色块)。

④ 留言标题/发言人用模块主题色加粗
   原来发言人是 muted 灰、常规字重 —— 漏了全站「以模块色标识」的规矩
   (观影紫 / 旅程绿 / 任务赭红)。改成 this.accent + FontWeight.Medium。
   同时把「附件」「留言」「子任务」三个小节标题也从 ink 改成主题色。

   ⚠️ 正文**没有**跟着改主题色:整段上色会掉可读性(色彩需要留白)。
      如果用户要求正文也变色,改一行即可(已在代码注释里标了位置)。

⑤ 编译通过,已装真机。
This commit is contained in:
2026-11-05 19:19:57 +08:00
parent 4e2fbb04b7
commit ad25792732
4 changed files with 178 additions and 17 deletions
@@ -194,7 +194,7 @@ export struct FbPersonPickSheet {
.height(15)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(FbTint.faint))
TextInput({ text: this.keyword, placeholder: '名字或拼音首字母,如 yyq 找「杨永全」' })
TextInput({ text: this.keyword, placeholder: '输入名字或拼音首字母开始搜索' })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
@@ -259,6 +259,27 @@ export struct FbPersonPickSheet {
// ---- 候选人网格 ----
Scroll() {
Column() {
/* 🔴🔴 #77b(用户 2026-11-05):「选人的时候速度确实有些慢。
实在不行,我们就不把人列出来了,只留搜索框。」
「包括下面的协作人共享那个地方也是一样的。弹出来之后只有搜索框,
输入拼音首字母就可以找到这个人。」
慢的根因(不只是节点多):
· 家里名册 **800 人**(不是 8 个);
· 每个人一个 `FbAvatar`,而 `FbAvatar` 内部会调
`fbImageSrc(avatar, token)` —— 那是**逐个查本地缓存 + 拼 URL**,
800 次字符串/文件系统操作全在**打开弹窗那一帧**同步跑完;
· 再加 800 个 Column/Text 节点参与布局。
⇒ 打开就卡。
⇒ 未输入关键词时**一个候选都不渲染**,只显示一句提示。
只有真打了字才过滤、才渲染(结果通常 1~20 个,瞬间出来)。
这也正是用户要的交互:"只留搜索框,搜索的时候再出来人"。
⚠️ 不能改成"只显示前 N 个" —— 800 人里翻前 20 个没用,
用户还是得搜。搜索才是这一屏的唯一入口。
⚠️ 暂存区(已选的那几个)**不受影响**:它只渲染用户点过的,
数量极少,而且必须一直可见(否则用户忘记选了谁)。 */
if (this.people.length === 0) {
Text('名册还没同步,同看的人稍后可以在网页端补')
.fontSize(FbMetric.fontTiny)
@@ -266,6 +287,19 @@ export struct FbPersonPickSheet {
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 24, bottom: 24 })
} else if (this.keyword.trim() === '') {
/* 没输入 = 不渲染任何人 */
Column({ space: 6 }) {
Text('输入名字或拼音首字母找人')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
Text('例:yyq 找「杨永全」,或直接打「杨」')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
}
.width('100%')
.padding({ top: 28, bottom: 28 })
.alignItems(HorizontalAlign.Center)
} else if (this.filtered().length === 0) {
Text('没有匹配的人')
.fontSize(FbMetric.fontTiny)
+128 -16
View File
@@ -35,9 +35,11 @@ import {
} from '../model/Types';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
import { FbCheckCircle } from './Widgets';
import { FbCheckCircle, FbAvatar } from './Widgets';
import { FbPersonPickSheet } from './FbPersonPickSheet';
import { FbDateTimePicker, fbIsoOf, fbJoinDateTime } from './FbDateTimeField';
import { fbAttachOpen } from '../common/FbAttachOpen';
import { common } from '@kit.AbilityKit';
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { fbApi, FbApiResult, FbApiOptions } from '../data/FbHttp';
@@ -123,6 +125,10 @@ export struct FbTaskPanel {
@Local noteEdit: boolean = false;
@Local noteDraft: string = '';
@Local inlineBusy: boolean = false;
/* #77c:附件预览浮层 —— 存当前要全屏看的那张图的地址,空 = 没开 */
@Local previewSrc: string = '';
@Local previewName: string = '';
// 共享(工单 #26)
@Local roster: FbRosterPerson[] = [];
@Local pickingShare: boolean = false;
@@ -374,6 +380,43 @@ export struct FbTaskPanel {
await this.saveField('note', v, -1);
}
/**
* 点附件(#77c)。
*
* 🔴 用户 2026-11-05:「附件那个地方如果是Word文档,好像还是不能预览。
* 按道理说,应该打开本机上默认能够打开这个文件的应用,让它把文件打开才对。」
*
* 分两种情况:
* · **图片** → 就地全屏预览(不跳出 App,看附件不用切来切去)
* · **其它**(Word / PDF / Excel…)→ `fbAttachOpen()`:
* 先下载一份明文到 cacheDir,再用**隐式 Want + action.viewData**
* 交给系统「打开方式」—— 系统自己找能打开 .docx 的应用。
*
* ⚠️ 不能用「显式 Want + 包名」:我们不知道用户装的是 WPS 还是 Office,
* 也不知道包名。让系统按 MIME/扩展名匹配才是唯一可靠的做法。
* ⚠️ `fbAttachOpen()` 已经处理好"下不下来 / 没有可用应用"的提示文案,
* 这里只把它的返回串显示出来,不自己再编一套。
*/
private async tapAttach(a: FbTaskAttach): Promise<void> {
if (a.isImage) {
/* 图片:全屏预览。用 `fbImageSrc`(本地缓存优先,没有就远端 URL) */
this.previewSrc = fbImageSrc(a.key, this.token());
this.previewName = a.name;
return;
}
/* 非图片:交给系统打开 */
const ctx: common.Context | undefined =
this.getUIContext().getHostContext() as common.Context | undefined;
if (ctx === undefined) {
this.note = '打不开:拿不到上下文,请重启应用再试。';
return;
}
this.note = '正在下载…';
const err: string = await fbAttachOpen(this.token(), a.key, false, ctx);
/* `fbAttachOpen` 成功时返回 ''(已交给系统),失败时返回人话 */
this.note = err !== '' ? err : '';
}
private categoryOf(): string {
const d: FbTaskDetail | null = this.detail;
return d !== null ? d.category : '';
@@ -755,6 +798,56 @@ export struct FbTaskPanel {
})
}
/* 🔴 #77f(用户 2026-11-05):「附件那个地方…按道理说,应该打开本机上
默认能够打开这个文件的应用」——图片走就地全屏预览(不跳出 App),
非图片走 `fbAttachOpen()` 交给系统(见 `tapAttach`)。
⚠️ 预览层放在**所有浮层最后**:它要盖住面板本身。
⚠️ 整块用 `Stack` 铺满 + 半透明黑底,点击任意处关闭。
⚠️ `objectFit(ImageFit.Contain)` —— 预览要看全图,
用 Cover 会裁掉边缘(缩略图用 Cover 是对的,预览不是)。 */
if (this.previewSrc !== '') {
Stack({ alignContent: Alignment.Center }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#E6000000')
.onClick((): void => {
this.previewSrc = '';
this.previewName = '';
})
Image(this.previewSrc)
.width('100%')
.constraintSize({ maxHeight: '78%' })
.objectFit(ImageFit.Contain)
/* 底部:文件名 + 关闭提示 */
Column({ space: 4 }) {
Text(this.previewName !== '' ? this.previewName : '附件')
.fontSize(FbMetric.fontSmall)
.fontColor(Color.White)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Center)
Text('点任意处关闭')
.fontSize(10)
.fontColor('#B3FFFFFF')
}
.width('100%')
.padding({ left: 24, right: 24, bottom: 40 })
.alignItems(HorizontalAlign.Center)
.position({ x: 0, y: '85%' })
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Block)
.onClick((): void => {
this.previewSrc = '';
this.previewName = '';
})
}
/* 负责人浮层(单选) */
if (this.attPickOpen) {
FbPersonPickSheet({
@@ -1231,7 +1324,7 @@ export struct FbTaskPanel {
Text('子任务')
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.fontColor(this.accent)
if (this.kids().length > 0) {
Text(this.kidDoneCount().toString() + '/' + this.kids().length.toString())
.fontSize(FbMetric.fontTiny)
@@ -1319,7 +1412,7 @@ export struct FbTaskPanel {
Text('附件')
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.fontColor(this.accent)
if (this.attachs().length > 0) {
Text(this.attachs().length.toString())
.fontSize(FbMetric.fontTiny)
@@ -1410,6 +1503,10 @@ export struct FbTaskPanel {
.width(64)
.height(64)
.margin({ right: 8, bottom: 8 })
/* #77c:点缩略图 → 图片全屏预览 / 文件交给系统应用打开 */
.onClick((): void => {
this.tapAttach(a);
})
}
/** 留言 */
@@ -1424,7 +1521,7 @@ export struct FbTaskPanel {
Text('留言')
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.fontColor(this.accent)
if (this.comments().length > 0) {
Text(this.comments().length.toString())
.fontSize(FbMetric.fontTiny)
@@ -1438,23 +1535,33 @@ export struct FbTaskPanel {
ForEach(this.comments(), (c: FbTaskComment) => {
Row({ space: 8 }) {
/* 头像位:一个小色点(端上没拉头像,用底色 + 名字首字最直观) */
Row() {
Text(c.name.substring(0, 1))
.fontSize(10)
.fontColor(this.accent)
}
.width(22)
.height(22)
.borderRadius(11)
.backgroundColor(c.color !== '' ? c.color : FbColors.inputBg)
.justifyContent(FlexAlign.Center)
/* 🔴 #77e(用户 2026-11-05):「留言里面的人,前面还是没有头像,需要补充。」
⇒ 原来这里画的是"名字首字色块"(当时端上确实没拉头像)。
现在 `FbTaskComment.avatar` 有了(服务端 SQL 一直返回 p.avatar,
端上原来没解析),换成 `FbAvatar`:
有头像图用图、没设头像才退回首字色块。
⚠️ 用公共 `FbAvatar` 而不是自己拼 `Image`:全站头像的口径
(尺寸/圆角/兜底色)都收在那一个组件里,自己拼迟早不一致。 */
FbAvatar({
name: c.name,
color: c.color !== '' ? c.color : '#A84A28',
dia: 24,
fontSizeVp: 10,
imageUrl: c.avatar !== ''
? fbImageSrc(c.avatar, this.token()) : ''
})
Column({ space: 2 }) {
Row({ space: 6 }) {
/* 🔴 #77e(用户 2026-11-05):「根据我们之前样式的规定,
它的标题和里面的文字,应该按照现在这个模块的主题色加粗显示。」
⇒ 留言人名用**模块主题色 + 加粗**(原来是 muted 灰、常规字重)。
这是全站「小节标题/发言人以模块色标识」的既有规矩
(观影用紫、旅程用绿、任务用赭红…),这里原来漏了。 */
Text(c.name)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.fontWeight(FontWeight.Medium)
.fontColor(this.accent)
Text(c.createdAt)
.fontSize(9)
.fontColor(FbColors.faint)
@@ -1462,6 +1569,11 @@ export struct FbTaskPanel {
.width('100%')
.alignItems(VerticalAlign.Center)
/* 正文也按主题色加粗?——**不**。
用户说的是"标题和里面的文字",但正文若整段主题色,
简明度会掉(色彩是需要留白的)。
⇒ 名字(=发言标题)主题色加粗;正文保持 ink 正文色。
这条取舍写在 commit 里,若用户要求正文也改色,一行就能调。 */
Text(c.content)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
+4
View File
@@ -199,6 +199,8 @@ interface RawTaskKid {
interface RawTaskComment {
id?: number; person_id?: number; content?: string; created_at?: string;
name?: string | null; my_nickname?: string | null; avatar_color?: string | null;
/* #77d:留言人头像 key */
avatar?: string | null;
}
interface RawTaskAttach {
@@ -1033,6 +1035,8 @@ export function parseTaskDetail(json: string): FbTaskDetail | null {
name: nick !== '' ? nick : s(c.name),
color: s(c.avatar_color),
createdAt: s(c.created_at),
/* #77d:留言人头像(服务端 SQL 里一直有 p.avatar,端上原来没取) */
avatar: s(c.avatar),
};
comments.push(one);
}
+11
View File
@@ -899,6 +899,17 @@ export interface FbTaskComment {
name: string;
color: string;
createdAt: string;
/**
* 留言人头像 key(裸 key,渲染走 `fbImageSrc`)。
*
* 🔴 #77d(用户 2026-11-05):「留言里面的人,前面还是没有头像,需要补充。」
*
* ⚠️ 服务端 `Task::comments()` 的 SQL 里**一直有** `p.avatar`
* (`SELECT m.*, p.name, p.avatar, p.avatar_color, ...`),
* 端上 `parseTaskComment` 只取了 name/color ⇒ 头像从来没显示过。
* 空串 = 没设头像,UI 用首字色块兜底。
*/
avatar: string;
}
/** 任务下的一个附件 */