Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbTaskEditSheet.ets
T

357 lines
12 KiB
Plaintext
Raw Normal View History

/**
* 家书 FamilyBook · 「新建 / 编辑任务」浮层(鸿蒙端)
*
* 对应网页版 `templates/tasks/_edit_form.php`。
*
* 🔴 用户 2026-10-20:「新建任务、设置这些东西都这都没有。」
* 端上「新建任务」按钮原来**没有 `onTap`**,列表项也没有 `onClick`。
*
* ⚠️ 编辑态**不显示状态选择**:状态只走 `/tasks/<id>/status` 子动作
* (服务端 `PATCH` 会忽略请求里的 status,见 `FbTaskRepo` 文件头)。
* 在这里放个状态选择器 → 用户改完保存 → 状态没变 ⇒ 典型的"看起来成功了"。
* 改成完成请用列表上的那个圈(见 `FbTasksPage` 的 `toggleDone`)。
*/
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme';
import { FbBoardOpt } from '../model/Types';
import { FbSheetClose } from './FbSheetClose';
import { FbSheetRing } from './FbSheetRing';
import { FbDateTimeRow, FbDateTimePicker, fbIsoOf, fbJoinDateTime } from './FbDateTimeField';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { createTask, updateTask, FbTaskDraft } from '../data/FbTaskRepo';
import { FbApiResult } from '../data/FbHttp';
@ComponentV2
export struct FbTaskEditSheet {
/** 模块主题色(任务 = 赭红) */
@Param accent: string = '#A8574F';
/** 可选的看板(父页面从已加载任务里反推出来) */
@Param boards: FbBoardOpt[] = [];
/** 编辑的这条(0 = 新建) */
@Param editingId: number = 0;
@Param editingTitle: string = '';
@Param editingNote: string = '';
@Param editingDueAt: string = '';
@Param editingBoardId: number = 0;
@Event onClose: () => void = (): void => {};
@Event onSaved: () => void = (): void => {};
@Local title: string = '';
@Local note: string = '';
@Local boardId: number = 0;
/** 截止日期 `YYYY-MM-DD`,空 = 没有期限 */
@Local dueDate: string = '';
/** 截止时分 */
@Local dueTime: string = '18:00';
@Local pickerOpen: boolean = false;
@Local saving: boolean = false;
@Local note2: string = '';
aboutToAppear(): void {
this.title = this.editingTitle;
this.note = this.editingNote;
this.boardId = this.editingBoardId;
// `2026-10-25 18:00` → 日期 + 时分(服务端给的是空格分隔)
const raw: string = this.editingDueAt.trim();
if (raw.length >= 16) {
this.dueDate = raw.substring(0, 10);
this.dueTime = raw.substring(11, 16);
} else if (raw.length >= 10) {
this.dueDate = raw.substring(0, 10);
}
}
build() {
/* 🔴🔴 根必须 `Stack`(见 pitfalls.md:全屏遮罩不能当 Column 子项排) */
Stack({ alignContent: Alignment.Center }) {
Column()
.width('100%')
.height('100%')
.backgroundColor(FB_MASK)
.onClick((): void => {
this.onClose();
})
Column() {
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
Column({ space: 12 }) {
// ---- 标题栏 ----
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(this.editingId > 0 ? '编辑任务' : '新建任务')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
Text('写清楚要做什么;期限可以留空')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accent
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Scroll() {
Column({ space: 12 }) {
// ---- 标题 ----
this.textField('做什么', '如「交电费」「给爸配眼镜」', this.title,
(v: string): void => {
this.title = v;
this.note2 = '';
})
// ---- 备注 ----
this.areaField('备注', '想补充的细节(可空)', this.note,
(v: string): void => {
this.note = v;
})
// ---- 截止时间 ----
Column({ space: 6 }) {
Row({ space: 8 }) {
Text('期限')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width(28)
if (this.dueDate !== '') {
Text('不设期限')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusPill)
.onClick((): void => {
this.dueDate = '';
})
} else {
Column().layoutWeight(1)
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (this.dueDate === '') {
Row({ space: 6 }) {
Image($r('app.media.fb_ic_plus'))
.width(12)
.height(12)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(this.accent))
Text('设一个期限')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
}
.padding({ left: 10, right: 12, top: 7, bottom: 7 })
.backgroundColor(fbAlpha(this.accent, 0.08))
.borderRadius(FbMetric.radiusPill)
.alignItems(VerticalAlign.Center)
.onClick((): void => {
this.dueDate = fbIsoOf(new Date());
this.pickerOpen = true;
})
} else {
FbDateTimeRow({
label: '截止',
date: this.dueDate,
time: this.dueTime,
accent: this.accent,
onTap: (): void => {
this.pickerOpen = true;
}
})
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// ---- 看板 ----
Column({ space: 6 }) {
Text('看板')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Scroll() {
Row({ space: 8 }) {
this.boardChip(0, '不指定', '#A8574F')
ForEach(this.boards, (b: FbBoardOpt) => {
this.boardChip(b.id, b.name, b.color)
}, (b: FbBoardOpt): string => 'tb_' + b.id.toString())
}
.padding({ right: 4 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.constraintSize({ maxHeight: 360 })
.width('100%')
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
if (this.note2 !== '') {
Text(this.note2)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
.maxLines(3)
}
// ---- 保存 ----
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '建好这件事'))
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.height(44)
.borderRadius(FbMetric.radiusS)
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98))
.onClick((): void => {
this.save();
})
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
// ---- 日期时间选择 ----
if (this.pickerOpen) {
FbDateTimePicker({
title: '这个任务什么时候到期',
date: this.dueDate !== '' ? this.dueDate : fbIsoOf(new Date()),
time: this.dueTime,
accent: this.accent,
onConfirm: (d: string, t: string): void => {
this.dueDate = d;
this.dueTime = t;
this.pickerOpen = false;
},
onCancel: (): void => {
this.pickerOpen = false;
}
})
}
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
}
private async save(): Promise<void> {
if (this.saving) {
return;
}
const title: string = this.title.trim();
if (title === '') {
this.note2 = '任务得写清楚要做什么。';
return;
}
this.saving = true;
this.note2 = '';
const d: FbTaskDraft = {
title: title,
note: this.note.trim(),
// 没有期限就发空串(服务端 `Api::dt('due_at', '')` → null)
dueAt: this.dueDate !== '' ? fbJoinDateTime(this.dueDate, this.dueTime) : '',
boardId: this.boardId,
status: 'todo'
};
const token: string = FB_TOKEN_STORE.get();
const res: FbApiResult = this.editingId > 0
? await updateTask(token, this.editingId, d)
: await createTask(token, d);
this.saving = false;
if (!res.ok) {
this.note2 = res.error !== '' ? res.error : '保存失败,请稍后再试。';
return;
}
this.onSaved();
}
/** 看板单选胶囊 */
@Builder
boardChip(id: number, name: string, color: string) {
Text(name)
.fontSize(FbMetric.fontTiny)
.fontColor(this.boardId === id ? '#FFFFFF' : FbColors.muted)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(this.boardId === id ? (id === 0 ? this.accent : color) : FbColors.inputBg)
.borderRadius(FbMetric.radiusPill)
.onClick((): void => {
this.boardId = id;
})
}
@Builder
textField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
Row({ space: 8 }) {
Text(label)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width(44)
TextInput({ text: value, placeholder: placeholder })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(42)
.layoutWeight(1)
.padding({ left: 10, right: 10 })
.onChange((v: string): void => {
onChange(v);
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
@Builder
areaField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
Row({ space: 8 }) {
Text(label)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width(44)
TextArea({ text: value, placeholder: placeholder })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(80)
.layoutWeight(1)
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
.onChange((v: string): void => {
onChange(v);
})
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
}