From ad25792732171e99de44e0fc6ede648d7f9a74d4 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 5 Nov 2026 19:19:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=89=E4=BA=BA=E6=94=B9=E3=80=8C=E5=8F=AA?= =?UTF-8?q?=E7=95=99=E6=90=9C=E7=B4=A2=E6=A1=86=E3=80=8D+=20=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E7=94=A8=E7=B3=BB=E7=BB=9F=E6=89=93=E5=BC=80=20+=20?= =?UTF-8?q?=E7=95=99=E8=A8=80=E5=A4=B4=E5=83=8F=E4=B8=8E=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E8=89=B2=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 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 改成主题色。 ⚠️ 正文**没有**跟着改主题色:整段上色会掉可读性(色彩需要留白)。 如果用户要求正文也变色,改一行即可(已在代码注释里标了位置)。 ⑤ 编译通过,已装真机。 --- .../main/ets/components/FbPersonPickSheet.ets | 36 ++++- entry/src/main/ets/components/FbTaskPanel.ets | 144 ++++++++++++++++-- entry/src/main/ets/data/FbParse.ets | 4 + entry/src/main/ets/model/Types.ets | 11 ++ 4 files changed, 178 insertions(+), 17 deletions(-) diff --git a/entry/src/main/ets/components/FbPersonPickSheet.ets b/entry/src/main/ets/components/FbPersonPickSheet.ets index 035a72c..82cfbb6 100644 --- a/entry/src/main/ets/components/FbPersonPickSheet.ets +++ b/entry/src/main/ets/components/FbPersonPickSheet.ets @@ -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) diff --git a/entry/src/main/ets/components/FbTaskPanel.ets b/entry/src/main/ets/components/FbTaskPanel.ets index 9162642..1c32c24 100644 --- a/entry/src/main/ets/components/FbTaskPanel.ets +++ b/entry/src/main/ets/components/FbTaskPanel.ets @@ -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 { + 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) diff --git a/entry/src/main/ets/data/FbParse.ets b/entry/src/main/ets/data/FbParse.ets index 078cb82..f01690e 100644 --- a/entry/src/main/ets/data/FbParse.ets +++ b/entry/src/main/ets/data/FbParse.ets @@ -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); } diff --git a/entry/src/main/ets/model/Types.ets b/entry/src/main/ets/model/Types.ets index 53afabc..5580332 100644 --- a/entry/src/main/ets/model/Types.ets +++ b/entry/src/main/ets/model/Types.ets @@ -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; } /** 任务下的一个附件 */