留言底部被裁 + 留言支持附件 (#77)

用户 2026-11-05:
  「留言输入框下面还有东西,现在被挡住了,根本看不见,也滚不动了。
   但下面应该还有字什么的,看看是什么。」
  「留言是可以需要添加附件的。」

① 被挡住的是什么
   是 #77 刚加的「回车发送」那行小字。

   根因:`commentSection()` 是 `Scroll` 里**最后一个**子元素,
   而 Scroll 内容末尾只留了 `height(6)`。
   Scroll 本身是 `flexShrink(1)`(内容不够高时按内容收缩),
   最后一行下边距一旦小于"卡片 padding + footBar 高度",
   就会被判定为"已到底" —— 滚不过去、也看不见。
   ⇒ 底部留白从 6 改成 28。

② 留言带附件
   服务端网页端早就有(tasks/comment_add 里的 attach_save_files),
   但 **API 的 comments 端点只收 content**,且空文字直接 422
   ⇒ 端上根本没有传附件的入口。三处一起改:

   · app/ApiHandler.php:comments POST 支持 multipart
       - 有 file 就走附件流程:先 addComment 拿 cid,再 Attachment::save
         (顺序不能反:save 要 owner_id,cid 是插入后才有的)
       - kind 用 task_comment,与网页端 attach_save_files 同一归属类型
         (不同的话同一条留言的附件在两端互相看不见)
       - **允许只发附件不写字**:自动补占位文案「(附件)」,与网页端同口径
   · FbTaskRepo.addTaskCommentWithFile():手拼 multipart
       - 不能复用 fbUploadOne(那个是"一个文件 + 一个字段",
         评论要同时传 content + file)
       - 文件段必须带 Content-Type:服务端按它判"是不是图片"
       - boundary 之间用 \r\n(用 \n 有些解析器会把整段当一个字段,
         表现为"文字上去了、附件不见了")
   · FbTaskPanel:
       - 「+ 添加附件」按钮(DocumentViewPicker,任意文件)
       - 选中的文件名显示成 chip,可点 ✕ 去掉
       - 附件**不立刻上传**,随留言一起发(与「任务附件」那块语义不同:
         那个传完就生效)
       - 发成功后清空待发附件,否则下一条会重复带上

   实测(模拟上传链路):
     只有文字          => cid=18 ✅
     文字 + 附件        => cid=19,附件数 1 ✅(key=files/6b4e...txt)
     只发附件(占位)   => cid=20 ✅
   测试数据已清理。
This commit is contained in:
2026-11-05 19:25:32 +08:00
parent ad25792732
commit 32a35ca222
2 changed files with 277 additions and 15 deletions
+155 -14
View File
@@ -47,8 +47,7 @@ import { fbImageSrc } from '../data/FbAttachQueue';
import {
addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask,
setTaskStatus, uploadTaskAttach, FbTaskDraft,
fetchTaskShares, shareTask, unshareTask,
} from '../data/FbTaskRepo';
fetchTaskShares, shareTask, unshareTask, addTaskCommentWithFile } from '../data/FbTaskRepo';
import { parseTaskDetail } from '../data/FbParse';
import { picker } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
@@ -129,6 +128,13 @@ export struct FbTaskPanel {
/* #77c:附件预览浮层 —— 存当前要全屏看的那张图的地址,空 = 没开 */
@Local previewSrc: string = '';
@Local previewName: string = '';
/* #77g:留言待发附件(URI + 显示名)。
⚠️ 只存**一个**:服务端评论附件接口一次收一个 `file`。
要支持多个得改 API 成数组,用户没提,先按单个做(够用且简单)。 */
@Local cmtAttachUri: string = '';
@Local cmtAttachNames: string[] = [];
@Local cmtBusy: boolean = false;
// 共享(工单 #26)
@Local roster: FbRosterPerson[] = [];
@Local pickingShare: boolean = false;
@@ -583,21 +589,90 @@ export struct FbTaskPanel {
this.onChanged();
}
/** 选一个附件挂到待发留言上(#77g)——**不立刻上传**,随留言一起发 */
private async pickCmtAttach(): Promise<void> {
if (this.cmtBusy) {
return;
}
try {
const opts: picker.DocumentSelectOptions = new picker.DocumentSelectOptions();
opts.maxSelectNumber = 1;
const p: picker.DocumentViewPicker = new picker.DocumentViewPicker();
const uris: string[] = await p.select(opts);
if (uris.length === 0) {
return;
}
const u: string = uris[0];
const slash: number = u.lastIndexOf('/');
const nm: string = slash >= 0 && slash < u.length - 1 ? u.substring(slash + 1) : '附件';
this.cmtAttachUri = u;
/* ⚠️ 用**新数组**赋值:`@Local` 只代理第一层,
push 到原数组不会触发刷新(本项目踩过多次)。 */
this.cmtAttachNames = [nm];
this.note = '';
} catch (e) {
const be: BusinessError = e as BusinessError;
hilog.warn(0x0000, 'FamilyBook', '选留言附件失败:%{public}s', JSON.stringify(be));
this.note = '选文件失败,请再试一次。';
}
}
/** 去掉待发附件 */
private dropCmtAttach(idx: number): void {
this.cmtAttachUri = '';
this.cmtAttachNames = [];
}
/** 发一条留言 */
/**
* 发一条留言(#77g:支持带附件)。
*
* 🔴 用户 2026-11-05:「留言是可以需要添加附件的。」
*
* 两条路径:
* · 没附件 → 原来的 `addTaskComment()`(JSON,轻)
* · 有附件 → `addTaskCommentWithFile()`(multipart:content + file)
*
* ⚠️ **允许只发附件不写字**:拍张照片直接发是常见用法
* (服务端也认,会自动补占位文案「(附件)」)。
* 所以判空从"文字为空就 return"改成"文字和附件都为空才 return"。
* ⚠️ 发成功后要清掉待发附件,否则下一条留言会重复带上这张图。
*/
private async sendComment(): Promise<void> {
const txt: string = this.cmtDraft.trim();
if (this.busy || txt === '') {
const hasFile: boolean = this.cmtAttachUri !== '';
if (this.busy || this.cmtBusy) {
return;
}
if (txt === '' && !hasFile) {
return;
}
this.busy = true;
this.cmtBusy = true;
this.note = '';
const res: FbApiResult = await addTaskComment(this.token(), this.taskId, txt);
this.busy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '留言没发出去。';
return;
if (hasFile) {
const err: string = await addTaskCommentWithFile(
this.token(), this.taskId, txt, this.cmtAttachUri);
this.busy = false;
this.cmtBusy = false;
if (err !== '') {
this.note = err;
return; // 附件留着,让用户能重试
}
} else {
const res: FbApiResult = await addTaskComment(this.token(), this.taskId, txt);
this.busy = false;
this.cmtBusy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '留言没发出去。';
return;
}
}
this.cmtDraft = '';
this.cmtAttachUri = '';
this.cmtAttachNames = [];
await this.reload();
this.onChanged();
}
@@ -730,7 +805,18 @@ export struct FbTaskPanel {
.width('100%')
.padding({ top: 10 })
}
Row().height(6)
/* 🔴 #77g(用户 2026-11-05):「留言输入框下面还有东西,
现在被挡住了,根本看不见,也滚不动了。」
—— 被挡住的是 `commentSection()` 末尾那行「回车发送」小字。
根因:`commentSection` 是 `Scroll` 里的**最后一个**子元素,
而这里原来只留了 `height(6)`。
`Scroll` 又是 `flexShrink(1)`(内容不够高时按内容收缩),
最后一行的下边距一旦小于"卡片 padding + footBar 高度",
就会被判定为"已到底",滚不过去也看不见。
⇒ 底部留一块**足够高**的空白(不是 6vp 意思一下),
保证最后一行能滚到可视区中间。 */
Row().height(28)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
@@ -1634,12 +1720,67 @@ export struct FbTaskPanel {
.width('100%')
.alignItems(VerticalAlign.Center)
/* 「回车发送」提示:放在输入行**下方**,小字、不抢视线 */
Text('回车发送')
.fontSize(10)
.fontColor(FbColors.faint)
/* 🔴 #77g(用户 2026-11-05):「留言是可以需要添加附件的。」
服务端**早就支持** —— `tasks/comment_add` 里一直有
`attach_save_files($familyId, 'task_comment', $cid, 'attach', $me)`
(网页端那个表单也已经有 file input),只是**鸿蒙端从来没传过附件**。
⇒ `addTaskComment()` 目前只发 JSON(content 一个字段),
要带附件必须改成 multipart 上传 —— 见 FbTaskRepo 的改动。
⚠️ 交互设计:附件是"随这条留言一起发出",不是立刻上传
(与「任务附件」那块不同 —— 那个传完就生效)。
所以选中的文件先挂在本地,点「发送」时和文字一起提交。
⚠️ 选中的文件名要显示出来,否则用户不知道有没有选上。 */
if (this.cmtAttachNames.length > 0) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.cmtAttachNames, (n: string, i: number) => {
Row({ space: 4 }) {
Text('📎')
.fontSize(9)
Text(n)
.fontSize(9)
.fontColor(FbColors.muted)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.constraintSize({ maxWidth: 120 })
Text('✕')
.fontSize(9)
.fontColor(FbColors.faint)
.onClick((): void => {
this.dropCmtAttach(i);
})
}
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.margin({ right: 6, bottom: 4 })
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusPill)
.alignItems(VerticalAlign.Center)
}, (n: string, i: number): string => 'ca_' + i.toString() + '_' + n)
}
.width('100%')
.padding({ top: 2 })
.padding({ top: 6 })
}
/* 「回车发送」+「添加附件」同一行:一个说明、一个动作 */
Row({ space: 8 }) {
Text('回车发送')
.fontSize(10)
.fontColor(FbColors.faint)
.layoutWeight(1)
Text(this.cmtBusy ? '发送中…' : '+ 添加附件')
.fontSize(FbMetric.fontTiny)
.fontColor(this.cmtBusy ? FbColors.faint : this.accent)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(fbAlpha(this.accent, 0.10))
.borderRadius(FbMetric.radiusPill)
.onClick((): void => {
this.pickCmtAttach();
})
}
.width('100%')
.padding({ top: 6 })
.alignItems(VerticalAlign.Center)
}
.width('100%')
.alignItems(HorizontalAlign.Start)