v79 端上:任务面板(首页/任务页共用)+ 任务模块功能补齐
用户原话:「鸿蒙端的功能,也需要同样适配,在首页上可以直接修改编辑任务,
另外任务模块的功能需要完善。」
端上原来只有"看列表 / 勾完成 / 新建改标题",网页 v78 那套「就地处理」
(改状态、挪分组、子任务、附件、留言、删除)一件都没有;首页点任务
弹的还是 v73 的只读卡。
一、新增 FbTaskPanel(首页与任务页共用同一组件)
· 改状态三段(待办/进行中/已完成)+ 标题左侧状态圈
· 挪看板 chips、勾/加子任务、加图片附件并删、发留言
· 编辑 → 交给宿主开 FbTaskEditSheet;删除要**点两次**(端上没有 undo)
· 每个动作成功后再 getTask() 重拉一次:一次动作会牵动子任务计数、
附件张数、看板色,逐处同步 DOM 必然漏一处
二、退役 FbTaskDetailSheet(v73 只读卡,387 行)
⚠️ 它已无人挂载,而守卫还在逐条"通过" —— 等于给死代码盖章。
三、任务模块补齐
· 卡片点击 → 面板(不再直接进编辑表单:误触一下就开始改标题,
且子任务/附件/留言没地方放)
· 「只看已完成」改真打 ?view=done:看板视图服务端就写死
status <> 'done',在客户端筛永远是空
· 看板列表改走 ?view=boards:原来从任务反推 ⇒ 空看板永远选不到
· 编辑表单补分类/循环两排
四、顺带修掉一个自查发现的真 bug
任务页里 sheetCount 有**两个** @Monitor 都监听 boardSheetOpen、都写同一个值
⇒ 关掉设置浮层时谁最后执行不确定,会把还开着的面板"顺便让位结束",
底部悬浮栏浮上来盖住面板按钮。已并成一个取并集的 monitor。
服务端(另一仓):新增 GET /tasks?view=boards、POST/DELETE /tasks/<id>/attach,
详情补 board_name/board_color_calc,越权删别条任务的附件 → 404。
验证:静态守卫 1055/0(新增 v79 一组,含"全页只许一个 sheetCount 赋值点")、
反向验证 215/0(新增 15 条注入,逐条证明新断言会红)、
服务端 e2e 1558/0、_fb_v79_api.py 26/0、编译通过并已装机。
This commit is contained in:
@@ -1,387 +0,0 @@
|
|||||||
/**
|
|
||||||
* 家书 FamilyBook · 任务详情卡(鸿蒙端)
|
|
||||||
*
|
|
||||||
* 🔴 用户 2026-10-21 原话:
|
|
||||||
* 「任务也是一样,点击任务之后弹出来详情,可以标记可以标记它是不是完成了什么之类的。」
|
|
||||||
* ⇒ 首页「近期任务」里点一行 → **就地弹出详情卡**(不跳任务模块),
|
|
||||||
* 卡上能直接勾「完成 / 未完成」。
|
|
||||||
*
|
|
||||||
* ⚠️ 为什么单独一张卡,而不是复用 `FbTaskEditSheet`(任务页的编辑浮层):
|
|
||||||
* 那是**表单**(标题框 / 备注框 / 截止时间 / 看板选择 / 保存按钮),
|
|
||||||
* 适合"我要改这个任务";用户此刻要的是"**看看它是什么、顺手勾一下完成**"。
|
|
||||||
* 拿表单当详情,点开就有五个可编辑控件 + 一颗"保存",观感是"我是不是要改什么"——
|
|
||||||
* 而且误触一下就把标题改了。两件事用两个界面。
|
|
||||||
*
|
|
||||||
* ⚠️ 与 `FbEventSheet`(日程详情)同一套视觉语言:
|
|
||||||
* 外圈主题色描边环(`FbSheetRing`)、关闭统一右上角 ✕(`FbSheetClose`)、
|
|
||||||
* 底部只放动作按钮、`92%` 宽居中、内容超高才滚动。
|
|
||||||
* ⇒ 用户在首页连点一个日程、一条任务,两张卡长得像"同一个应用的弹框"。
|
|
||||||
*
|
|
||||||
* 🔴 两处容易做错的地方(都已按正确方式实现,别改回去):
|
|
||||||
* ① 完成状态**必须走 `/tasks/<id>/status`**(`setTaskStatus`),
|
|
||||||
* 不能走 `PATCH /tasks/<id>` —— 服务端 PATCH 会把 status 原样忽略,
|
|
||||||
* 表现是"提示保存成功、但状态没变"(`FbTaskRepo` 文件头写着)。
|
|
||||||
* ② 勾完**立刻**把卡片上的状态改掉(本地先改),再让宿主去重拉对齐派生字段
|
|
||||||
* (逾期、子任务进度这些是服务端算的)。等网络回来再改,用户会以为没点上。
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FbColors, FbMetric, FbTint } from '../common/Theme';
|
|
||||||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
|
||||||
import { FbTaskItem } from '../model/Types';
|
|
||||||
import { FbSheetRing } from './FbSheetRing';
|
|
||||||
import { FbSheetClose } from './FbSheetClose';
|
|
||||||
|
|
||||||
const FB_SHEET_BOTTOM_GAP: number = 16;
|
|
||||||
|
|
||||||
@ComponentV2
|
|
||||||
export struct FbTaskDetailSheet {
|
|
||||||
/**
|
|
||||||
* 这条任务(首页给的那份 `FbTaskItem`)。
|
|
||||||
*
|
|
||||||
* ⚠️ 用 `| null` 而不是给一份对象字面量默认值 —— 与本工程其它浮层
|
|
||||||
* (`FbEventSheet.event`)同一个写法。理由:
|
|
||||||
* ① 对象字面量默认值在 `@Param` 上会走 `arkts-no-untyped-obj-literals` 那条规则;
|
|
||||||
* ② 这份数据**永远由宿主传入**(开着卡就一定有它),默认值只是"没给时不崩",
|
|
||||||
* 给一个空壳反而会画出「未命名任务」这种假内容。
|
|
||||||
*/
|
|
||||||
@Param task: FbTaskItem | null = null;
|
|
||||||
/**
|
|
||||||
* 卡上显示的**当前状态**(`todo` / `doing` / `done`)。
|
|
||||||
*
|
|
||||||
* ⚠️ 从 `task.status` 拷一份出来放在宿主那边管,是因为"勾完"之后要**立刻**变:
|
|
||||||
* 如果把 `task` 整个换掉,宿主得重建这份数据(还要重拉接口),中间会有一次闪烁。
|
|
||||||
* 这里只用一个字符串,宿主那边改一次就行。
|
|
||||||
*/
|
|
||||||
@Param status: string = 'todo';
|
|
||||||
/** 正在提交(按钮变灰,防连点) */
|
|
||||||
@Param busy: boolean = false;
|
|
||||||
/** 提交失败/成功的说明(空 = 不显示) */
|
|
||||||
@Param note: string = '';
|
|
||||||
/** 环色 —— 任务模块主题色(与任务页浮层一致) */
|
|
||||||
@Param accent: string = '';
|
|
||||||
/** 点「完成 / 取消完成」 */
|
|
||||||
@Event onToggle: () => void = (): void => {
|
|
||||||
};
|
|
||||||
/** 点「改一改」(跳任务页的编辑浮层) */
|
|
||||||
@Event onEdit: () => void = (): void => {
|
|
||||||
};
|
|
||||||
@Event onClose: () => void = (): void => {
|
|
||||||
};
|
|
||||||
|
|
||||||
private accentColor(): string {
|
|
||||||
if (this.accent !== '') {
|
|
||||||
return this.accent;
|
|
||||||
}
|
|
||||||
const m: FbModuleDef | undefined = fbModuleOf('tasks');
|
|
||||||
return m !== undefined ? fbThemeVars(m.color).brand : FbTint.brand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取主题色的半透明版本(八位色 `#AARRGGBB`:**alpha 在前**) */
|
|
||||||
private accentAlpha(aa: string): string {
|
|
||||||
const c: string = this.accentColor().replace('#', '');
|
|
||||||
const rgb: string = c.length >= 8 ? c.substring(2) : c;
|
|
||||||
return '#' + aa + rgb;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isDone(): boolean {
|
|
||||||
return this.status === 'done';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 取值器 ----
|
|
||||||
⚠️ 为什么要一串 `xxxOf()` 而不是在 `build()` 里直接写 `this.task!.xxx`:
|
|
||||||
① `build()` 里**不能声明变量**(只有 UI 组件语法),条件与文案都得靠方法;
|
|
||||||
② `task` 是可空类型,每个访问点都写 `!` 会让"哪些地方依赖它"看不出来;
|
|
||||||
③ 这些取值器同时是**兜底**(空 → 空串),漏传一份任务也不会崩。
|
|
||||||
⚠️ 不要把它们改成 `@Param` 多参数 `@Builder` —— 那会踩"多值参数不参与观察"。 */
|
|
||||||
private titleOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null && t.title !== '' ? t.title : '未命名任务';
|
|
||||||
}
|
|
||||||
|
|
||||||
private boardOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null ? t.board : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private boardColorOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null && t.boardColor !== '' ? t.boardColor : '#9AA0A6';
|
|
||||||
}
|
|
||||||
|
|
||||||
private dueOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null ? t.dueText : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private overdueOf(): boolean {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null && t.overdue && !this.isDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private assigneeOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null ? t.assignee : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private childOf(): number {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null ? t.childTotal : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private childText(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
if (t === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return t.childDone.toString() + ' / ' + t.childTotal.toString() + ' 已完成';
|
|
||||||
}
|
|
||||||
|
|
||||||
private noteOf(): string {
|
|
||||||
const t: FbTaskItem | null = this.task;
|
|
||||||
return t !== null ? t.note : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 状态中文(`待办` / `进行中` / `已完成`) */
|
|
||||||
private statusText(): string {
|
|
||||||
if (this.status === 'done') {
|
|
||||||
return '已完成';
|
|
||||||
}
|
|
||||||
return this.status === 'doing' ? '进行中' : '待办';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态的强调色。
|
|
||||||
*
|
|
||||||
* ⚠️ 完成用**绿**、逾期用**红** —— 与任务页/网页版同一套语义。
|
|
||||||
* 这里不跟模块主题色走:状态是"事实",不是"我在哪个模块"。
|
|
||||||
*/
|
|
||||||
private statusTint(): string {
|
|
||||||
if (this.status === 'done') {
|
|
||||||
return '#3F7D5A';
|
|
||||||
}
|
|
||||||
return this.status === 'doing' ? '#A8574F' : '#7C8AA0';
|
|
||||||
}
|
|
||||||
|
|
||||||
private statusWash(): string {
|
|
||||||
if (this.status === 'done') {
|
|
||||||
return '#EAF3EE';
|
|
||||||
}
|
|
||||||
return this.status === 'doing' ? '#FBEFEE' : '#F1F3F5';
|
|
||||||
}
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
row(label: string, value: string) {
|
|
||||||
Row({ space: 10 }) {
|
|
||||||
Text(label)
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor(FbColors.faint)
|
|
||||||
.width(56)
|
|
||||||
Text(value)
|
|
||||||
.fontSize(FbMetric.fontSmall)
|
|
||||||
.fontColor(FbColors.ink)
|
|
||||||
.layoutWeight(1)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ top: 8, bottom: 8 })
|
|
||||||
.alignItems(VerticalAlign.Top)
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
Column() {
|
|
||||||
/* 🔴 包裹这层 `Column` 只为了挂 `.width('92%')`。
|
|
||||||
⚠️ **不能**把属性直接挂在 `FbSheetRing` 上(`FbSheetRing(...) { … }.width(...)`):
|
|
||||||
带**尾随闭包**的自定义组件后面接属性方法,ArkTS 编译器会报
|
|
||||||
「Declaration or statement expected」外加一条「Cannot find name 'width'」,
|
|
||||||
位置正好落在那行属性的开头 —— 看着像"括号没配平",其实是语法不支持。
|
|
||||||
⇒ 照 `FbEventSheet` / `FbMovieDetailSheet` 的既有形态:环组件**自身不挂属性**,
|
|
||||||
外面包一层容器,把宽度/边距挂到容器上。
|
|
||||||
(编译实测:不包这层就红在 `FbTaskDetailSheet.ets:370`。) */
|
|
||||||
Column() {
|
|
||||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
|
|
||||||
Column({ space: 0 }) {
|
|
||||||
// ---- 标题区:状态徽标 + 标题 + 关闭 ----
|
|
||||||
Row({ space: 10 }) {
|
|
||||||
/* 左侧那颗**可点的状态圆**:点一下就是"完成/取消完成"。
|
|
||||||
🔴 用户要的就是"可以标记它是不是完成了"——
|
|
||||||
把这件事做成**标题左边那颗圈**(与任务页列表里那颗同一个交互),
|
|
||||||
而不是只在底部放个按钮:这是用户最习惯的位置。 */
|
|
||||||
Stack() {
|
|
||||||
Column()
|
|
||||||
.width(22)
|
|
||||||
.height(22)
|
|
||||||
.borderRadius(11)
|
|
||||||
.backgroundColor(this.isDone() ? this.statusTint() : Color.Transparent)
|
|
||||||
.border({ width: 1.5, color: this.isDone() ? this.statusTint() : '#C9CDD2' })
|
|
||||||
if (this.isDone()) {
|
|
||||||
Text('✓')
|
|
||||||
.fontSize(13)
|
|
||||||
.fontColor(Color.White)
|
|
||||||
.fontWeight(FontWeight.Medium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width(30)
|
|
||||||
.height(30)
|
|
||||||
.onClick((): void => {
|
|
||||||
if (!this.busy) {
|
|
||||||
this.onToggle();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Text(this.titleOf())
|
|
||||||
.fontSize(FbMetric.fontTitle)
|
|
||||||
.fontWeight(FontWeight.Medium)
|
|
||||||
.fontColor(this.isDone() ? FbColors.faint : FbColors.ink)
|
|
||||||
.decoration({
|
|
||||||
type: this.isDone() ? TextDecorationType.LineThrough : TextDecorationType.None,
|
|
||||||
color: FbColors.faint
|
|
||||||
})
|
|
||||||
.maxLines(2)
|
|
||||||
.layoutWeight(1)
|
|
||||||
|
|
||||||
Text(this.statusText())
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor(this.statusTint())
|
|
||||||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
|
||||||
.backgroundColor(this.statusWash())
|
|
||||||
.borderRadius(FbMetric.radiusPill)
|
|
||||||
|
|
||||||
FbSheetClose({
|
|
||||||
onClose: (): void => {
|
|
||||||
this.onClose();
|
|
||||||
},
|
|
||||||
color: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.alignItems(VerticalAlign.Top)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.color(FbColors.line)
|
|
||||||
.margin({ top: 12, bottom: 4 })
|
|
||||||
|
|
||||||
Scroll() {
|
|
||||||
Column({ space: 0 }) {
|
|
||||||
if (this.boardOf() !== '') {
|
|
||||||
// 看板:左侧带看板本色的小点(与首页那行同一个色,视觉上能对上号)
|
|
||||||
Row({ space: 10 }) {
|
|
||||||
Text('看板')
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor(FbColors.faint)
|
|
||||||
.width(56)
|
|
||||||
Row({ space: 6 }) {
|
|
||||||
Column()
|
|
||||||
.width(8)
|
|
||||||
.height(8)
|
|
||||||
.borderRadius(4)
|
|
||||||
.backgroundColor(this.boardColorOf())
|
|
||||||
Text(this.boardOf())
|
|
||||||
.fontSize(FbMetric.fontSmall)
|
|
||||||
.fontColor(FbColors.ink)
|
|
||||||
}
|
|
||||||
.layoutWeight(1)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ top: 8, bottom: 8 })
|
|
||||||
.alignItems(VerticalAlign.Center)
|
|
||||||
}
|
|
||||||
if (this.dueOf() !== '') {
|
|
||||||
Row({ space: 10 }) {
|
|
||||||
Text('截止')
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor(FbColors.faint)
|
|
||||||
.width(56)
|
|
||||||
Text(this.dueOf())
|
|
||||||
.fontSize(FbMetric.fontSmall)
|
|
||||||
.fontColor(this.overdueOf() ? '#C0533F' : FbColors.ink)
|
|
||||||
.layoutWeight(1)
|
|
||||||
if (this.overdueOf()) {
|
|
||||||
Text('已逾期')
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor('#C0533F')
|
|
||||||
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
|
|
||||||
.backgroundColor('#FDECEC')
|
|
||||||
.borderRadius(FbMetric.radiusPill)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ top: 8, bottom: 8 })
|
|
||||||
.alignItems(VerticalAlign.Center)
|
|
||||||
}
|
|
||||||
if (this.assigneeOf() !== '') {
|
|
||||||
this.row('负责人', this.assigneeOf())
|
|
||||||
}
|
|
||||||
if (this.childOf() > 0) {
|
|
||||||
this.row('子任务', this.childText())
|
|
||||||
}
|
|
||||||
if (this.noteOf() !== '') {
|
|
||||||
this.row('备注', this.noteOf())
|
|
||||||
}
|
|
||||||
if (this.note !== '') {
|
|
||||||
Text(this.note)
|
|
||||||
.fontSize(FbMetric.fontTiny)
|
|
||||||
.fontColor(this.statusTint())
|
|
||||||
.width('100%')
|
|
||||||
.padding({ top: 8 })
|
|
||||||
}
|
|
||||||
Row().height(8)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
}
|
|
||||||
/* 与 `FbEventSheet` 同一条纪律:
|
|
||||||
`flexShrink(1)` 而不是 `layoutWeight(1)` —— 后者会让短内容也把卡片顶到 72%,
|
|
||||||
底下留一大片空白。 */
|
|
||||||
.flexShrink(1)
|
|
||||||
.width('100%')
|
|
||||||
.scrollBar(BarState.Off)
|
|
||||||
.align(Alignment.TopStart)
|
|
||||||
|
|
||||||
// ---- 底部:主操作(勾完成)+ 次操作(改一改)----
|
|
||||||
Row({ space: 10 }) {
|
|
||||||
Text(this.busy ? '处理中…' : (this.isDone() ? '标记为未完成' : '标记为已完成'))
|
|
||||||
.fontSize(FbMetric.fontSmall)
|
|
||||||
.fontColor(Color.White)
|
|
||||||
.textAlign(TextAlign.Center)
|
|
||||||
.layoutWeight(1)
|
|
||||||
.height(42)
|
|
||||||
/* 已完成时按钮**不用主色实底**:那时候它是"撤销"而不是"完成",
|
|
||||||
视觉强度该低一档(与 `FbTaskEditSheet` 删除按钮的取舍同一个思路)。 */
|
|
||||||
.backgroundColor(this.busy ? '#C9CDD2'
|
|
||||||
: (this.isDone() ? this.statusTint() : this.accentColor()))
|
|
||||||
.borderRadius(FbMetric.radiusS)
|
|
||||||
.onClick((): void => {
|
|
||||||
if (!this.busy) {
|
|
||||||
this.onToggle();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Text('改一改')
|
|
||||||
.fontSize(FbMetric.fontSmall)
|
|
||||||
.fontColor(this.accentColor())
|
|
||||||
.textAlign(TextAlign.Center)
|
|
||||||
.width(88)
|
|
||||||
.height(42)
|
|
||||||
.backgroundColor(this.accentAlpha('14'))
|
|
||||||
.borderRadius(FbMetric.radiusS)
|
|
||||||
.border({ width: 1, color: this.accentAlpha('33') })
|
|
||||||
.onClick((): void => {
|
|
||||||
this.onEdit();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.margin({ top: 14 })
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.constraintSize({ maxHeight: '72%' })
|
|
||||||
.padding({ left: 18, right: 18, top: 18, bottom: 20 + FB_SHEET_BOTTOM_GAP })
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width('92%')
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.height('100%')
|
|
||||||
.justifyContent(FlexAlign.Center)
|
|
||||||
.hitTestBehavior(HitTestMode.Transparent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme';
|
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme';
|
||||||
import { FbBoardOpt } from '../model/Types';
|
import { FbBoardOpt, FbCatOpt } from '../model/Types';
|
||||||
import { FbSheetClose } from './FbSheetClose';
|
import { FbSheetClose } from './FbSheetClose';
|
||||||
import { FbSheetRing } from './FbSheetRing';
|
import { FbSheetRing } from './FbSheetRing';
|
||||||
import { FbDateTimeRow, FbDateTimePicker, fbIsoOf, fbJoinDateTime } from './FbDateTimeField';
|
import { FbDateTimeRow, FbDateTimePicker, fbIsoOf, fbJoinDateTime } from './FbDateTimeField';
|
||||||
@@ -25,14 +25,19 @@ import { FbApiResult } from '../data/FbHttp';
|
|||||||
export struct FbTaskEditSheet {
|
export struct FbTaskEditSheet {
|
||||||
/** 模块主题色(任务 = 赭红) */
|
/** 模块主题色(任务 = 赭红) */
|
||||||
@Param accent: string = '#A8574F';
|
@Param accent: string = '#A8574F';
|
||||||
/** 可选的看板(父页面从已加载任务里反推出来) */
|
/** 可选的看板(父页面用 `?view=boards` 拿**全部**看板,含空看板) */
|
||||||
@Param boards: FbBoardOpt[] = [];
|
@Param boards: FbBoardOpt[] = [];
|
||||||
|
/** 可选的分类(v79;同上,来自 `?view=boards` 的 categories) */
|
||||||
|
@Param cats: FbCatOpt[] = [];
|
||||||
/** 编辑的这条(0 = 新建) */
|
/** 编辑的这条(0 = 新建) */
|
||||||
@Param editingId: number = 0;
|
@Param editingId: number = 0;
|
||||||
@Param editingTitle: string = '';
|
@Param editingTitle: string = '';
|
||||||
@Param editingNote: string = '';
|
@Param editingNote: string = '';
|
||||||
@Param editingDueAt: string = '';
|
@Param editingDueAt: string = '';
|
||||||
@Param editingBoardId: number = 0;
|
@Param editingBoardId: number = 0;
|
||||||
|
/** v79:分类 / 循环(编辑时回填) */
|
||||||
|
@Param editingCatId: number = 0;
|
||||||
|
@Param editingRepeat: string = '';
|
||||||
|
|
||||||
@Event onClose: () => void = (): void => {};
|
@Event onClose: () => void = (): void => {};
|
||||||
@Event onSaved: () => void = (): void => {};
|
@Event onSaved: () => void = (): void => {};
|
||||||
@@ -40,6 +45,10 @@ export struct FbTaskEditSheet {
|
|||||||
@Local title: string = '';
|
@Local title: string = '';
|
||||||
@Local note: string = '';
|
@Local note: string = '';
|
||||||
@Local boardId: number = 0;
|
@Local boardId: number = 0;
|
||||||
|
/** v79:分类 id(0 = 不分类) */
|
||||||
|
@Local catId: number = 0;
|
||||||
|
/** v79:循环规则('' = 不循环) */
|
||||||
|
@Local repeat: string = '';
|
||||||
/** 截止日期 `YYYY-MM-DD`,空 = 没有期限 */
|
/** 截止日期 `YYYY-MM-DD`,空 = 没有期限 */
|
||||||
@Local dueDate: string = '';
|
@Local dueDate: string = '';
|
||||||
/** 截止时分 */
|
/** 截止时分 */
|
||||||
@@ -52,6 +61,8 @@ export struct FbTaskEditSheet {
|
|||||||
this.title = this.editingTitle;
|
this.title = this.editingTitle;
|
||||||
this.note = this.editingNote;
|
this.note = this.editingNote;
|
||||||
this.boardId = this.editingBoardId;
|
this.boardId = this.editingBoardId;
|
||||||
|
this.catId = this.editingCatId;
|
||||||
|
this.repeat = this.editingRepeat;
|
||||||
// `2026-10-25 18:00` → 日期 + 时分(服务端给的是空格分隔)
|
// `2026-10-25 18:00` → 日期 + 时分(服务端给的是空格分隔)
|
||||||
const raw: string = this.editingDueAt.trim();
|
const raw: string = this.editingDueAt.trim();
|
||||||
if (raw.length >= 16) {
|
if (raw.length >= 16) {
|
||||||
@@ -196,11 +207,58 @@ export struct FbTaskEditSheet {
|
|||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
// ---- 分类(v79)----
|
||||||
|
if (this.cats.length > 0) {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text('分类')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
Scroll() {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
this.catChip(0, '不分类')
|
||||||
|
ForEach(this.cats, (c: FbCatOpt) => {
|
||||||
|
this.catChip(c.id, c.name)
|
||||||
|
}, (c: FbCatOpt): string => 'tc_' + c.id.toString())
|
||||||
|
}
|
||||||
|
.padding({ right: 4 })
|
||||||
|
}
|
||||||
|
.scrollable(ScrollDirection.Horizontal)
|
||||||
|
.scrollBar(BarState.Off)
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 循环(v79)----
|
||||||
|
⚠️ 循环的"生成下一条"是**服务端**做的(`Task::setStatus` 完成时
|
||||||
|
spawnNext),端上只需要把 `repeat_rule` 存进去 —— 别在这儿
|
||||||
|
自己再排一遍,否则会出现两条一模一样的任务。 */
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text('循环')
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
Scroll() {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
this.repeatChip('', '不循环')
|
||||||
|
this.repeatChip('day', '每天')
|
||||||
|
this.repeatChip('week', '每周')
|
||||||
|
this.repeatChip('month', '每月')
|
||||||
|
this.repeatChip('year', '每年')
|
||||||
|
}
|
||||||
|
.padding({ right: 4 })
|
||||||
|
}
|
||||||
|
.scrollable(ScrollDirection.Horizontal)
|
||||||
|
.scrollBar(BarState.Off)
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
.constraintSize({ maxHeight: 360 })
|
.constraintSize({ maxHeight: 430 })
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.scrollBar(BarState.Off)
|
.scrollBar(BarState.Off)
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
@@ -274,7 +332,13 @@ export struct FbTaskEditSheet {
|
|||||||
// 没有期限就发空串(服务端 `Api::dt('due_at', '')` → null)
|
// 没有期限就发空串(服务端 `Api::dt('due_at', '')` → null)
|
||||||
dueAt: this.dueDate !== '' ? fbJoinDateTime(this.dueDate, this.dueTime) : '',
|
dueAt: this.dueDate !== '' ? fbJoinDateTime(this.dueDate, this.dueTime) : '',
|
||||||
boardId: this.boardId,
|
boardId: this.boardId,
|
||||||
status: 'todo'
|
status: 'todo',
|
||||||
|
/* 0 = 顶层任务(新建子任务走 `FbTaskPanel.addKid`,不在这里) */
|
||||||
|
parentId: 0,
|
||||||
|
/* -1 = 这次不改负责人:表单里没有这一项,传 0 会把已指派的负责人清空 */
|
||||||
|
assigneeId: -1,
|
||||||
|
categoryId: this.catId,
|
||||||
|
repeat: this.repeat
|
||||||
};
|
};
|
||||||
const token: string = FB_TOKEN_STORE.get();
|
const token: string = FB_TOKEN_STORE.get();
|
||||||
const res: FbApiResult = this.editingId > 0
|
const res: FbApiResult = this.editingId > 0
|
||||||
@@ -302,6 +366,34 @@ export struct FbTaskEditSheet {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分类单选胶囊(v79) */
|
||||||
|
@Builder
|
||||||
|
catChip(id: number, name: string) {
|
||||||
|
Text(name)
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(this.catId === id ? '#FFFFFF' : FbColors.muted)
|
||||||
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||||
|
.backgroundColor(this.catId === id ? this.accent : FbColors.inputBg)
|
||||||
|
.borderRadius(FbMetric.radiusPill)
|
||||||
|
.onClick((): void => {
|
||||||
|
this.catId = id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 循环单选胶囊(v79;'' = 不循环) */
|
||||||
|
@Builder
|
||||||
|
repeatChip(v: string, name: string) {
|
||||||
|
Text(name)
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(this.repeat === v ? '#FFFFFF' : FbColors.muted)
|
||||||
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||||
|
.backgroundColor(this.repeat === v ? this.accent : FbColors.inputBg)
|
||||||
|
.borderRadius(FbMetric.radiusPill)
|
||||||
|
.onClick((): void => {
|
||||||
|
this.repeat = v;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
textField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
|
textField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import {
|
|||||||
FbMark,
|
FbMark,
|
||||||
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNotificationItem, FbResourceItem,
|
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNotificationItem, FbResourceItem,
|
||||||
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
|
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
|
||||||
|
FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt,
|
||||||
} from '../model/Types';
|
} from '../model/Types';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -171,6 +172,45 @@ interface RawTaskBoard {
|
|||||||
id?: number; name?: string; color?: string; tasks?: RawTask[];
|
id?: number; name?: string; color?: string; tasks?: RawTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- v79:任务详情(`GET /tasks/<id>`)才有那几块 ---- */
|
||||||
|
|
||||||
|
interface RawTaskKid {
|
||||||
|
id?: number; title?: string; status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawTaskComment {
|
||||||
|
id?: number; person_id?: number; content?: string; created_at?: string;
|
||||||
|
name?: string | null; my_nickname?: string | null; avatar_color?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawTaskAttach {
|
||||||
|
id?: number; key?: string; name?: string | null; mime?: string | null;
|
||||||
|
size?: number; is_image?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务详情的原始行:任务本体 + 详情独占的几列 + 三块子集合 */
|
||||||
|
interface RawTaskDetail extends RawTask {
|
||||||
|
assignee_id?: number | null;
|
||||||
|
/** 我对负责人的称呼(优先于 `assignee_name`,与网页 `person_name()` 同口径) */
|
||||||
|
assignee_nick?: string | null;
|
||||||
|
creator_name?: string | null;
|
||||||
|
creator_nick?: string | null;
|
||||||
|
repeat_rule?: string | null;
|
||||||
|
parent_title?: string | null;
|
||||||
|
children?: RawTaskKid[];
|
||||||
|
comments?: RawTaskComment[];
|
||||||
|
attachs?: RawTaskAttach[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `?view=boards` 的看板元数据(v79) */
|
||||||
|
interface RawBoardMeta {
|
||||||
|
id?: number; name?: string; color?: string; open?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawCatMeta {
|
||||||
|
id?: number; name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface RawLedgerBook {
|
interface RawLedgerBook {
|
||||||
id?: number; name?: string; my_perm?: string | null; entry_count?: number;
|
id?: number; name?: string; my_perm?: string | null; entry_count?: number;
|
||||||
share_perm?: string | null;
|
share_perm?: string | null;
|
||||||
@@ -799,6 +839,18 @@ export function parseMemories(v: Object | null | undefined): FbMemoryItem[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扁平的任务列表 JSON(`?view=done` / `?view=status`)→ `FbTaskItem[]`。
|
||||||
|
*
|
||||||
|
* ⚠️ 与 `parseTasks`(看板视图)**不能互换**:那个认 `{boards:[...]}`,
|
||||||
|
* 这个认 `[...]`。缓存键也必须分开(`tasks` vs `tasks_done`),
|
||||||
|
* 否则两个形状会互相覆盖,表现是"切一次已完成,看板全空了"。
|
||||||
|
*/
|
||||||
|
export function parseTaskList(json: string): FbTaskItem[] {
|
||||||
|
const rows: RawTask[] = asArray<RawTask>(json);
|
||||||
|
return parseTaskRows(rows);
|
||||||
|
}
|
||||||
|
|
||||||
/** 一批任务行 → FbTaskItem[](首页与任务页共用同一段形状转换) */
|
/** 一批任务行 → FbTaskItem[](首页与任务页共用同一段形状转换) */
|
||||||
export function parseTaskRows(rows: RawTask[]): FbTaskItem[] {
|
export function parseTaskRows(rows: RawTask[]): FbTaskItem[] {
|
||||||
const out: FbTaskItem[] = [];
|
const out: FbTaskItem[] = [];
|
||||||
@@ -808,6 +860,156 @@ export function parseTaskRows(rows: RawTask[]): FbTaskItem[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `?view=boards` → 看板 + 分类(v79)。
|
||||||
|
*
|
||||||
|
* 🔴 端上原来是从已加载任务里**反推**看板的(`FbTasksPage.boardOpts`),
|
||||||
|
* 那样只能看到"有任务的看板" —— 用户刚建好、还没放任务的看板在端上
|
||||||
|
* 永远选不到(表现为"网页能建,端上挪不过去")。
|
||||||
|
*/
|
||||||
|
export function parseTaskMeta(json: string): FbTaskMeta {
|
||||||
|
const root: Record<string, Object> | null = asObject(json);
|
||||||
|
const boards: FbBoardOpt[] = [];
|
||||||
|
const cats: FbCatOpt[] = [];
|
||||||
|
if (root === null) {
|
||||||
|
const empty: FbTaskMeta = { boards: boards, cats: cats };
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
for (const b of arr<RawBoardMeta>(root['boards'] as RawBoardMeta[])) {
|
||||||
|
const id: number = n(b.id);
|
||||||
|
const name: string = s(b.name);
|
||||||
|
if (name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const one: FbBoardOpt = { id: id, name: name, color: s(b.color) };
|
||||||
|
boards.push(one);
|
||||||
|
}
|
||||||
|
for (const c of arr<RawCatMeta>(root['categories'] as RawCatMeta[])) {
|
||||||
|
const name: string = s(c.name);
|
||||||
|
if (name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const one: FbCatOpt = { id: n(c.id), name: name };
|
||||||
|
cats.push(one);
|
||||||
|
}
|
||||||
|
const meta: FbTaskMeta = { boards: boards, cats: cats };
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一条任务的详情 → `FbTaskDetail`(v79 面板用)。
|
||||||
|
*
|
||||||
|
* ⚠️ `base` 走**同一个** `taskRow()`(与首页/任务页列表同源)——
|
||||||
|
* 三处各写一份转换,迟早出现"同一张卡在两个页面显示不一样"。
|
||||||
|
*/
|
||||||
|
export function parseTaskDetail(json: string): FbTaskDetail | null {
|
||||||
|
const root: Record<string, Object> | null = asObject(json);
|
||||||
|
if (root === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const d: RawTaskDetail = root as RawTaskDetail;
|
||||||
|
if (n(d.id) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const base: FbTaskItem = taskRow(d, s(d.board_name), s(d.board_color_calc));
|
||||||
|
|
||||||
|
const kids: FbTaskKid[] = [];
|
||||||
|
for (const k of arr<RawTaskKid>(d.children)) {
|
||||||
|
if (n(k.id) <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const one: FbTaskKid = {
|
||||||
|
id: n(k.id),
|
||||||
|
title: s(k.title),
|
||||||
|
status: s(k.status) !== '' ? s(k.status) : 'todo',
|
||||||
|
};
|
||||||
|
kids.push(one);
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments: FbTaskComment[] = [];
|
||||||
|
for (const c of arr<RawTaskComment>(d.comments)) {
|
||||||
|
const nick: string = s(c.my_nickname);
|
||||||
|
const one: FbTaskComment = {
|
||||||
|
id: n(c.id),
|
||||||
|
content: s(c.content),
|
||||||
|
name: nick !== '' ? nick : s(c.name),
|
||||||
|
color: s(c.avatar_color),
|
||||||
|
createdAt: s(c.created_at),
|
||||||
|
};
|
||||||
|
comments.push(one);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachs: FbTaskAttach[] = [];
|
||||||
|
for (const a of arr<RawTaskAttach>(d.attachs)) {
|
||||||
|
const name: string = s(a.name);
|
||||||
|
const mime: string = s(a.mime);
|
||||||
|
const isImg: boolean = n(a.is_image) === 1 || mime.indexOf('image/') === 0;
|
||||||
|
const one: FbTaskAttach = {
|
||||||
|
id: n(a.id),
|
||||||
|
name: name !== '' ? name : '附件',
|
||||||
|
kind: attachKindOf(mime, name, isImg),
|
||||||
|
isImage: isImg,
|
||||||
|
key: s(a.key),
|
||||||
|
sizeWord: sizeWordOf(n(a.size)),
|
||||||
|
};
|
||||||
|
attachs.push(one);
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorNick: string = s(d.creator_nick);
|
||||||
|
const detail: FbTaskDetail = {
|
||||||
|
base: base,
|
||||||
|
assigneeId: n(d.assignee_id),
|
||||||
|
categoryId: n(d.category_id),
|
||||||
|
category: s(d.category_name),
|
||||||
|
repeat: s(d.repeat_rule),
|
||||||
|
parentTitle: s(d.parent_title),
|
||||||
|
creator: creatorNick !== '' ? creatorNick : s(d.creator_name),
|
||||||
|
kids: kids,
|
||||||
|
comments: comments,
|
||||||
|
attachs: attachs,
|
||||||
|
};
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件是「哪一类」—— 决定画缩略图还是画一个图标 + 后缀 */
|
||||||
|
function attachKindOf(mime: string, name: string, isImg: boolean): string {
|
||||||
|
if (isImg) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
const dot: number = name.lastIndexOf('.');
|
||||||
|
const ext: string = dot >= 0 ? name.substring(dot + 1).toLowerCase() : '';
|
||||||
|
if (mime === 'application/pdf' || ext === 'pdf') {
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
if (ext === 'doc' || ext === 'docx') {
|
||||||
|
return 'doc';
|
||||||
|
}
|
||||||
|
if (ext === 'xls' || ext === 'xlsx' || ext === 'csv') {
|
||||||
|
return 'xls';
|
||||||
|
}
|
||||||
|
if (ext === 'ppt' || ext === 'pptx') {
|
||||||
|
return 'ppt';
|
||||||
|
}
|
||||||
|
if (ext === 'txt' || ext === 'md') {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件大小(给人看的那个串) */
|
||||||
|
function sizeWordOf(bytes: number): string {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return bytes.toString() + ' B';
|
||||||
|
}
|
||||||
|
if (bytes < 1024 * 1024) {
|
||||||
|
return (Math.round(bytes / 102.4) / 10).toString() + ' KB';
|
||||||
|
}
|
||||||
|
return (Math.round(bytes / 104857.6) / 10).toString() + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 内部工具
|
// 内部工具
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
||||||
|
import { fbUploadOne } from './FbUpload';
|
||||||
|
|
||||||
/** 新建/编辑任务提交的字段 */
|
/** 新建/编辑任务提交的字段 */
|
||||||
export interface FbTaskDraft {
|
export interface FbTaskDraft {
|
||||||
@@ -29,6 +30,22 @@ export interface FbTaskDraft {
|
|||||||
boardId: number;
|
boardId: number;
|
||||||
/** todo / doing / done(只在**新建**时生效;改状态走 `setTaskStatus`) */
|
/** todo / doing / done(只在**新建**时生效;改状态走 `setTaskStatus`) */
|
||||||
status: string;
|
status: string;
|
||||||
|
/**
|
||||||
|
* 父任务 id(v79;0 = 顶层任务)。
|
||||||
|
* 面板里「加一条子任务」就是 `parentId = 当前任务` 的新建请求。
|
||||||
|
*/
|
||||||
|
parentId: number;
|
||||||
|
/**
|
||||||
|
* 负责人 id。
|
||||||
|
*
|
||||||
|
* `> 0` = 指定这个人;`0` = 新建时"用默认值(我自己)"、编辑时"清空负责人";
|
||||||
|
* **`-1` = 这次不改**(编辑表单目前没有负责人这一项 ⇒ 不能传 0 把它抹掉)。
|
||||||
|
*/
|
||||||
|
assigneeId: number;
|
||||||
|
/** 分类 id(0 = 不分类) */
|
||||||
|
categoryId: number;
|
||||||
|
/** 循环规则:空 / day / week / month / year */
|
||||||
|
repeat: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新建任务 */
|
/** 新建任务 */
|
||||||
@@ -41,12 +58,27 @@ export async function createTask(token: string, d: FbTaskDraft): Promise<FbApiRe
|
|||||||
body['due_at'] = d.dueAt;
|
body['due_at'] = d.dueAt;
|
||||||
body['board_id'] = d.boardId;
|
body['board_id'] = d.boardId;
|
||||||
body['status'] = d.status;
|
body['status'] = d.status;
|
||||||
|
/* v79:这三个原来没传 —— 端上因此**建不出子任务**、也指定不了负责人/分类。
|
||||||
|
⚠️ `assignee_id` 只在 >0 时才传:服务端 `Api::int('assignee_id', $pid)`
|
||||||
|
的默认值是"我自己",传 0 会把它覆盖成"没人负责"。 */
|
||||||
|
if (d.parentId > 0) {
|
||||||
|
body['parent_id'] = d.parentId;
|
||||||
|
}
|
||||||
|
if (d.assigneeId > 0) {
|
||||||
|
body['assignee_id'] = d.assigneeId;
|
||||||
|
}
|
||||||
|
if (d.categoryId > 0) {
|
||||||
|
body['category_id'] = d.categoryId;
|
||||||
|
}
|
||||||
|
if (d.repeat !== '') {
|
||||||
|
body['repeat_rule'] = d.repeat;
|
||||||
|
}
|
||||||
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||||
return await fbApi('/api/v1/tasks', opts);
|
return await fbApi('/api/v1/tasks', opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 改任务(标题 / 备注 / 截止 / 看板)。
|
* 改任务(标题 / 备注 / 截止 / 看板 / 负责人 / 分类 / 循环)。
|
||||||
*
|
*
|
||||||
* ⚠️ **不带 status**:服务端 PATCH 会忽略它(见文件头说明),
|
* ⚠️ **不带 status**:服务端 PATCH 会忽略它(见文件头说明),
|
||||||
* 带上只会让人以为"改成功了",实际状态没动。
|
* 带上只会让人以为"改成功了",实际状态没动。
|
||||||
@@ -57,6 +89,15 @@ export async function updateTask(token: string, id: number, d: FbTaskDraft): Pro
|
|||||||
body['note'] = d.note;
|
body['note'] = d.note;
|
||||||
body['due_at'] = d.dueAt;
|
body['due_at'] = d.dueAt;
|
||||||
body['board_id'] = d.boardId;
|
body['board_id'] = d.boardId;
|
||||||
|
/* v79:负责人/分类/循环也走 PATCH(服务端这三个字段都认)。
|
||||||
|
⚠️ `assignee_id` 只在**明确要改**(>=0)时才传:编辑表单里没有负责人这一项,
|
||||||
|
传 0 会把原来指派的负责人**静默清空**(与 v77「没传 ≠ 清空」同一类 bug)。
|
||||||
|
⚠️ 分类和循环则**必须**照传(0 / 空串就是"清空",那是合法操作)。 */
|
||||||
|
if (d.assigneeId >= 0) {
|
||||||
|
body['assignee_id'] = d.assigneeId;
|
||||||
|
}
|
||||||
|
body['category_id'] = d.categoryId;
|
||||||
|
body['repeat_rule'] = d.repeat;
|
||||||
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
|
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
|
||||||
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
||||||
}
|
}
|
||||||
@@ -74,3 +115,62 @@ export async function deleteTask(token: string, id: number): Promise<FbApiResult
|
|||||||
const opts: FbApiOptions = { method: 'DELETE', token: token };
|
const opts: FbApiOptions = { method: 'DELETE', token: token };
|
||||||
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
|
v79(用户 2026-10-22):「鸿蒙端的功能,也需要同样适配……
|
||||||
|
任务模块的功能需要完善。」
|
||||||
|
⇒ 下面这些是网页 v78「就地面板」在端上的对等物。
|
||||||
|
⚠️ 除 `getTask` 外全部返回 `FbApiResult`,且**服务端都会回整条任务** ——
|
||||||
|
调用方拿到 `res.data` 直接重画面板,不用再补一次 GET。
|
||||||
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 取一条任务的详情(含子任务 / 留言 / 附件 / 共享人) */
|
||||||
|
export async function getTask(token: string, id: number): Promise<FbApiResult> {
|
||||||
|
const opts: FbApiOptions = { method: 'GET', token: token };
|
||||||
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挪到另一个看板(网页版「分组」的等价物)。
|
||||||
|
*
|
||||||
|
* ⚠️ 走 `/move` 而不是 `PATCH`:`/move` 会顺手把这一列重排 position;
|
||||||
|
* PATCH 只改 board_id,同列里会留下重复的 position(网页端拖拽用的是 /move)。
|
||||||
|
*/
|
||||||
|
export async function moveTask(token: string, id: number, boardId: number): Promise<FbApiResult> {
|
||||||
|
const body: Record<string, Object> = {};
|
||||||
|
body['board_id'] = boardId;
|
||||||
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||||
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/move', opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发一条留言(服务端只回 `{id}`,要整块内容就再 `getTask` 一次) */
|
||||||
|
export async function addTaskComment(token: string, id: number, content: string): Promise<FbApiResult> {
|
||||||
|
const body: Record<string, Object> = {};
|
||||||
|
body['content'] = content;
|
||||||
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||||
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/comments', opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传一个附件(图片/文档都行,服务端按 mime 自己分)。
|
||||||
|
*
|
||||||
|
* @return 空串 = 成功;否则是给人看的错误说明(与 `fbUploadOne` 的约定一致)
|
||||||
|
*/
|
||||||
|
export async function uploadTaskAttach(token: string, id: number, uri: string): Promise<string> {
|
||||||
|
return await fbUploadOne('/api/v1/tasks/' + id.toString() + '/attach', 'file', uri, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删一个附件(列表里点 ✕) */
|
||||||
|
export async function delTaskAttach(token: string, id: number, aid: number): Promise<FbApiResult> {
|
||||||
|
/* ⚠️ 两处都踩过,别改回去:
|
||||||
|
① 路由只解析「资源/id/子动作」**三段** ⇒ 不能写成 `/attach/<aid>`,
|
||||||
|
那样服务端只会拿到 `$sub='attach'`,附件 id 被静默丢掉;
|
||||||
|
② 附件 id 走 **query**,不要塞 JSON body —— 实测 `DELETE` 带 JSON 体时
|
||||||
|
服务端读到的是空(`Api::param` 拿不到),症状是
|
||||||
|
「要指定删哪个附件」400,而请求看起来完全正常。 */
|
||||||
|
const q: Record<string, string> = {};
|
||||||
|
q['attachment_id'] = aid.toString();
|
||||||
|
const opts: FbApiOptions = { method: 'DELETE', token: token, query: q };
|
||||||
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/attach', opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -702,6 +702,78 @@ export interface FbBoardOpt {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分类选项(任务的「分类」;`id=0` = 不分类) */
|
||||||
|
export interface FbCatOpt {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务元数据(v79):**全部**看板 + 全部分类 + 状态/循环词表。
|
||||||
|
*
|
||||||
|
* 🔴 为什么单开一个类型、而不是继续从任务列表里反推看板:
|
||||||
|
* 反推只能看见"**有任务的**看板" ⇒ 刚建好、还没放任务的看板在端上永远选不到
|
||||||
|
* (用户建了个看板,端上挪不过去 —— 这在 v78 网页版是能做的)。
|
||||||
|
* 元数据走 `GET /tasks?view=boards`(很轻,不带任务)。
|
||||||
|
*/
|
||||||
|
export interface FbTaskMeta {
|
||||||
|
boards: FbBoardOpt[];
|
||||||
|
cats: FbCatOpt[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 子任务(面板里那几行;只勾状态、不展开二级) */
|
||||||
|
export interface FbTaskKid {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务下的一条留言 */
|
||||||
|
export interface FbTaskComment {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
/** 留言人显示名(服务端已按"我怎么称呼他"处理过) */
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务下的一个附件 */
|
||||||
|
export interface FbTaskAttach {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
/** image / pdf / doc / file …(决定画缩略图还是画图标) */
|
||||||
|
kind: string;
|
||||||
|
isImage: boolean;
|
||||||
|
/** 存储键(`photo/xxx.jpg`),画图时交给 fbImageUrl */
|
||||||
|
key: string;
|
||||||
|
sizeWord: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务详情(v79 面板用):列表行 + 只有详情接口才给的那几块。
|
||||||
|
*
|
||||||
|
* ⚠️ `base` 复用 `FbTaskItem`:用户点开时先用列表那一份渲染(不空白),
|
||||||
|
* 详情接口回来再把 `base` 换掉。两处形状一致才不会"点开闪一下再变样"。
|
||||||
|
*/
|
||||||
|
export interface FbTaskDetail {
|
||||||
|
base: FbTaskItem;
|
||||||
|
/** 负责人 id(编辑时回填;0 = 未指派) */
|
||||||
|
assigneeId: number;
|
||||||
|
/** 负责人显示名(`base.assignee` 的同源,详情以这里为准) */
|
||||||
|
categoryId: number;
|
||||||
|
category: string;
|
||||||
|
/** 循环规则(day/week/month/year;空 = 不循环) */
|
||||||
|
repeat: string;
|
||||||
|
/** 父任务标题(子任务才有;空 = 本来就是顶层任务) */
|
||||||
|
parentTitle: string;
|
||||||
|
/** 建这条的人显示名 */
|
||||||
|
creator: string;
|
||||||
|
kids: FbTaskKid[];
|
||||||
|
comments: FbTaskComment[];
|
||||||
|
attachs: FbTaskAttach[];
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 纪念日(days)
|
// 纪念日(days)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -39,24 +39,26 @@
|
|||||||
|
|
||||||
import { FbColors, FbMetric, FB_MASK } from '../../common/Theme';
|
import { FbColors, FbMetric, FB_MASK } from '../../common/Theme';
|
||||||
import { FbAppState, fbAppState } from '../../common/Store';
|
import { FbAppState, fbAppState } from '../../common/Store';
|
||||||
import { FbCalEvent, FbHomeData, FbMark, FbMemoryItem, FbRecentDay, FbTaskItem,
|
import { FbCalEvent, FbCatOpt, FbHomeData, FbMark, FbMemoryItem, FbRecentDay, FbTaskItem,
|
||||||
|
FbBoardOpt, FbTaskDetail, FbTaskMeta,
|
||||||
FbWeather, isEditableEvent } from '../../model/Types';
|
FbWeather, isEditableEvent } from '../../model/Types';
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
import { parseHome } from '../../data/FbParse';
|
import { parseHome } from '../../data/FbParse';
|
||||||
import { loadModule } from '../../data/FbModuleLoad';
|
import { loadModule, loadPath } from '../../data/FbModuleLoad';
|
||||||
import { FB_CAL_SETTINGS, FbCalStyle } from '../../data/FbCalSettings';
|
import { FB_CAL_SETTINGS, FbCalStyle } from '../../data/FbCalSettings';
|
||||||
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
||||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||||
import { setTaskStatus } from '../../data/FbTaskRepo';
|
import { parseTaskMeta } from '../../data/FbParse';
|
||||||
import { FB_CARD_FORMS } from '../../data/FbCardForms';
|
import { FB_CARD_FORMS } from '../../data/FbCardForms';
|
||||||
import { fbApi, FbApiResult } from '../../data/FbHttp';
|
import { fbApi, FbApiResult } from '../../data/FbHttp';
|
||||||
import { FbSectionTitle } from '../../components/Widgets';
|
import { FbSectionTitle } from '../../components/Widgets';
|
||||||
import { FbStatCell } from '../../components/FbStatCell';
|
import { FbStatCell } from '../../components/FbStatCell';
|
||||||
import { FbDayEventPanel } from '../../components/FbDayEventPanel';
|
import { FbDayEventPanel } from '../../components/FbDayEventPanel';
|
||||||
import { FbEventSheet } from '../../components/FbEventSheet';
|
import { FbEventSheet } from '../../components/FbEventSheet';
|
||||||
import { FbTaskDetailSheet } from '../../components/FbTaskDetailSheet';
|
import { FbTaskPanel } from '../../components/FbTaskPanel';
|
||||||
|
import { FbTaskEditSheet } from '../../components/FbTaskEditSheet';
|
||||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||||
@@ -142,14 +144,36 @@ export struct FbHomePage {
|
|||||||
/** 丢弃在途请求的令牌(连点两条日程时,先回来的那条不该覆盖后点的) */
|
/** 丢弃在途请求的令牌(连点两条日程时,先回来的那条不该覆盖后点的) */
|
||||||
@Local evtToken: number = 0;
|
@Local evtToken: number = 0;
|
||||||
|
|
||||||
/** 摘要卡显示的那条任务(非 null = 任务详情卡开着) */
|
/** 摘要卡显示的那条任务(非 null = 任务面板开着) */
|
||||||
@Local tskBase: FbTaskItem | null = null;
|
@Local tskBase: FbTaskItem | null = null;
|
||||||
/** 任务卡上显示的当前状态(勾完**立刻**变,不等网络) */
|
/**
|
||||||
@Local tskStatus: string = 'todo';
|
* v79:面板里能用的**全部**看板 / 分类(`?view=boards`)。
|
||||||
/** 任务状态正在提交(防连点) */
|
*
|
||||||
@Local tskBusy: boolean = false;
|
* 🔴 为什么不在页面加载时就拉:首页 `/home` 不返回看板表,
|
||||||
/** 任务卡的提示(失败原因 / 成功说明) */
|
* 而"挪分组/选分类"只有点开任务后才用得上 —— 按需拉更省一次请求。
|
||||||
@Local tskNote: string = '';
|
* ⚠️ 拉的是**全部看板**(含空看板),不是从任务里反推
|
||||||
|
* (反推会让新建的空看板永远选不到)。
|
||||||
|
*/
|
||||||
|
@Local tskBoards: FbBoardOpt[] = [];
|
||||||
|
@Local tskCats: FbCatOpt[] = [];
|
||||||
|
/** 任务编辑表单是否打开(v79:在首页直接改,不再跳任务页) */
|
||||||
|
@Local tskEditOpen: boolean = false;
|
||||||
|
/** 编辑表单回填的字段 */
|
||||||
|
@Local tskEditId: number = 0;
|
||||||
|
@Local tskEditTitle: string = '';
|
||||||
|
@Local tskEditNote: string = '';
|
||||||
|
@Local tskEditDueAt: string = '';
|
||||||
|
@Local tskEditBoardId: number = 0;
|
||||||
|
@Local tskEditCatId: number = 0;
|
||||||
|
@Local tskEditRepeat: string = '';
|
||||||
|
/**
|
||||||
|
* 面板的"重画脉冲"(v79)。
|
||||||
|
*
|
||||||
|
* 🔴 为什么需要:编辑表单保存成功后,面板里的标题/截止/分类都变了,
|
||||||
|
* 但面板的数据是它**自己**拉的 —— 宿主只重拉首页列表,它不会跟着变。
|
||||||
|
* ⚠️ 别用"关掉再打开"代替:会闪一下、还会丢掉滚动位置。
|
||||||
|
*/
|
||||||
|
@Local tskTick: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 详情卡开着 ⇒ 底部悬浮快捷栏让位。
|
* 详情卡开着 ⇒ 底部悬浮快捷栏让位。
|
||||||
@@ -158,9 +182,9 @@ export struct FbHomePage {
|
|||||||
* `Index` 的 Stack** 里,层级比本页高 ⇒ 不让位就会盖住卡片底部的按钮、
|
* `Index` 的 Stack** 里,层级比本页高 ⇒ 不让位就会盖住卡片底部的按钮、
|
||||||
* 还把点击抢走(名册/日程页都踩过)。
|
* 还把点击抢走(名册/日程页都踩过)。
|
||||||
*/
|
*/
|
||||||
@Monitor('evtBase', 'tskBase')
|
@Monitor('evtBase', 'tskBase', 'tskEditOpen')
|
||||||
onDetailChanged(): void {
|
onDetailChanged(): void {
|
||||||
this.app.sheetCount = (this.evtBase !== null || this.tskBase !== null) ? 1 : 0;
|
this.app.sheetCount = (this.evtBase !== null || this.tskBase !== null || this.tskEditOpen) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
@@ -181,6 +205,12 @@ export struct FbHomePage {
|
|||||||
|
|
||||||
/** 返回手势:先关详情卡,再交给 `Index`(返回 true = 已消费) */
|
/** 返回手势:先关详情卡,再交给 `Index`(返回 true = 已消费) */
|
||||||
private backOnce(): boolean {
|
private backOnce(): boolean {
|
||||||
|
/* v79:**后打开的先关** —— 编辑表单是压在面板之上的,返回一次只该关它。
|
||||||
|
顺序反了会"按一次返回,两层一起没了"(用户会以为点错了)。 */
|
||||||
|
if (this.tskEditOpen) {
|
||||||
|
this.tskEditOpen = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (this.evtBase !== null) {
|
if (this.evtBase !== null) {
|
||||||
this.closeEvent();
|
this.closeEvent();
|
||||||
return true;
|
return true;
|
||||||
@@ -333,69 +363,50 @@ export struct FbHomePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// v73:任务详情卡
|
// v79:任务面板(v73 的"详情卡"升级成可就地处理的面板)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
private openTask(t: FbTaskItem): void {
|
private openTask(t: FbTaskItem): void {
|
||||||
this.tskBase = t;
|
this.tskBase = t;
|
||||||
this.tskStatus = t.status;
|
/* 看板/分类表按需拉一次(拉过就不再拉;它是"元数据",几条而己) */
|
||||||
this.tskBusy = false;
|
this.loadTaskMeta();
|
||||||
this.tskNote = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeTask(): void {
|
private closeTask(): void {
|
||||||
this.tskBase = null;
|
this.tskBase = null;
|
||||||
this.tskBusy = false;
|
this.tskEditOpen = false;
|
||||||
this.tskNote = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 拉全部看板 + 分类(面板的"挪分组"、表单的"分类"都要用) */
|
||||||
* 勾完成 / 取消完成。
|
private async loadTaskMeta(): Promise<void> {
|
||||||
*
|
if (this.tskBoards.length > 0) {
|
||||||
* 🔴🔴 **必须走 `/tasks/<id>/status`**(`setTaskStatus`)——
|
|
||||||
* 服务端 `PATCH /tasks/<id>` 会把请求里的 status **原样忽略**
|
|
||||||
* (见 `FbTaskRepo` 文件头),打 PATCH 会"看起来成功、状态没变"。
|
|
||||||
* ⚠️ 这是本项目已经踩过一次的坑,别改成 updateTask。
|
|
||||||
*
|
|
||||||
* ⚠️ 已完成 → 取消完成统一回 `todo`(**不回 `doing`**):
|
|
||||||
* 用户只想撤销一次误勾,把它标成"进行中"是替他做了他没表达的决定。
|
|
||||||
* (与任务页 `toggleDone` 同一套规则。)
|
|
||||||
*/
|
|
||||||
private async toggleTask(): Promise<void> {
|
|
||||||
const t: FbTaskItem | null = this.tskBase;
|
|
||||||
if (t === null || this.tskBusy) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.tskBusy = true;
|
await loadPath<FbTaskMeta>('tasks_meta', '/api/v1/tasks?view=boards', parseTaskMeta,
|
||||||
this.tskNote = '';
|
(m: FbTaskMeta): void => {
|
||||||
const next: string = this.tskStatus === 'done' ? 'todo' : 'done';
|
this.tskBoards = m.boards;
|
||||||
const res: FbApiResult = await setTaskStatus(FB_TOKEN_STORE.get(), t.id, next);
|
this.tskCats = m.cats;
|
||||||
this.tskBusy = false;
|
},
|
||||||
if (!res.ok) {
|
(note: string): void => {
|
||||||
this.tskNote = res.error !== '' ? res.error : '改状态失败,请稍后再试。';
|
/* ⚠️ 失败**不拦路**:面板里状态/子任务/留言都还能用,
|
||||||
return;
|
只是"换看板"那一排会少几个(甚至只剩默认看板)。 */
|
||||||
}
|
this.syncNote = note;
|
||||||
/* 本地先把状态改掉(卡上立刻有反馈),再重拉首页数据对齐派生字段
|
});
|
||||||
(逾期、看板计数、"近期任务"里这条还在不在,都是服务端算的)。
|
|
||||||
⚠️ 顺序不能反 —— 等网络回来再改,用户会以为"没点上"又点一次。 */
|
|
||||||
this.tskStatus = next;
|
|
||||||
this.tskNote = next === 'done' ? '已标记完成' : '已改回未完成';
|
|
||||||
this.load();
|
|
||||||
// 桌面卡片上的数字(今天日程/在做/待办)也可能受影响 —— 顺手推一次
|
|
||||||
this.pushCards();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 面板里点「编辑」→ 在首页就地打开编辑表单(v79,不再跳任务页) */
|
||||||
* 任务卡上点「改一改」→ 切到任务模块。
|
private openTaskEdit(d: FbTaskDetail): void {
|
||||||
*
|
this.tskEditId = d.base.id;
|
||||||
* ⚠️ 与日程同理:改标题/改截止/看板/负责人是一整套表单,属于任务页的职责。
|
this.tskEditTitle = d.base.title;
|
||||||
* 用户要的"弹详情、能勾完成"这里已经给了;"改内容"跳过去是对的。
|
this.tskEditNote = d.base.note;
|
||||||
*/
|
this.tskEditDueAt = d.base.dueAt;
|
||||||
private editTaskFromDetail(): void {
|
this.tskEditBoardId = d.base.boardId;
|
||||||
this.closeTask();
|
this.tskEditCatId = d.categoryId;
|
||||||
this.app.switchModule('tasks');
|
this.tskEditRepeat = d.repeat;
|
||||||
|
this.tskEditOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把最新数据推给桌面上所有日程卡片。
|
* 把最新数据推给桌面上所有日程卡片。
|
||||||
*
|
*
|
||||||
@@ -873,31 +884,61 @@ export struct FbHomePage {
|
|||||||
.transition(fbSheetTransition())
|
.transition(fbSheetTransition())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 任务详情卡(v73)====================
|
// ==================== 任务面板(v79:可就地处理)====================
|
||||||
|
/* 🔴 用户 2026-10-22:「在首页上可以直接修改编辑任务。」
|
||||||
|
⇒ v73 的"只读详情卡"升级成面板:状态 / 看板 / 子任务 / 附件 / 留言
|
||||||
|
全在这张卡里做完,**不跳任务模块**(跳模块是用户明确否掉的)。
|
||||||
|
⚠️ 面板根节点自带遮罩与 `Stack`,这里**不要**再包一层遮罩 ——
|
||||||
|
包了就会有两层暗色叠在一起(越点越黑)。 */
|
||||||
if (this.tskBase !== null) {
|
if (this.tskBase !== null) {
|
||||||
Column()
|
FbTaskPanel({
|
||||||
.width('100%')
|
taskId: this.tskBase.id,
|
||||||
.height('100%')
|
seed: this.tskBase,
|
||||||
.backgroundColor(FB_MASK)
|
boards: this.tskBoards,
|
||||||
.transition(fbMaskTransition())
|
|
||||||
.onClick((): void => {
|
|
||||||
this.closeTask();
|
|
||||||
})
|
|
||||||
|
|
||||||
FbTaskDetailSheet({
|
|
||||||
task: this.tskBase,
|
|
||||||
status: this.tskStatus,
|
|
||||||
busy: this.tskBusy,
|
|
||||||
note: this.tskNote,
|
|
||||||
accent: this.taskAccent(),
|
accent: this.taskAccent(),
|
||||||
onToggle: (): void => {
|
refreshTick: this.tskTick,
|
||||||
this.toggleTask();
|
|
||||||
},
|
|
||||||
onEdit: (): void => {
|
|
||||||
this.editTaskFromDetail();
|
|
||||||
},
|
|
||||||
onClose: (): void => {
|
onClose: (): void => {
|
||||||
this.closeTask();
|
this.closeTask();
|
||||||
|
},
|
||||||
|
onEdit: (d: FbTaskDetail): void => {
|
||||||
|
this.openTaskEdit(d);
|
||||||
|
},
|
||||||
|
onChanged: (): void => {
|
||||||
|
/* 一次动作会牵动"近期任务"这张卡的排序/计数(服务端算的)⇒ 重拉首页 */
|
||||||
|
this.load();
|
||||||
|
this.pushCards();
|
||||||
|
},
|
||||||
|
onDeleted: (): void => {
|
||||||
|
this.closeTask();
|
||||||
|
this.load();
|
||||||
|
this.pushCards();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.transition(fbSheetTransition())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 任务编辑表单(v79:就地改,不跳任务页)====================
|
||||||
|
if (this.tskEditOpen) {
|
||||||
|
FbTaskEditSheet({
|
||||||
|
accent: this.taskAccent(),
|
||||||
|
boards: this.tskBoards,
|
||||||
|
cats: this.tskCats,
|
||||||
|
editingId: this.tskEditId,
|
||||||
|
editingTitle: this.tskEditTitle,
|
||||||
|
editingNote: this.tskEditNote,
|
||||||
|
editingDueAt: this.tskEditDueAt,
|
||||||
|
editingBoardId: this.tskEditBoardId,
|
||||||
|
editingCatId: this.tskEditCatId,
|
||||||
|
editingRepeat: this.tskEditRepeat,
|
||||||
|
onClose: (): void => {
|
||||||
|
this.tskEditOpen = false;
|
||||||
|
},
|
||||||
|
onSaved: (): void => {
|
||||||
|
this.tskEditOpen = false;
|
||||||
|
/* 面板还开着 ⇒ 让它自己重拉一次(标题/截止/分类都变了) */
|
||||||
|
this.tskTick = this.tskTick + 1;
|
||||||
|
this.load();
|
||||||
|
this.pushCards();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.transition(fbSheetTransition())
|
.transition(fbSheetTransition())
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
||||||
import { FbAppState, fbAppState } from '../../common/Store';
|
import { FbAppState, fbAppState } from '../../common/Store';
|
||||||
import { FbBoardOpt, FbGroup, FbTaskItem } from '../../model/Types';
|
import { FbBoardOpt, FbCatOpt, FbGroup, FbTaskItem, FbTaskDetail, FbTaskMeta } from '../../model/Types';
|
||||||
import { fbGroupTask } from '../../model/DemoData';
|
import { fbGroupTask } from '../../model/DemoData';
|
||||||
import { parseTasks } from '../../data/FbParse';
|
import { parseTasks, parseTaskList, parseTaskMeta } from '../../data/FbParse';
|
||||||
import { loadModule } from '../../data/FbModuleLoad';
|
import { loadModule, loadPath } from '../../data/FbModuleLoad';
|
||||||
import { FbCheckCircle, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
import { FbCheckCircle, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||||
import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
|
import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
|
||||||
@@ -25,6 +25,7 @@ import { FbSheetClose } from '../../components/FbSheetClose';
|
|||||||
import { FB_BACK } from '../../common/FbBack';
|
import { FB_BACK } from '../../common/FbBack';
|
||||||
import { FB_SET_TASKS } from '../../model/FbSettingsMenus';
|
import { FB_SET_TASKS } from '../../model/FbSettingsMenus';
|
||||||
import { FbTaskEditSheet } from '../../components/FbTaskEditSheet';
|
import { FbTaskEditSheet } from '../../components/FbTaskEditSheet';
|
||||||
|
import { FbTaskPanel } from '../../components/FbTaskPanel';
|
||||||
import { setTaskStatus } from '../../data/FbTaskRepo';
|
import { setTaskStatus } from '../../data/FbTaskRepo';
|
||||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||||
import { FbApiResult } from '../../data/FbHttp';
|
import { FbApiResult } from '../../data/FbHttp';
|
||||||
@@ -77,12 +78,32 @@ export struct FbTasksPage {
|
|||||||
/** `2026-10-25 18:00`;空 = 没有期限 */
|
/** `2026-10-25 18:00`;空 = 没有期限 */
|
||||||
@Local editingDueAt: string = '';
|
@Local editingDueAt: string = '';
|
||||||
@Local editingBoardId: number = 0;
|
@Local editingBoardId: number = 0;
|
||||||
|
/** v79:分类 / 循环(编辑时回填;0 / '' = 不分类、不循环) */
|
||||||
|
@Local editingCatId: number = 0;
|
||||||
|
@Local editingRepeat: string = '';
|
||||||
/** 正在改状态的那条(防连点打重复请求) */
|
/** 正在改状态的那条(防连点打重复请求) */
|
||||||
@Local busyId: number = 0;
|
@Local busyId: number = 0;
|
||||||
/** 「分类与看板」浮层是否打开 */
|
/** 「分类与看板」浮层是否打开 */
|
||||||
@Local boardSheetOpen: boolean = false;
|
@Local boardSheetOpen: boolean = false;
|
||||||
/** 操作失败时的一行红字(如勾完成失败) */
|
/** 操作失败时的一行红字(如勾完成失败) */
|
||||||
@Local opNote: string = '';
|
@Local opNote: string = '';
|
||||||
|
/* ---- v79:任务面板(点卡片打开;与首页共用 `FbTaskPanel`)---- */
|
||||||
|
/** 面板正在看的那条(0 = 面板关着) */
|
||||||
|
@Local panelId: number = 0;
|
||||||
|
/** 先用列表里那一份渲染(不空白等网络) */
|
||||||
|
@Local panelSeed: FbTaskItem | null = null;
|
||||||
|
/** 面板重画脉冲(编辑保存后 +1) */
|
||||||
|
@Local panelTick: number = 0;
|
||||||
|
/**
|
||||||
|
* 全部看板 / 分类(`?view=boards`)。
|
||||||
|
*
|
||||||
|
* 🔴 原来 `boardOpts()` 是**从已加载任务里反推**的 ⇒ 只能看见"有任务的看板",
|
||||||
|
* 新建的空看板在端上永远选不到(网页能建、端上挪不过去)。
|
||||||
|
*/
|
||||||
|
@Local metaBoards: FbBoardOpt[] = [];
|
||||||
|
@Local metaCats: FbCatOpt[] = [];
|
||||||
|
/** 已完成列表(`?view=done`;看板视图**不含 done**,所以必须单独拉) */
|
||||||
|
@Local doneTasks: FbTaskItem[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
||||||
@@ -101,11 +122,17 @@ export struct FbTasksPage {
|
|||||||
|
|
||||||
private backOnce(): boolean {
|
private backOnce(): boolean {
|
||||||
/* v65:返回手势要**先关新的浮层**,再关设置菜单 ——
|
/* v65:返回手势要**先关新的浮层**,再关设置菜单 ——
|
||||||
顺序按"后打开的在前",否则按一次返回会跳关两层。 */
|
顺序按"后打开的在前",否则按一次返回会跳关两层。
|
||||||
|
v79:面板与编辑表单又叠了一层(编辑在面板之上),所以顺序是
|
||||||
|
编辑 → 面板 → 分类与看板 → 设置。 */
|
||||||
if (this.editOpen) {
|
if (this.editOpen) {
|
||||||
this.editOpen = false;
|
this.editOpen = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (this.panelId > 0) {
|
||||||
|
this.closePanel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (this.boardSheetOpen) {
|
if (this.boardSheetOpen) {
|
||||||
this.boardSheetOpen = false;
|
this.boardSheetOpen = false;
|
||||||
return true;
|
return true;
|
||||||
@@ -120,9 +147,30 @@ export struct FbTasksPage {
|
|||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
FB_BACK.register('tasks', (): boolean => this.backOnce());
|
FB_BACK.register('tasks', (): boolean => this.backOnce());
|
||||||
this.load();
|
this.load();
|
||||||
|
this.loadMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉全部看板 + 分类(筛选条、编辑表单、面板的"挪分组"共用一份) */
|
||||||
|
private async loadMeta(): Promise<void> {
|
||||||
|
await loadPath<FbTaskMeta>('tasks_meta', '/api/v1/tasks?view=boards', parseTaskMeta,
|
||||||
|
(m: FbTaskMeta): void => {
|
||||||
|
this.metaBoards = m.boards;
|
||||||
|
this.metaCats = m.cats;
|
||||||
|
},
|
||||||
|
(note: string): void => {
|
||||||
|
this.syncNote = note;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async load(): Promise<void> {
|
private async load(): Promise<void> {
|
||||||
|
/* 🔴 v79 修一个真 bug:设置里的「只看已完成」以前筛的是**看板视图**的结果,
|
||||||
|
而看板视图在服务端就写死了 `status <> 'done'`(`Task::boardView` 那条 SQL)
|
||||||
|
⇒ 筛出来永远是空,用户看到的是"已完成里一条都没有"。
|
||||||
|
现在改成真的打 `?view=done`(网页 `Task::doneList` 同一口径)。 */
|
||||||
|
if (this.statusFilter === 'done') {
|
||||||
|
await this.loadDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await loadModule<FbTaskItem[]>('tasks', parseTasks,
|
await loadModule<FbTaskItem[]>('tasks', parseTasks,
|
||||||
(rows: FbTaskItem[]): void => {
|
(rows: FbTaskItem[]): void => {
|
||||||
this.tasks = rows;
|
this.tasks = rows;
|
||||||
@@ -132,31 +180,38 @@ export struct FbTasksPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 已完成列表(单独缓存键:与看板视图**形状不同**,不能共用 'tasks' 那个键) */
|
||||||
|
private async loadDone(): Promise<void> {
|
||||||
|
await loadPath<FbTaskItem[]>('tasks_done', '/api/v1/tasks?view=done', parseTaskList,
|
||||||
|
(rows: FbTaskItem[]): void => {
|
||||||
|
this.doneTasks = rows;
|
||||||
|
},
|
||||||
|
(note: string): void => {
|
||||||
|
this.syncNote = note;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 顶部提示(同步失败 / 未登录时显示一行小字) */
|
/** 顶部提示(同步失败 / 未登录时显示一行小字) */
|
||||||
@Local syncNote: string = '';
|
@Local syncNote: string = '';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置浮层开着 ⇒ 让底部悬浮快捷栏让位。
|
* 本页任何一个浮层开着 ⇒ 底部悬浮快捷栏让位。
|
||||||
*
|
*
|
||||||
* 🔴 悬浮栏在**更外层 Index 的 Stack** 里,层级比本页高 ⇒
|
* 🔴 悬浮栏在**更外层 Index 的 Stack** 里,层级比本页高 ⇒
|
||||||
* 浮层底部的按钮会被它盖住、点击被抢走(名册/日程页都踩过)。
|
* 浮层底部的按钮会被它盖住、点击被抢走(名册/日程页都踩过)。
|
||||||
|
*
|
||||||
|
* ⚠️🔴 本页所有浮层开关(设置 / 分类看板 / 新建编辑 / 任务面板)**只能有
|
||||||
|
* **这一个** `@Monitor` 赋 `sheetCount`,且必须**取并集**。
|
||||||
|
* 曾经这里是两个函数(一个只管 `boardSheetOpen`、一个管四个)——
|
||||||
|
* 两个都监听同一个字段、都写同一个值,关掉其中一个时谁最后执行不确定
|
||||||
|
* ⇒ 会把另一个还开着的浮层"顺便让位结束",悬浮栏浮上来盖住面板。
|
||||||
|
* (2026-10-22 自查发现;守卫里有断言防回退)
|
||||||
*/
|
*/
|
||||||
@Monitor('settingsOpen')
|
@Monitor('settingsOpen', 'boardSheetOpen', 'editOpen', 'panelId')
|
||||||
onSettingsChanged(): void {
|
onAnySheetChanged(): void {
|
||||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
const any: boolean = this.settingsOpen || this.boardSheetOpen || this.editOpen || this.panelId > 0;
|
||||||
}
|
fbAppState().sheetCount = any ? 1 : 0;
|
||||||
|
|
||||||
/** 新建/编辑浮层开着 ⇒ 底部悬浮快捷栏也要让位(与 `settingsOpen` 不会同时开) */
|
|
||||||
@Monitor('editOpen')
|
|
||||||
onEditChanged(): void {
|
|
||||||
fbAppState().sheetCount = this.editOpen ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 「分类与看板」浮层同理 */
|
|
||||||
@Monitor('boardSheetOpen')
|
|
||||||
onBoardSheetChanged(): void {
|
|
||||||
fbAppState().sheetCount = this.boardSheetOpen ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开「新建」浮层(清空回填字段) */
|
/** 打开「新建」浮层(清空回填字段) */
|
||||||
@@ -166,17 +221,46 @@ export struct FbTasksPage {
|
|||||||
this.editingNote = '';
|
this.editingNote = '';
|
||||||
this.editingDueAt = '';
|
this.editingDueAt = '';
|
||||||
this.editingBoardId = 0;
|
this.editingBoardId = 0;
|
||||||
|
this.editingCatId = 0;
|
||||||
|
this.editingRepeat = '';
|
||||||
this.opNote = '';
|
this.opNote = '';
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开「编辑」浮层(把这条任务的字段回填进去) */
|
/**
|
||||||
private openEdit(t: FbTaskItem): void {
|
* 打开任务面板(v79)。
|
||||||
this.editingId = t.id;
|
*
|
||||||
this.editingTitle = t.title;
|
* 🔴 用户 2026-10-22:「任务模块的功能需要完善。」
|
||||||
this.editingNote = t.note;
|
* 以前点卡片**直接进编辑表单** —— 想勾个子任务、看留言、加个照片都没地方去;
|
||||||
this.editingDueAt = t.dueAt;
|
* 而且误触一下卡片就开始改标题。现在点卡片先开面板(只读 + 就地动作),
|
||||||
this.editingBoardId = t.boardId;
|
* 面板里的「编辑」才进表单。
|
||||||
|
*/
|
||||||
|
private openPanel(t: FbTaskItem): void {
|
||||||
|
this.panelId = t.id;
|
||||||
|
this.panelSeed = t;
|
||||||
|
this.opNote = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private closePanel(): void {
|
||||||
|
this.panelId = 0;
|
||||||
|
this.panelSeed = null;
|
||||||
|
this.editOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开「编辑」浮层(回填字段)。
|
||||||
|
*
|
||||||
|
* ⚠️ 入参是**详情**不是列表行:分类/循环只有详情接口才给,
|
||||||
|
* 拿列表行回填会把这两项当成"空"提交 ⇒ 一保存就清空(v77 同类坑)。
|
||||||
|
*/
|
||||||
|
private openEdit(d: FbTaskDetail): void {
|
||||||
|
this.editingId = d.base.id;
|
||||||
|
this.editingTitle = d.base.title;
|
||||||
|
this.editingNote = d.base.note;
|
||||||
|
this.editingDueAt = d.base.dueAt;
|
||||||
|
this.editingBoardId = d.base.boardId;
|
||||||
|
this.editingCatId = d.categoryId;
|
||||||
|
this.editingRepeat = d.repeat;
|
||||||
this.opNote = '';
|
this.opNote = '';
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
}
|
}
|
||||||
@@ -220,8 +304,16 @@ export struct FbTasksPage {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 看板列表(从已加载的任务里反推:`/tasks` 的 boards 里没任务的那些画出来也没用) */
|
/**
|
||||||
|
* 看板列表:**优先用元数据**(`?view=boards`,含空看板)。
|
||||||
|
*
|
||||||
|
* ⚠️ 元数据还没回来时退回"从任务里反推"(老行为),这样首屏不会空着;
|
||||||
|
* 但**真正可选**的看板以元数据为准 —— 反推漏掉空看板是老 bug。
|
||||||
|
*/
|
||||||
private boardOpts(): FbBoardOpt[] {
|
private boardOpts(): FbBoardOpt[] {
|
||||||
|
if (this.metaBoards.length > 0) {
|
||||||
|
return this.metaBoards;
|
||||||
|
}
|
||||||
const out: FbBoardOpt[] = [];
|
const out: FbBoardOpt[] = [];
|
||||||
const seen: number[] = [];
|
const seen: number[] = [];
|
||||||
for (const t of this.tasks) {
|
for (const t of this.tasks) {
|
||||||
@@ -326,6 +418,7 @@ export struct FbTasksPage {
|
|||||||
.borderRadius(FbMetric.radiusPill)
|
.borderRadius(FbMetric.radiusPill)
|
||||||
.onClick((): void => {
|
.onClick((): void => {
|
||||||
this.statusFilter = '';
|
this.statusFilter = '';
|
||||||
|
this.load();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,6 +498,8 @@ export struct FbTasksPage {
|
|||||||
if (action === 'taskdone') {
|
if (action === 'taskdone') {
|
||||||
this.statusFilter = 'done';
|
this.statusFilter = 'done';
|
||||||
this.settingsOpen = false;
|
this.settingsOpen = false;
|
||||||
|
/* 🔴 必须重拉:看板视图里根本没有 done,本地筛只会得到空列表 */
|
||||||
|
this.load();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'taskboards') {
|
if (action === 'taskboards') {
|
||||||
@@ -417,16 +512,46 @@ export struct FbTasksPage {
|
|||||||
.transition(fbSheetTransition())
|
.transition(fbSheetTransition())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 任务面板(v79:点卡片打开,可就地处理)----
|
||||||
|
/* ⚠️ 面板自带遮罩与 Stack,这里**不要**再包一层 —— 两层暗色会叠在一起。 */
|
||||||
|
if (this.panelId > 0) {
|
||||||
|
FbTaskPanel({
|
||||||
|
taskId: this.panelId,
|
||||||
|
seed: this.panelSeed,
|
||||||
|
boards: this.boardOpts(),
|
||||||
|
accent: this.accent,
|
||||||
|
refreshTick: this.panelTick,
|
||||||
|
onClose: (): void => {
|
||||||
|
this.closePanel();
|
||||||
|
},
|
||||||
|
onEdit: (d: FbTaskDetail): void => {
|
||||||
|
this.openEdit(d);
|
||||||
|
},
|
||||||
|
onChanged: (): void => {
|
||||||
|
/* 状态/看板/子任务都可能改变排序与计数 ⇒ 重拉列表 */
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
onDeleted: (): void => {
|
||||||
|
this.closePanel();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.transition(fbSheetTransition())
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 新建 / 编辑任务浮层(v65)----
|
// ---- 新建 / 编辑任务浮层(v65)----
|
||||||
if (this.editOpen) {
|
if (this.editOpen) {
|
||||||
FbTaskEditSheet({
|
FbTaskEditSheet({
|
||||||
accent: this.accent,
|
accent: this.accent,
|
||||||
boards: this.boardOpts(),
|
boards: this.boardOpts(),
|
||||||
|
cats: this.metaCats,
|
||||||
editingId: this.editingId,
|
editingId: this.editingId,
|
||||||
editingTitle: this.editingTitle,
|
editingTitle: this.editingTitle,
|
||||||
editingNote: this.editingNote,
|
editingNote: this.editingNote,
|
||||||
editingDueAt: this.editingDueAt,
|
editingDueAt: this.editingDueAt,
|
||||||
editingBoardId: this.editingBoardId,
|
editingBoardId: this.editingBoardId,
|
||||||
|
editingCatId: this.editingCatId,
|
||||||
|
editingRepeat: this.editingRepeat,
|
||||||
onClose: (): void => {
|
onClose: (): void => {
|
||||||
this.editOpen = false;
|
this.editOpen = false;
|
||||||
},
|
},
|
||||||
@@ -434,6 +559,8 @@ export struct FbTasksPage {
|
|||||||
this.editOpen = false;
|
this.editOpen = false;
|
||||||
// 服务端会派生逾期/看板色,保存后重拉一遍
|
// 服务端会派生逾期/看板色,保存后重拉一遍
|
||||||
this.load();
|
this.load();
|
||||||
|
// 面板还开着 ⇒ 让它自己重拉(标题/截止/分类都变了)
|
||||||
|
this.panelTick = this.panelTick + 1;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.transition(fbSheetTransition())
|
.transition(fbSheetTransition())
|
||||||
@@ -663,14 +790,19 @@ export struct FbTasksPage {
|
|||||||
.borderRadius(FbMetric.radius)
|
.borderRadius(FbMetric.radius)
|
||||||
.margin({ bottom: 8 })
|
.margin({ bottom: 8 })
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
/* 🔴 v65:整卡可点 → 打开编辑(原来列表项**没有 `onClick`** ⇒ "点不进去") */
|
/* 🔴 v65:整卡可点 ⇒ "点得进去";v79 起进的是**面板**(不是直接进编辑表单) */
|
||||||
.onClick((): void => {
|
.onClick((): void => {
|
||||||
this.openEdit(t);
|
this.openPanel(t);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前数据源:看板视图 或 已完成列表(两者的形状不同,不能混用) */
|
||||||
|
private source(): FbTaskItem[] {
|
||||||
|
return this.statusFilter === 'done' ? this.doneTasks : this.tasks;
|
||||||
|
}
|
||||||
|
|
||||||
private filtered(): FbTaskItem[] {
|
private filtered(): FbTaskItem[] {
|
||||||
let list: FbTaskItem[] = this.tasks;
|
let list: FbTaskItem[] = this.source();
|
||||||
if (this.boardFilter.length > 0) {
|
if (this.boardFilter.length > 0) {
|
||||||
list = list.filter((t: FbTaskItem): boolean => t.board === this.boardFilter);
|
list = list.filter((t: FbTaskItem): boolean => t.board === this.boardFilter);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user