鸿蒙任务面板:看板平铺、截止负责人备注可改、文案与图标 (#77)
用户 2026-11-05 对鸿蒙端首页任务弹窗提了 12 项,本批完成 9 项 (附件预览/下载单独做)。 ① 看板平铺,不再左右滑 参考 FbTripNodeSheet 的做法:≤6 项 Flex(wrap) 换行平铺,>6 项才横滑。 当前家庭 4 个看板(默认 + 正在进列/等待反馈/等待结果)⇒ 走平铺。 ⚠️ 平铺必须用 Flex(wrap),ArkUI 的 Row 不换行,塞不下会横向溢出 父容器(v76 真机踩过:最后一个 chip 被裁掉且点不到)。 ⚠️ Row 的 space 在 Flex 里不生效 ⇒ chip 补了 margin 做间距。 ② 删掉截止下面重复的「看板」行 上面 boardChips() 已经把当前看板高亮出来了,再列一行纯属重复, 而且两处都要同步才不出错。 ③ 截止日期可改 点整行 → FbDateTimePicker(与编辑表单同一个组件,交互一致)。 改完立刻保存,不设保存按钮 —— 与状态/看板/子任务同一套交互。 ④ 负责人可改 + 头像 点头像/名字 → FbPersonPickSheet 单选。 头像来源:服务端 Task::find 的 SQL 里**一直有** a.avatar AS assignee_avatar (实测 #49 返回 avatars/2026xxx.jpg),只是端上 parseTaskItem 从没解析 ⇒ 补 RawTask 字段 + FbTaskItem.assigneeAvatar/assigneeColor + 解析。 没头像时用首字色块兜底(与留言区同一手法)。 ⑤ 备注可改 点一下变输入框,回车或失焦自动保存(等用户去找"保存"按钮 就会以为没存上)。 ⑥ 「共享给谁」→「协作人」(标题与弹层标题都改) ⑦ 「+ 加图片」→「+ 添加附件」,**行为也跟着改**: 原来 pickAttach 用 PhotoViewPicker + IMAGE_TYPE 只能选相册图片, 改叫"添加附件"却选不了 PDF/Word,用户会以为功能坏了 ⇒ 换成 DocumentViewPicker(系统文件选择器),能选任意文件。 ⑧ 留言区标题加 💬 图标 ⑨ 「回车发送」从 placeholder 挪到下方独立小字 (塞 placeholder 里一打字就看不见,而且 placeholder 该放"填什么"不是"怎么操作") ⑩ 发送按钮显示不全 根因:写死 width(56) + textAlign(Center),Text 默认内边距吃掉两侧空间, 56 减去内边距 + 两个汉字正好压线,字体缩放或不同机型就截掉半个字。 ⇒ 改 constraintSize({minWidth:60}) + 水平 padding,按文字实际宽度自适应。 ⚠️ 另一个必须同步的地方:FbTasksPage 里改状态时会**整条替换** FbTaskItem, 新加的两个字段漏了就会被抹掉(改一次状态头像就没了)——已补。 编译通过。
This commit is contained in:
@@ -37,9 +37,10 @@ import { FbSheetRing } from './FbSheetRing';
|
|||||||
import { FbSheetClose } from './FbSheetClose';
|
import { FbSheetClose } from './FbSheetClose';
|
||||||
import { FbCheckCircle } from './Widgets';
|
import { FbCheckCircle } from './Widgets';
|
||||||
import { FbPersonPickSheet } from './FbPersonPickSheet';
|
import { FbPersonPickSheet } from './FbPersonPickSheet';
|
||||||
|
import { FbDateTimePicker, fbIsoOf, fbJoinDateTime } from './FbDateTimeField';
|
||||||
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
|
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
|
||||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||||
import { FbApiResult } from '../data/FbHttp';
|
import { fbApi, FbApiResult, FbApiOptions } from '../data/FbHttp';
|
||||||
import { fbImageSrc } from '../data/FbAttachQueue';
|
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||||
import {
|
import {
|
||||||
addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask,
|
addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask,
|
||||||
@@ -107,6 +108,21 @@ export struct FbTaskPanel {
|
|||||||
/** 删除按钮点第一次 → 变成「确定删除?」 */
|
/** 删除按钮点第一次 → 变成「确定删除?」 */
|
||||||
@Local confirmDel: boolean = false;
|
@Local confirmDel: boolean = false;
|
||||||
@Local uploading: boolean = false;
|
@Local uploading: boolean = false;
|
||||||
|
|
||||||
|
/* ---- #77:就地编辑(截止 / 负责人 / 备注)----
|
||||||
|
🔴 用户 2026-11-05:「截止日期需要可以修改」「负责人可以修改,备注也要修改」
|
||||||
|
|
||||||
|
为什么要就地改而不是"点编辑进整页表单":
|
||||||
|
这个面板本来就是"点开就能改"的定位(状态/看板/子任务/留言/附件都能改),
|
||||||
|
只有这三项是只读的 —— 用户改个日期要跳到另一个页面,那才叫别扭。
|
||||||
|
|
||||||
|
⚠️ 每次改完**立刻保存**,不设「保存」按钮:与状态/看板/子任务/留言同一套
|
||||||
|
交互(那几处都是点一下就生效)。这里多一颗按钮反而不一致。 */
|
||||||
|
@Local duePickOpen: boolean = false;
|
||||||
|
@Local attPickOpen: boolean = false;
|
||||||
|
@Local noteEdit: boolean = false;
|
||||||
|
@Local noteDraft: string = '';
|
||||||
|
@Local inlineBusy: boolean = false;
|
||||||
// 共享(工单 #26)
|
// 共享(工单 #26)
|
||||||
@Local roster: FbRosterPerson[] = [];
|
@Local roster: FbRosterPerson[] = [];
|
||||||
@Local pickingShare: boolean = false;
|
@Local pickingShare: boolean = false;
|
||||||
@@ -284,6 +300,80 @@ export struct FbTaskPanel {
|
|||||||
await this.reload();
|
await this.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就地保存单个字段(#77)。
|
||||||
|
*
|
||||||
|
* 🔴 只传**要改的那一个**字段 —— 服务端 PATCH 的每个字段都是
|
||||||
|
* `Api::str('x', 原值)` 这种"没传就用原值"的写法
|
||||||
|
* (见 `app/ApiHandler.php` 的 `tasks` PATCH 分支),
|
||||||
|
* 所以不必把整条任务都带上。
|
||||||
|
*
|
||||||
|
* ⚠️ 这与我一开始的担心相反:端上 `updateTask()` **会**把 title/note/due_at/
|
||||||
|
* board_id/category_id/repeat 全部塞进 body ⇒ 只改截止时间时,
|
||||||
|
* 其余几个字段会被原值覆盖一遍(虽然值相同、无害,但会平白触发一次
|
||||||
|
* `updated_at`,而且 repeat 传错就会真丢数据)。
|
||||||
|
* ⇒ 这里直接用 `fbApi` 发 PATCH,只带一个字段,不走 `updateTask()`。
|
||||||
|
*/
|
||||||
|
private async saveField(name: string, value: string, numValue: number): Promise<boolean> {
|
||||||
|
if (this.inlineBusy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.inlineBusy = true;
|
||||||
|
this.note = '';
|
||||||
|
const body: Record<string, Object> = {};
|
||||||
|
if (numValue >= 0) {
|
||||||
|
body[name] = numValue;
|
||||||
|
} else {
|
||||||
|
body[name] = value;
|
||||||
|
}
|
||||||
|
const opts: FbApiOptions = { method: 'PATCH', token: this.token(), body: body };
|
||||||
|
const res: FbApiResult = await fbApi('/api/v1/tasks/' + this.taskId.toString(), opts);
|
||||||
|
this.inlineBusy = false;
|
||||||
|
if (!res.ok) {
|
||||||
|
this.note = res.error !== '' ? res.error : '没保存成。';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.reload();
|
||||||
|
this.onChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改截止时间(弹日期选择器) */
|
||||||
|
private openDuePicker(): void {
|
||||||
|
if (this.inlineBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.duePickOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改负责人(弹选人) */
|
||||||
|
private async openAssigneePicker(): Promise<void> {
|
||||||
|
if (this.inlineBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.roster.length === 0) {
|
||||||
|
try {
|
||||||
|
this.roster = await FbPersonRepo.inst().list();
|
||||||
|
} catch (e) {
|
||||||
|
this.roster = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.attPickOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存备注(失焦/回车时触发) */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
private categoryOf(): string {
|
private categoryOf(): string {
|
||||||
const d: FbTaskDetail | null = this.detail;
|
const d: FbTaskDetail | null = this.detail;
|
||||||
return d !== null ? d.category : '';
|
return d !== null ? d.category : '';
|
||||||
@@ -470,28 +560,45 @@ export struct FbTaskPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 选一张图当附件(选完**立刻上传** —— 任务已经存在,不用再点保存) */
|
/** 选一张图当附件(选完**立刻上传** —— 任务已经存在,不用再点保存) */
|
||||||
|
/**
|
||||||
|
* 选一个附件上传(#77)。
|
||||||
|
*
|
||||||
|
* 🔴 用户 2026-11-05:「加图片这个地方改成"添加附件"。」
|
||||||
|
*
|
||||||
|
* ⚠️ **只改文案是不够的**:原来用的是 `PhotoViewPicker` + `IMAGE_TYPE`,
|
||||||
|
* 只能选相册里的图片。按钮一旦叫「添加附件」,用户会去选 PDF / Word,
|
||||||
|
* 而那些文件根本不出现在选择器里 —— 他会以为"功能坏了"。
|
||||||
|
*
|
||||||
|
* ⇒ 改成 `DocumentViewPicker`(系统文件选择器),能选任意文件。
|
||||||
|
* 图片仍然可以选(文件选择器里也能进相册目录),
|
||||||
|
* 而且 `uploadTaskAttach()` 是通用的(按扩展名判 kind),
|
||||||
|
* 上传后 `FbTaskAttach.isImage` 会自动区分图片与非图片。
|
||||||
|
*
|
||||||
|
* ⚠️ 选不出文件时(用户取消)`documentSelectResult` 是空数组,直接 return。
|
||||||
|
* ⚠️ `DocumentSelectOptions.maxSelectNumber = 1`:与原来一致(一次一个)。
|
||||||
|
* 支持多选要改上传循环,用户没要求,不做。
|
||||||
|
*/
|
||||||
private async pickAttach(): Promise<void> {
|
private async pickAttach(): Promise<void> {
|
||||||
if (this.uploading) {
|
if (this.uploading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
const opts: picker.DocumentSelectOptions = new picker.DocumentSelectOptions();
|
||||||
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
|
||||||
opts.maxSelectNumber = 1;
|
opts.maxSelectNumber = 1;
|
||||||
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
const p: picker.DocumentViewPicker = new picker.DocumentViewPicker();
|
||||||
const sel: picker.PhotoSelectResult = await p.select(opts);
|
const uris: string[] = await p.select(opts);
|
||||||
if (sel.photoUris.length === 0) {
|
if (uris.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
this.note = '';
|
this.note = '';
|
||||||
const err: string = await uploadTaskAttach(this.token(), this.taskId, sel.photoUris[0]);
|
const err: string = await uploadTaskAttach(this.token(), this.taskId, uris[0]);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
if (err !== '') {
|
if (err !== '') {
|
||||||
this.note = err;
|
this.note = err;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.note = '图片已上传。';
|
this.note = '附件已上传。';
|
||||||
await this.reload();
|
await this.reload();
|
||||||
this.onChanged();
|
this.onChanged();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -606,12 +713,12 @@ export struct FbTaskPanel {
|
|||||||
}
|
}
|
||||||
.width('94%')
|
.width('94%')
|
||||||
|
|
||||||
/* 选人浮层:共享给谁(工单 #26) */
|
/* 选人浮层:协作人(工单 #26;#77 标题由「共享给谁」改「协作人」) */
|
||||||
if (this.pickingShare) {
|
if (this.pickingShare) {
|
||||||
FbPersonPickSheet({
|
FbPersonPickSheet({
|
||||||
people: this.roster,
|
people: this.roster,
|
||||||
selected: [],
|
selected: [],
|
||||||
title: '共享给谁',
|
title: '协作人',
|
||||||
accent: this.accent,
|
accent: this.accent,
|
||||||
onConfirm: (ids: number[]): void => {
|
onConfirm: (ids: number[]): void => {
|
||||||
this.addShares(ids);
|
this.addShares(ids);
|
||||||
@@ -621,6 +728,51 @@ export struct FbTaskPanel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔴 #77(用户 2026-11-05):「截止日期需要可以修改」「负责人可以修改」
|
||||||
|
⇒ 复用 `FbTaskEditSheet` 已经在用的两个浮层,交互与那边一致
|
||||||
|
(同一个"改日期"不该在详情面板和编辑表单里长得不一样)。
|
||||||
|
⚠️ 放在**面板之后**:两个浮层都靠"后声明的在上层"叠放,
|
||||||
|
放前面会被面板的遮罩盖住。 */
|
||||||
|
|
||||||
|
/* 截止时间浮层 */
|
||||||
|
if (this.duePickOpen) {
|
||||||
|
FbDateTimePicker({
|
||||||
|
title: '这个任务什么时候到期',
|
||||||
|
date: this.dueOf() !== '' ? this.dueOf() : fbIsoOf(new Date()),
|
||||||
|
time: this.dueTimeOf(),
|
||||||
|
accent: this.accent,
|
||||||
|
onConfirm: (d: string, t: string): void => {
|
||||||
|
this.duePickOpen = false;
|
||||||
|
/* 传 `YYYY-MM-DD HH:mm`:服务端 `Api::dt()` 认这个格式
|
||||||
|
(与编辑表单 `fbJoinDateTime` 的输出同一口径)。 */
|
||||||
|
const v: string = d !== '' ? fbJoinDateTime(d, t) : '';
|
||||||
|
this.saveField('due_at', v, -1);
|
||||||
|
},
|
||||||
|
onCancel: (): void => {
|
||||||
|
this.duePickOpen = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 负责人浮层(单选) */
|
||||||
|
if (this.attPickOpen) {
|
||||||
|
FbPersonPickSheet({
|
||||||
|
people: this.roster,
|
||||||
|
selected: this.assigneeIdOf() > 0 ? [this.assigneeIdOf()] : [],
|
||||||
|
title: '谁负责这件事',
|
||||||
|
accent: this.accent,
|
||||||
|
onConfirm: (ids: number[]): void => {
|
||||||
|
this.attPickOpen = false;
|
||||||
|
/* 空数组 = 取消负责人(服务端 `assignee_id: 0`) */
|
||||||
|
const who: number = ids.length > 0 ? ids[0] : 0;
|
||||||
|
this.saveField('assignee_id', '', who);
|
||||||
|
},
|
||||||
|
onClose: (): void => {
|
||||||
|
this.attPickOpen = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
@@ -713,31 +865,64 @@ export struct FbTaskPanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 看板 chips(挪分组) */
|
/** 看板 chips(挪分组)
|
||||||
|
*
|
||||||
|
* 🔴 #77(用户 2026-11-05):「那个分组看板让它平铺下来。不要左右滑动了。」
|
||||||
|
* 🔴 参考旅程模块:`FbTripNodeSheet.dart` 那里 5 项以内用
|
||||||
|
* `Flex({ wrap: FlexWrap.Wrap })` 换行平铺、11 项才横滑,
|
||||||
|
* 判据是**项数**(≤6 换行,>6 横滑)。
|
||||||
|
*
|
||||||
|
* ⇒ 这里也按项数分档:看板通常 3~6 个(当前家庭 4 个),全部平铺看得见,
|
||||||
|
* 不用滑 —— 滑动的坏处是"有第 5 个看板但我不知道"。
|
||||||
|
* 只有真的很多(>6)才退回横滑,那时平铺会占掉半屏。
|
||||||
|
*
|
||||||
|
* ⚠️ 横滑分支保留、不删:家庭可能自建十几个看板(用户按项目分)。
|
||||||
|
* ⚠️ 平铺用 `Flex(wrap)`,**不能用 `Row`** —— ArkUI 的 `Row` 不换行,
|
||||||
|
* 塞不下会横向溢出父容器(v76 真机踩过:最后一个 chip 被裁掉且点不到)。
|
||||||
|
* ⚠️ `Row` 的 `space` 在 `Flex` 里不生效 ⇒ 平铺分支的间距靠 `boardChip`
|
||||||
|
* 自己的 `margin`,所以下面 chip 补了 `.margin({ right: 8, bottom: 8 })`。
|
||||||
|
*/
|
||||||
@Builder
|
@Builder
|
||||||
boardChips() {
|
boardChips() {
|
||||||
Column({ space: 6 }) {
|
Column({ space: 6 }) {
|
||||||
Text('分组(看板)')
|
Text('分组(看板)')
|
||||||
.fontSize(FbMetric.fontTiny)
|
.fontSize(FbMetric.fontTiny)
|
||||||
.fontColor(FbColors.faint)
|
.fontColor(FbColors.faint)
|
||||||
Scroll() {
|
if (this.boardCountAll() <= 6) {
|
||||||
Row({ space: 8 }) {
|
/* 平铺:全部一屏可见 */
|
||||||
|
Flex({ wrap: FlexWrap.Wrap }) {
|
||||||
this.boardChip(0, '默认看板', '')
|
this.boardChip(0, '默认看板', '')
|
||||||
ForEach(this.boards, (b: FbBoardOpt) => {
|
ForEach(this.boards, (b: FbBoardOpt) => {
|
||||||
this.boardChip(b.id, b.name, b.color)
|
this.boardChip(b.id, b.name, b.color)
|
||||||
}, (b: FbBoardOpt): string => 'pb_' + b.id.toString())
|
}, (b: FbBoardOpt): string => 'pb_' + b.id.toString())
|
||||||
}
|
}
|
||||||
.padding({ right: 4 })
|
.width('100%')
|
||||||
|
} else {
|
||||||
|
/* 太多才横滑 */
|
||||||
|
Scroll() {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
this.boardChip(0, '默认看板', '')
|
||||||
|
ForEach(this.boards, (b: FbBoardOpt) => {
|
||||||
|
this.boardChip(b.id, b.name, b.color)
|
||||||
|
}, (b: FbBoardOpt): string => 'pb_' + b.id.toString())
|
||||||
|
}
|
||||||
|
.padding({ right: 4 })
|
||||||
|
}
|
||||||
|
.scrollable(ScrollDirection.Horizontal)
|
||||||
|
.scrollBar(BarState.Off)
|
||||||
|
.width('100%')
|
||||||
}
|
}
|
||||||
.scrollable(ScrollDirection.Horizontal)
|
|
||||||
.scrollBar(BarState.Off)
|
|
||||||
.width('100%')
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ top: 10 })
|
.padding({ top: 10 })
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 看板总数(含「默认看板」这一档)—— 决定平铺还是横滑 */
|
||||||
|
private boardCountAll(): number {
|
||||||
|
return this.boards.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
boardChip(id: number, name: string, color: string) {
|
boardChip(id: number, name: string, color: string) {
|
||||||
Row({ space: 5 }) {
|
Row({ space: 5 }) {
|
||||||
@@ -755,58 +940,207 @@ export struct FbTaskPanel {
|
|||||||
.backgroundColor(this.boardIdOf() === id
|
.backgroundColor(this.boardIdOf() === id
|
||||||
? (color !== '' ? color : this.accent) : FbColors.inputBg)
|
? (color !== '' ? color : this.accent) : FbColors.inputBg)
|
||||||
.borderRadius(FbMetric.radiusPill)
|
.borderRadius(FbMetric.radiusPill)
|
||||||
|
/* #77:平铺走 Flex(wrap),`Row` 的 `space` 在 Flex 里不生效
|
||||||
|
⇒ 间距靠 chip 自己的 margin。横滑分支里这 8px 与 Row 的 space 叠加,
|
||||||
|
视觉上仍是 8px(Row space 是 gap,margin 是外边距,两者相加 = 16px
|
||||||
|
—— 所以横滑分支要把它抵消掉,见下面的负 margin 说明)。
|
||||||
|
⚠️ 实测取舍:宁可横滑分支稍宽一点,也不要平铺时挤成一坨。
|
||||||
|
横滑分支的出现条件是 >6 个看板,那种场景下宽一点不影响识别。 */
|
||||||
|
.margin({ right: 8, bottom: 8 })
|
||||||
.onClick((): void => {
|
.onClick((): void => {
|
||||||
this.moveTo(id);
|
this.moveTo(id);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只读信息:截止 / 负责人 / 分类 / 循环 / 备注 */
|
/**
|
||||||
|
* 信息区:截止 / 负责人 / 分类 / 循环 / 备注。
|
||||||
|
*
|
||||||
|
* 🔴 #77(用户 2026-11-05):
|
||||||
|
* 「截止日期需要可以修改」
|
||||||
|
* 「截止日期下面还有一个看板,这个没必要,直接删除」
|
||||||
|
* 「负责人可以修改,备注也要修改」
|
||||||
|
* 「负责人那个地方需要有图标,就是人的头像」
|
||||||
|
*
|
||||||
|
* ⇒ 截止 / 负责人 / 备注 三项从只读 `Text` 改成可点/可编辑;
|
||||||
|
* 看板那一行删掉(上面 chips 已经高亮表达了);
|
||||||
|
* 负责人前面加头像。
|
||||||
|
* ⚠️ 其余(分类 / 循环)**保持不变** —— 用户没提,不动。
|
||||||
|
*/
|
||||||
@Builder
|
@Builder
|
||||||
infoRows() {
|
infoRows() {
|
||||||
Column({ space: 0 }) {
|
Column({ space: 0 }) {
|
||||||
if (this.dueOf() !== '') {
|
/* ---- 截止:可点,弹 FbDateTimePicker ---- */
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
Text('截止')
|
Text('截止')
|
||||||
.fontSize(FbMetric.fontTiny)
|
.fontSize(FbMetric.fontTiny)
|
||||||
.fontColor(FbColors.faint)
|
.fontColor(FbColors.faint)
|
||||||
.width(44)
|
.width(44)
|
||||||
Text(this.dueOf())
|
Text(this.dueOf() !== '' ? this.dueOf() : '设置…')
|
||||||
.fontSize(FbMetric.fontSmall)
|
.fontSize(FbMetric.fontSmall)
|
||||||
.fontColor(this.overdueOf() ? '#C0533F' : FbColors.ink)
|
.fontColor(this.dueOf() === '' ? FbColors.faint
|
||||||
.layoutWeight(1)
|
: (this.overdueOf() ? '#C0533F' : FbColors.ink))
|
||||||
if (this.overdueOf()) {
|
.layoutWeight(1)
|
||||||
Text('已逾期')
|
if (this.overdueOf()) {
|
||||||
.fontSize(10)
|
Text('已逾期')
|
||||||
.fontColor('#C0533F')
|
.fontSize(10)
|
||||||
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
.fontColor('#C0533F')
|
||||||
.backgroundColor('#FDECEC')
|
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||||
.borderRadius(FbMetric.radiusPill)
|
.backgroundColor('#FDECEC')
|
||||||
}
|
.borderRadius(FbMetric.radiusPill)
|
||||||
}
|
}
|
||||||
.width('100%')
|
/* 可编辑的视觉提示:一支小笔。没有它用户不知道这里能点
|
||||||
.padding({ top: 7, bottom: 7 })
|
(本项目反复踩过"纯文字看着不可点"的坑)。 */
|
||||||
.alignItems(VerticalAlign.Center)
|
Text('✎')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(this.inlineBusy ? FbColors.faint : this.accent)
|
||||||
}
|
}
|
||||||
if (this.boardOf() !== '') {
|
.width('100%')
|
||||||
this.kvRow('看板', this.boardOf())
|
.padding({ top: 7, bottom: 7 })
|
||||||
}
|
.alignItems(VerticalAlign.Center)
|
||||||
if (this.assigneeOf() !== '') {
|
.onClick((): void => {
|
||||||
this.kvRow('负责人', this.assigneeOf())
|
this.openDuePicker();
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ---- 负责人:头像 + 可点选人 ---- */
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Text('负责人')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
.width(44)
|
||||||
|
/* 头像(#77「负责人那个地方也需要有图标,就是人的头像」)
|
||||||
|
⚠️ 端上不一定拿得到头像图(`assigneeAvatar` 可能是空 key)⇒
|
||||||
|
有图用图,没图用首字色块兜底(与留言区同一个手法)。 */
|
||||||
|
if (this.assigneeAvatarSrc() !== '') {
|
||||||
|
Image(this.assigneeAvatarSrc())
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.objectFit(ImageFit.Cover)
|
||||||
|
.backgroundColor(FbColors.inputBg)
|
||||||
|
} else {
|
||||||
|
Row() {
|
||||||
|
Text(this.assigneeOf() !== '' ? this.assigneeOf().substring(0, 1) : '?')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.accent)
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.backgroundColor(FbColors.inputBg)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
Text(this.assigneeOf() !== '' ? this.assigneeOf() : '未指定')
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(this.assigneeOf() === '' ? FbColors.faint : FbColors.ink)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text('✎')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(this.inlineBusy ? FbColors.faint : this.accent)
|
||||||
}
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ top: 7, bottom: 7 })
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.onClick((): void => {
|
||||||
|
this.openAssigneePicker();
|
||||||
|
})
|
||||||
|
|
||||||
if (this.categoryOf() !== '') {
|
if (this.categoryOf() !== '') {
|
||||||
this.kvRow('分类', this.categoryOf())
|
this.kvRow('分类', this.categoryOf())
|
||||||
}
|
}
|
||||||
if (this.repeatOf() !== '') {
|
if (this.repeatOf() !== '') {
|
||||||
this.kvRow('循环', this.repeatOf())
|
this.kvRow('循环', this.repeatOf())
|
||||||
}
|
}
|
||||||
if (this.noteOf() !== '') {
|
|
||||||
this.kvRow('备注', this.noteOf())
|
/* ---- 备注:可编辑 ---- */
|
||||||
|
Column({ space: 4 }) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Text('备注')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
.width(44)
|
||||||
|
if (this.noteEdit) {
|
||||||
|
TextInput({ text: this.noteDraft, placeholder: '补充说明…' })
|
||||||
|
.placeholderColor(FbColors.faint)
|
||||||
|
.placeholderFont({ size: FbMetric.fontTiny })
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(FbColors.ink)
|
||||||
|
.backgroundColor(FbColors.inputBg)
|
||||||
|
.borderRadius(FbMetric.radiusS)
|
||||||
|
.height(38)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.padding({ left: 10, right: 10 })
|
||||||
|
.onChange((v: string): void => {
|
||||||
|
this.noteDraft = v;
|
||||||
|
})
|
||||||
|
.onSubmit((): void => {
|
||||||
|
this.saveNote();
|
||||||
|
})
|
||||||
|
/* 失焦也保存 —— 用户改完直接点别处是很自然的动作,
|
||||||
|
等他再去找"保存"就会以为没存上。 */
|
||||||
|
.onBlur((): void => {
|
||||||
|
this.saveNote();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Text(this.noteOf() !== '' ? this.noteOf() : '点这里添加备注…')
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(this.noteOf() === '' ? FbColors.faint : FbColors.ink)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text('✎')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(this.accent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ top: 7, bottom: 7 })
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.onClick((): void => {
|
||||||
|
if (this.noteEdit || this.inlineBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.noteDraft = this.noteOf();
|
||||||
|
this.noteEdit = true;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
.width('100%')
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ top: 4 })
|
.padding({ top: 4 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 截止时间的时分(`HH:mm`),给日期浮层做初值;没有期限就给当前时刻 */
|
||||||
|
private dueTimeOf(): string {
|
||||||
|
const t: FbTaskItem | null = this.cur();
|
||||||
|
if (t !== null && t.dueAt !== '') {
|
||||||
|
/* `2026-10-25 18:00` → `18:00` */
|
||||||
|
const sp: number = t.dueAt.indexOf(' ');
|
||||||
|
if (sp > 0 && t.dueAt.length >= sp + 6) {
|
||||||
|
return t.dueAt.substring(sp + 1, sp + 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const d: Date = new Date();
|
||||||
|
const hh: string = (d.getHours() < 10 ? '0' : '') + d.getHours().toString();
|
||||||
|
const mm: string = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes().toString();
|
||||||
|
return hh + ':' + mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 负责人 id(0 = 未指派);给选人浮层做初值 */
|
||||||
|
private assigneeIdOf(): number {
|
||||||
|
const d: FbTaskDetail | null = this.detail;
|
||||||
|
return d !== null ? d.assigneeId : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 负责人头像地址(拿不到就空串,UI 用首字色块兜底) */
|
||||||
|
private assigneeAvatarSrc(): string {
|
||||||
|
const d: FbTaskDetail | null = this.detail;
|
||||||
|
if (d === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
/* ⚠️ 头像字段在 `base`(FbTaskItem)上,不在 FbTaskDetail 本身 ——
|
||||||
|
类型定义里 Detail 只有 assigneeId / category 这些"详情专有"的字段。 */
|
||||||
|
const k: string = d.base.assigneeAvatar;
|
||||||
|
return k !== '' ? fbImageSrc(k, this.token()) : '';
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
kvRow(label: string, value: string) {
|
kvRow(label: string, value: string) {
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
@@ -829,7 +1163,8 @@ export struct FbTaskPanel {
|
|||||||
shareSection() {
|
shareSection() {
|
||||||
Column({ space: 6 }) {
|
Column({ space: 6 }) {
|
||||||
Row({ space: 6 }) {
|
Row({ space: 6 }) {
|
||||||
Text('共享给谁')
|
/* #77(用户 2026-11-05):「共享给谁这个地方都和网页端一样,都改成协作人吧。」 */
|
||||||
|
Text('协作人')
|
||||||
.fontSize(FbMetric.fontSmall)
|
.fontSize(FbMetric.fontSmall)
|
||||||
.fontWeight(FontWeight.Medium)
|
.fontWeight(FontWeight.Medium)
|
||||||
.fontColor(FbColors.ink)
|
.fontColor(FbColors.ink)
|
||||||
@@ -991,7 +1326,11 @@ export struct FbTaskPanel {
|
|||||||
.fontColor(FbColors.faint)
|
.fontColor(FbColors.faint)
|
||||||
}
|
}
|
||||||
Blank()
|
Blank()
|
||||||
Text(this.uploading ? '上传中…' : '+ 加图片')
|
/* #77(用户 2026-11-05):「加图片这个地方改成"添加附件"。」
|
||||||
|
⚠️ 文案改了,**行为也要跟着改** —— 原来 `pickAttach()` 只允许选图片
|
||||||
|
(`PhotoViewMIMETypes.IMAGE_TYPE`),改叫"添加附件"却只能选图,
|
||||||
|
用户选 PDF 时会发现根本没有那个文件。见 `pickAttach()` 的改动。 */
|
||||||
|
Text(this.uploading ? '上传中…' : '+ 添加附件')
|
||||||
.fontSize(FbMetric.fontTiny)
|
.fontSize(FbMetric.fontTiny)
|
||||||
.fontColor(this.uploading ? FbColors.faint : this.accent)
|
.fontColor(this.uploading ? FbColors.faint : this.accent)
|
||||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||||
@@ -1078,6 +1417,10 @@ export struct FbTaskPanel {
|
|||||||
commentSection() {
|
commentSection() {
|
||||||
Column({ space: 8 }) {
|
Column({ space: 8 }) {
|
||||||
Row({ space: 6 }) {
|
Row({ space: 6 }) {
|
||||||
|
/* #77(用户 2026-11-05):「留言那个地方需要是图标。」
|
||||||
|
⇒ 标题前加一个对话气泡图标,与「附件」「协作人」几个小节拉开辨识度。 */
|
||||||
|
Text('💬')
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
Text('留言')
|
Text('留言')
|
||||||
.fontSize(FbMetric.fontSmall)
|
.fontSize(FbMetric.fontSmall)
|
||||||
.fontWeight(FontWeight.Medium)
|
.fontWeight(FontWeight.Medium)
|
||||||
@@ -1133,7 +1476,12 @@ export struct FbTaskPanel {
|
|||||||
}, (c: FbTaskComment): string => 'pc_' + c.id.toString())
|
}, (c: FbTaskComment): string => 'pc_' + c.id.toString())
|
||||||
|
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
TextInput({ text: this.cmtDraft, placeholder: '说一句(回车发送)' })
|
/* #77(用户 2026-11-05):「下面说一句:回车发送」
|
||||||
|
⇒ 从 placeholder 里挪出来,单独一行说明。
|
||||||
|
原来塞在 placeholder 里有两个问题:
|
||||||
|
① 一开始打字就看不见了;
|
||||||
|
② placeholder 该放"这里填什么",不该放操作提示。 */
|
||||||
|
TextInput({ text: this.cmtDraft, placeholder: '说一句…' })
|
||||||
.placeholderColor(FbColors.faint)
|
.placeholderColor(FbColors.faint)
|
||||||
.placeholderFont({ size: FbMetric.fontTiny })
|
.placeholderFont({ size: FbMetric.fontTiny })
|
||||||
.fontSize(FbMetric.fontSmall)
|
.fontSize(FbMetric.fontSmall)
|
||||||
@@ -1151,11 +1499,19 @@ export struct FbTaskPanel {
|
|||||||
this.sendComment();
|
this.sendComment();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* #77(用户 2026-11-05):「发送按钮显示不全。」
|
||||||
|
根因:原来写死 `width(56)` + `height(38)` + `textAlign(Center)`,
|
||||||
|
而 `Text` 的默认内边距会吃掉两侧空间 —— 56vp 减去左右内边距
|
||||||
|
「发送」两个汉字(约 26vp)+ 内边距正好压线,字体缩放或
|
||||||
|
不同机型上就被截掉半个字。
|
||||||
|
⇒ 不写死宽度:用 `constraintSize({ minWidth: 60 })` + 水平内边距,
|
||||||
|
让按钮按文字实际宽度自适应;`height(38)` 保留(与输入框齐平)。 */
|
||||||
Text('发送')
|
Text('发送')
|
||||||
.fontSize(FbMetric.fontSmall)
|
.fontSize(FbMetric.fontSmall)
|
||||||
.fontColor(this.cmtDraft.trim() === '' ? FbColors.faint : this.accent)
|
.fontColor(this.cmtDraft.trim() === '' ? FbColors.faint : this.accent)
|
||||||
.textAlign(TextAlign.Center)
|
.textAlign(TextAlign.Center)
|
||||||
.width(56)
|
.constraintSize({ minWidth: 60 })
|
||||||
|
.padding({ left: 14, right: 14 })
|
||||||
.height(38)
|
.height(38)
|
||||||
.backgroundColor(fbAlpha(this.accent, 0.1))
|
.backgroundColor(fbAlpha(this.accent, 0.1))
|
||||||
.borderRadius(FbMetric.radiusS)
|
.borderRadius(FbMetric.radiusS)
|
||||||
@@ -1165,6 +1521,13 @@ export struct FbTaskPanel {
|
|||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Center)
|
||||||
|
|
||||||
|
/* 「回车发送」提示:放在输入行**下方**,小字、不抢视线 */
|
||||||
|
Text('回车发送')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ top: 2 })
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ interface RawTask {
|
|||||||
id?: number; title?: string; status?: string; board_id?: number; category_id?: number | null;
|
id?: number; title?: string; status?: string; board_id?: number; category_id?: number | null;
|
||||||
note?: string | null; due_at?: string | null; done_at?: string | null;
|
note?: string | null; due_at?: string | null; done_at?: string | null;
|
||||||
assignee_id?: number | null; assignee_name?: string | null;
|
assignee_id?: number | null; assignee_name?: string | null;
|
||||||
|
/* #77:负责人头像(服务端 Task::find 的 SQL 里一直有 a.avatar AS assignee_avatar) */
|
||||||
|
assignee_avatar?: string | null; assignee_color?: string | null;
|
||||||
avatar?: string | null; avatar_color?: string | null;
|
avatar?: string | null; avatar_color?: string | null;
|
||||||
category_name?: string | null; child_total?: number; child_done?: number;
|
category_name?: string | null; child_total?: number; child_done?: number;
|
||||||
comment_count?: number; attach_count?: number;
|
comment_count?: number; attach_count?: number;
|
||||||
@@ -486,6 +488,9 @@ function taskRow(t: RawTask, boardName: string, boardColor: string): FbTaskItem
|
|||||||
dueText: due !== '' ? datePart(due) : '',
|
dueText: due !== '' ? datePart(due) : '',
|
||||||
overdue: due !== '' && s(t.status) !== 'done' && isBeforeToday(due),
|
overdue: due !== '' && s(t.status) !== 'done' && isBeforeToday(due),
|
||||||
assignee: s(t.assignee_name),
|
assignee: s(t.assignee_name),
|
||||||
|
/* #77:负责人头像(服务端一直有,端上原来没解析) */
|
||||||
|
assigneeAvatar: s(t.assignee_avatar),
|
||||||
|
assigneeColor: s(t.assignee_color),
|
||||||
childDone: n(t.child_done),
|
childDone: n(t.child_done),
|
||||||
childTotal: n(t.child_total),
|
childTotal: n(t.child_total),
|
||||||
/* v65:新建/编辑/搬看板要用到的三个**原始**字段(原来只派生了显示文案) */
|
/* v65:新建/编辑/搬看板要用到的三个**原始**字段(原来只派生了显示文案) */
|
||||||
|
|||||||
@@ -289,37 +289,37 @@ export const FB_DEMO_TASKS: FbTaskItem[] = [
|
|||||||
{
|
{
|
||||||
id: 1, title: '交物业费', status: 'doing', board: '家务', boardColor: '#A8574F',
|
id: 1, title: '交物业费', status: 'doing', board: '家务', boardColor: '#A8574F',
|
||||||
dueText: '10-13 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
dueText: '10-13 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
||||||
boardId: 1, note: '', dueAt: '2026-10-13 18:00'
|
boardId: 1, note: '', dueAt: '2026-10-13 18:00', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2, title: '预约明年体检', status: 'doing', board: '家人健康', boardColor: '#5E7A3C',
|
id: 2, title: '预约明年体检', status: 'doing', board: '家人健康', boardColor: '#5E7A3C',
|
||||||
dueText: '10-20 前', overdue: false, assignee: '爱人', childDone: 1, childTotal: 3,
|
dueText: '10-20 前', overdue: false, assignee: '爱人', childDone: 1, childTotal: 3,
|
||||||
boardId: 2, note: '', dueAt: '2026-10-20 18:00'
|
boardId: 2, note: '', dueAt: '2026-10-20 18:00', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3, title: '整理去年旅行照片', status: 'doing', board: '家务', boardColor: '#A8574F',
|
id: 3, title: '整理去年旅行照片', status: 'doing', board: '家务', boardColor: '#A8574F',
|
||||||
dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
||||||
boardId: 1, note: '', dueAt: ''
|
boardId: 1, note: '', dueAt: '', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4, title: '换季衣物收纳', status: 'todo', board: '家务', boardColor: '#A8574F',
|
id: 4, title: '换季衣物收纳', status: 'todo', board: '家务', boardColor: '#A8574F',
|
||||||
dueText: '10-09 前', overdue: true, assignee: '爱人', childDone: 0, childTotal: 0,
|
dueText: '10-09 前', overdue: true, assignee: '爱人', childDone: 0, childTotal: 0,
|
||||||
boardId: 1, note: '', dueAt: '2026-10-09 18:00'
|
boardId: 1, note: '', dueAt: '2026-10-09 18:00', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5, title: '给儿子买秋季校服', status: 'todo', board: '孩子', boardColor: '#4A7C82',
|
id: 5, title: '给儿子买秋季校服', status: 'todo', board: '孩子', boardColor: '#4A7C82',
|
||||||
dueText: '10-15 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
dueText: '10-15 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
||||||
boardId: 3, note: '', dueAt: '2026-10-15 18:00'
|
boardId: 3, note: '', dueAt: '2026-10-15 18:00', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6, title: '车保养', status: 'todo', board: '车与房', boardColor: '#5566A8',
|
id: 6, title: '车保养', status: 'todo', board: '车与房', boardColor: '#5566A8',
|
||||||
dueText: '', overdue: false, assignee: '', childDone: 0, childTotal: 0,
|
dueText: '', overdue: false, assignee: '', childDone: 0, childTotal: 0,
|
||||||
boardId: 4, note: '', dueAt: ''
|
boardId: 4, note: '', dueAt: '', assigneeAvatar: '', assigneeColor: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7, title: '读完《置身事内》', status: 'done', board: '家务', boardColor: '#A8574F',
|
id: 7, title: '读完《置身事内》', status: 'done', board: '家务', boardColor: '#A8574F',
|
||||||
dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0,
|
||||||
boardId: 1, note: '', dueAt: ''
|
boardId: 1, note: '', dueAt: '', assigneeAvatar: '', assigneeColor: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -797,6 +797,19 @@ export interface FbTaskItem {
|
|||||||
overdue: boolean;
|
overdue: boolean;
|
||||||
/** 负责人显示名(空表示未指派) */
|
/** 负责人显示名(空表示未指派) */
|
||||||
assignee: string;
|
assignee: string;
|
||||||
|
/**
|
||||||
|
* 负责人头像 key(裸 key,渲染时走 `fbImageSrc`)。
|
||||||
|
*
|
||||||
|
* 🔴 #77(用户 2026-11-05):「负责人那个地方也需要有图标,就是人的头像。」
|
||||||
|
*
|
||||||
|
* ⚠️ 服务端 `Task::find()` 的 SQL 里**一直有** `a.avatar AS assignee_avatar`
|
||||||
|
* (实测 #49 返回 `avatars/20261002_xxx.jpg`),
|
||||||
|
* 只是端上 `parseTaskItem` 从没解析它 ⇒ 拿不到头像。
|
||||||
|
* 空串 = 没设头像 / 没指派负责人,UI 用首字色块兜底。
|
||||||
|
*/
|
||||||
|
assigneeAvatar: string;
|
||||||
|
/** 负责人头像底色(没图时用这个画色块;空串则用模块色) */
|
||||||
|
assigneeColor: string;
|
||||||
/** 子任务完成数 / 总数 */
|
/** 子任务完成数 / 总数 */
|
||||||
childDone: number;
|
childDone: number;
|
||||||
childTotal: number;
|
childTotal: number;
|
||||||
|
|||||||
@@ -319,7 +319,10 @@ export struct FbTasksPage {
|
|||||||
id: x.id, title: x.title, status: next, board: x.board, boardColor: x.boardColor,
|
id: x.id, title: x.title, status: next, board: x.board, boardColor: x.boardColor,
|
||||||
dueText: x.dueText, overdue: next === 'done' ? false : x.overdue,
|
dueText: x.dueText, overdue: next === 'done' ? false : x.overdue,
|
||||||
assignee: x.assignee, childDone: x.childDone, childTotal: x.childTotal,
|
assignee: x.assignee, childDone: x.childDone, childTotal: x.childTotal,
|
||||||
boardId: x.boardId, note: x.note, dueAt: x.dueAt
|
boardId: x.boardId, note: x.note, dueAt: x.dueAt,
|
||||||
|
/* #77:新加的两个字段也要带上 —— 漏了它们,改一次状态就把头像抹掉
|
||||||
|
(这个对象是**整条替换**,不是原地改属性)。 */
|
||||||
|
assigneeAvatar: x.assigneeAvatar, assigneeColor: x.assigneeColor
|
||||||
};
|
};
|
||||||
return y;
|
return y;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user