v65/v66:六个空壳模块接线 + 桌面服务卡片
用户 2026-10-20:
「做几个和 syncCalendar 差不多的卡片。另外其他的功能也逐步完成……旅程的
新建旅程啊,车辆管理、出行方式、节点设置……资源也点不进去……任务也点不
进去,也没法完成……纪念日、新增纪念日……财务的显示也有问题……笔记现在
也点不进去,记笔记也记不了。」
v65 —— 六个模块(任务 / 资源 / 纪念日 / 笔记 / 账本 / 旅程)原本是**同一套
空壳模板**复制出来的:`FbPrimaryButton` 没有 `onTap`、`ListItem` 没有
`onClick`、设置菜单的 `action` 全是空串。这一版把它们全部接上真实接口:
· 任务:新建 / 编辑(`FbTaskEditSheet` + `FbTaskRepo`)、列表可点、勾完成
(走 `/tasks/<id>/status` —— 服务端 `PATCH` **显式忽略** status,走 Patch
会得到"看起来成功的假成功")、按看板筛选
· 资源:新增 / 查看 / 改 / 删(`FbResourceSheet` + `FbResourceRepo`);
过滤维度从**并不存在的**「证件 / 资料」改成按权限与归属
(`resources` 表没有 category 列,那个筛选一直是筛空的)
· 纪念日:新增 / 编辑(`FbDayEditSheet` + `FbDayRepo`);派生生日
(id=0、来自名册)只给提示不给编辑 —— 点开 `/days/0` 必然 404
· 笔记:记一条 / 打开 / 改(`FbNoteSheet` + `FbNoteRepo`),正文按 blocks 存
· 账本:记一笔(`FbLedgerEntrySheet` + `FbLedgerRepo`);修掉财务显示自相
矛盾 —— 资产/负债改回**服务端 summary**(原来端上按金额正负号重算,与
服务端"负债取绝对值"的口径打架,会算出 净资产 ≠ 资产 − 负债)
· 旅程:题图卡片(封面 + 标题 + 城市)+ 行程 + 新建旅程 + 新建节点
服务端同时修掉两处**静默数据丢失**(网页端因提交格式凑巧没暴露,只有走
API 的客户端会中招):
· 新建笔记传的是 `body`,而 `Note::create()` 只读 `blocks` ⇒ 新笔记只有标题
· 改笔记时 `blocks` 恒为空,而 `Note::update()` 是整组替换 ⇒ 只改标题就把
正文全删光;另修 `annivPeopleOf()`(`find()` 的 SQL 不含 people,原来那个
兜底恒为空 ⇒ 只改纪念日标题会清空关联人)
v66 —— 桌面服务卡片(用户选了「桌面服务卡片」这一项):
· `FbFormAbility`(FormExtensionAbility)+ `form_config.json` 三块尺寸:
2×2 今日概览 / 2×4 左右分栏 / 4×4 今日全景
· `FbCardData` 只打 `/home` 一个接口(与网页首页同源),数据**扁平化**后
由 `formBindingData` 推给卡片;断网时用上次成功的缓存并注明"离线"
· 卡片是后台进程 ⇒ 请求带 `authProbe: true`:401 **不能**触发全局"踢回
登录页"(那会顺手把正在用应用的人踢下线,v62 那次事故同一个坑)
· 点卡片 → `postCardAction(router, fbModule)` → `EntryAbility` 读 Want 参数
直接落到对应模块;补了 `onNewWant`(没有它,第二次点卡片"什么也没发生")
· 4×4 右上角 ↻ 走 `postAction(message)` → `onFormEvent` 重取数再推回
验证:`hvigorw assembleHap` BUILD SUCCESSFUL;打包后的 hap 里确认
`FbFormAbility`(type=form)与 `$profile:form_config` 都在,卡片代码进了
`ets/widgets.abc`。静态守卫 730 条全绿,反向验证 110 条注入全部能让守卫变红。
⚠️ 设备仍断连(`hdc list targets` 为空),真机尚未验收。
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 2×2 服务卡片(今日概览)
|
||||
*
|
||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
||||
*
|
||||
* 这块卡只回答一句话:**今天有几件事、最要紧的那件是什么**。
|
||||
* 所以版面是「日期 → 大数字 → 一行提示」,不做列表(2×2 放列表只会挤成蚂蚁)。
|
||||
*
|
||||
* ⚠️ 卡片里**不做任何计算**(不排序、不筛选、不拼日期):
|
||||
* 全部由 `data/FbCardData.ets` 算好塞进 `LocalStorage`(原因见那边的文件头)。
|
||||
* 这里只负责画 —— 三种尺寸共用同一份数据,数字必须一致。
|
||||
*/
|
||||
|
||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
||||
|
||||
let storage2x2 = new LocalStorage();
|
||||
|
||||
@Entry(storage2x2)
|
||||
@Component
|
||||
struct FbCard2x2 {
|
||||
@LocalStorageProp('state') state: string = 'loading';
|
||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
||||
@LocalStorageProp('weekday') weekday: string = '';
|
||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
||||
@LocalStorageProp('marks') marks: string = '';
|
||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
||||
@LocalStorageProp('doing') doing: number = 0;
|
||||
@LocalStorageProp('todo') todo: number = 0;
|
||||
@LocalStorageProp('overdue') overdue: number = 0;
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('daysJson') daysJson: string = '[]';
|
||||
@LocalStorageProp('hint') hint: string = '';
|
||||
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||
|
||||
/** 副标题:`星期二 九月初一`(缺哪个就不占位) */
|
||||
private subLine(): string {
|
||||
const parts: string[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
if (this.lunarShort !== '') {
|
||||
parts.push(this.lunarShort);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 最下面那一行:优先"最近的纪念日倒计时",其次"今天第一条日程"。
|
||||
*
|
||||
* ⚠️ 不显示"共 N 条"这种废话 —— 大数字已经说了。
|
||||
*/
|
||||
private footLine(): string {
|
||||
if (this.hint !== '') {
|
||||
return this.hint;
|
||||
}
|
||||
const days = JSON.parse(this.daysJson) as FbCardDay[];
|
||||
if (days.length > 0) {
|
||||
const d: FbCardDay = days[0];
|
||||
return d.days === 0 ? `${d.title} 就是今天` : `${d.title} 还有 ${d.days} 天`;
|
||||
}
|
||||
const evs = JSON.parse(this.eventsJson) as FbCardEvent[];
|
||||
if (evs.length > 0) {
|
||||
return `${evs[0].time} ${evs[0].title}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 右下角小字:逾期优先(那是要人马上处理的) */
|
||||
private statLine(): string {
|
||||
if (this.overdue > 0) {
|
||||
return `逾期 ${this.overdue}`;
|
||||
}
|
||||
if (this.todo > 0) {
|
||||
return `待办 ${this.todo}`;
|
||||
}
|
||||
return this.updatedAt;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 抬头:日期 + 日子标记 ----
|
||||
Row() {
|
||||
Column({ space: 1 }) {
|
||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
Text(this.subLine())
|
||||
.fontSize(10)
|
||||
.fontColor('#A8988A')
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
if (this.marks !== '') {
|
||||
Text(this.marks)
|
||||
.fontSize(9)
|
||||
.fontColor('#A84A28')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
.backgroundColor('#F7EFE6')
|
||||
.borderRadius(4)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Blank()
|
||||
|
||||
// ---- 大数字 ----
|
||||
Row({ space: 6 }) {
|
||||
Text(`${this.eventCount}`)
|
||||
.fontSize(34)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#A84A28')
|
||||
Column({ space: 1 }) {
|
||||
Text('件日程')
|
||||
.fontSize(11)
|
||||
.fontColor('#2C231D')
|
||||
Text(this.statLine())
|
||||
.fontSize(10)
|
||||
.fontColor(this.overdue > 0 ? '#C0533F' : '#A8988A')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Bottom)
|
||||
|
||||
Blank()
|
||||
|
||||
// ---- 一行提示 ----
|
||||
Text(this.footLine())
|
||||
.fontSize(10)
|
||||
.fontColor(this.hint !== '' ? '#C0533F' : '#7E6B5A')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
// 整卡点击 → 打开日程(这张卡的主角就是"今天有几件事")
|
||||
.onClick((): void => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: { fbModule: 'calendar' }
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 2×4 服务卡片(今日日程 · 左右分栏)
|
||||
*
|
||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
||||
*
|
||||
* 左边一列是"今天怎么样"(日期 / 农历 / 件数 / 待办),右边是**今天的前三条日程**。
|
||||
* 2×2 只能给个数字,说不出"是什么事";这块卡就是补那一步。
|
||||
*
|
||||
* ⚠️ 与 2×2 / 4×4 **同一份数据**(`data/FbCardData.ets` 算一次,三卡共用),
|
||||
* 所以件数一定一致 —— 不要在卡片里自己算。
|
||||
*/
|
||||
|
||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
||||
|
||||
let storage2x4 = new LocalStorage();
|
||||
|
||||
@Entry(storage2x4)
|
||||
@Component
|
||||
struct FbCard2x4 {
|
||||
@LocalStorageProp('state') state: string = 'loading';
|
||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
||||
@LocalStorageProp('weekday') weekday: string = '';
|
||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
||||
@LocalStorageProp('doing') doing: number = 0;
|
||||
@LocalStorageProp('todo') todo: number = 0;
|
||||
@LocalStorageProp('overdue') overdue: number = 0;
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('hint') hint: string = '';
|
||||
|
||||
private subLine(): string {
|
||||
const parts: string[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
if (this.lunarShort !== '') {
|
||||
parts.push(this.lunarShort);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** 右侧最多三条(再多 2×4 的高度放不下,会被裁掉半行) */
|
||||
private top3(): FbCardEvent[] {
|
||||
const all = JSON.parse(this.eventsJson) as FbCardEvent[];
|
||||
const out: FbCardEvent[] = [];
|
||||
for (let i = 0; i < all.length && i < 3; i++) {
|
||||
out.push(all[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
build() {
|
||||
Row({ space: 10 }) {
|
||||
// ---- 左栏:今天的"账" ----
|
||||
Column({ space: 2 }) {
|
||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
Text(this.subLine())
|
||||
.fontSize(10)
|
||||
.fontColor('#A8988A')
|
||||
.maxLines(1)
|
||||
|
||||
Blank()
|
||||
|
||||
Row({ space: 5 }) {
|
||||
Text(`${this.eventCount}`)
|
||||
.fontSize(30)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#A84A28')
|
||||
Text('件日程')
|
||||
.fontSize(11)
|
||||
.fontColor('#2C231D')
|
||||
.margin({ bottom: 4 })
|
||||
}
|
||||
.alignItems(VerticalAlign.Bottom)
|
||||
|
||||
Text(this.overdue > 0 ? `逾期 ${this.overdue} 件` : `待办 ${this.todo} 件`)
|
||||
.fontSize(10)
|
||||
.fontColor(this.overdue > 0 ? '#C0533F' : '#A8988A')
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('38%')
|
||||
.height('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// 分隔线(视觉上把"账"和"事"分开)
|
||||
Divider()
|
||||
.vertical(true)
|
||||
.strokeWidth(0.5)
|
||||
.color('#EFE6D8')
|
||||
.height('86%')
|
||||
|
||||
// ---- 右栏:今天的事 ----
|
||||
Column({ space: 0 }) {
|
||||
Text('今天')
|
||||
.fontSize(10)
|
||||
.fontColor('#A8988A')
|
||||
.width('100%')
|
||||
.margin({ bottom: 5 })
|
||||
|
||||
if (this.hint !== '') {
|
||||
Text(this.hint)
|
||||
.fontSize(11)
|
||||
.fontColor('#C0533F')
|
||||
.maxLines(2)
|
||||
.width('100%')
|
||||
} else if (this.top3().length === 0) {
|
||||
Text('今天没有安排')
|
||||
.fontSize(11)
|
||||
.fontColor('#A8988A')
|
||||
.width('100%')
|
||||
} else {
|
||||
ForEach(this.top3(), (e: FbCardEvent) => {
|
||||
Row({ space: 6 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(13)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(e.color)
|
||||
.flexShrink(0)
|
||||
Text(e.time)
|
||||
.fontSize(10)
|
||||
.fontColor('#7E6B5A')
|
||||
.width(34)
|
||||
.flexShrink(0)
|
||||
Text(e.title)
|
||||
.fontSize(11)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height(20)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (e: FbCardEvent) => e.key)
|
||||
}
|
||||
Blank()
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.onClick((): void => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: { fbModule: 'calendar' }
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 4×4 服务卡片(今日全景)
|
||||
*
|
||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
||||
*
|
||||
* 这块是"不用打开应用就能看完"的那一块:
|
||||
* 抬头(日期/农历)→ 四个数字(日程/在做/待办/逾期)→ 今日日程 → 最近纪念日。
|
||||
* 四块内容的信息密度正好对上 4×4;再塞(任务详情、往年的今天)就会被裁。
|
||||
*
|
||||
* ⚠️ 右上角那个 ↻ 是"手动刷新":
|
||||
* 动态卡片**不能自己发起网络请求**,只能 `postCardAction({action:'message'})`
|
||||
* 把消息发给 `FbFormAbility.onFormEvent`,由那边重新取数再推回来。
|
||||
*/
|
||||
|
||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
||||
|
||||
let storage4x4 = new LocalStorage();
|
||||
|
||||
@Entry(storage4x4)
|
||||
@Component
|
||||
struct FbCard4x4 {
|
||||
@LocalStorageProp('state') state: string = 'loading';
|
||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
||||
@LocalStorageProp('weekday') weekday: string = '';
|
||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
||||
@LocalStorageProp('marks') marks: string = '';
|
||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
||||
@LocalStorageProp('doing') doing: number = 0;
|
||||
@LocalStorageProp('todo') todo: number = 0;
|
||||
@LocalStorageProp('overdue') overdue: number = 0;
|
||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
||||
@LocalStorageProp('daysJson') daysJson: string = '[]';
|
||||
@LocalStorageProp('hint') hint: string = '';
|
||||
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||
|
||||
private subLine(): string {
|
||||
const parts: string[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
if (this.lunarShort !== '') {
|
||||
parts.push(this.lunarShort);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** 今日日程最多画 5 条(第 6 条开始一定被裁) */
|
||||
private events(): FbCardEvent[] {
|
||||
const all = JSON.parse(this.eventsJson) as FbCardEvent[];
|
||||
const out: FbCardEvent[] = [];
|
||||
for (let i = 0; i < all.length && i < 5; i++) {
|
||||
out.push(all[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 最近纪念日最多 2 条(第三行会与"今日日程"抢高度) */
|
||||
private days(): FbCardDay[] {
|
||||
const all = JSON.parse(this.daysJson) as FbCardDay[];
|
||||
const out: FbCardDay[] = [];
|
||||
for (let i = 0; i < all.length && i < 2; i++) {
|
||||
out.push(all[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 手动刷新(走 message,见文件头) */
|
||||
private refresh(): void {
|
||||
postCardAction(this, {
|
||||
action: 'message',
|
||||
params: { msg: 'refresh' }
|
||||
});
|
||||
}
|
||||
|
||||
/** 一个数字格(日程 / 在做 / 待办 / 逾期) */
|
||||
@Builder
|
||||
statCell(label: string, value: number, color: string) {
|
||||
Column({ space: 1 }) {
|
||||
Text(`${value}`)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(color)
|
||||
.maxLines(1)
|
||||
Text(label)
|
||||
.fontSize(9)
|
||||
.fontColor('#A8988A')
|
||||
.maxLines(1)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 抬头 ----
|
||||
Row({ space: 6 }) {
|
||||
Column({ space: 1 }) {
|
||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
Text(this.subLine() + (this.marks !== '' ? ' ' + this.marks : ''))
|
||||
.fontSize(10)
|
||||
.fontColor('#A8988A')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text('↻')
|
||||
.fontSize(14)
|
||||
.fontColor('#7E6B5A')
|
||||
.width(26)
|
||||
.height(26)
|
||||
.textAlign(TextAlign.Center)
|
||||
.backgroundColor('#F5EFE4')
|
||||
.borderRadius(13)
|
||||
.onClick((): void => {
|
||||
this.refresh();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// ---- 四个数字 ----
|
||||
Row() {
|
||||
this.statCell('日程', this.eventCount, '#A84A28')
|
||||
this.statCell('在做', this.doing, '#2C231D')
|
||||
this.statCell('待办', this.todo, '#2C231D')
|
||||
this.statCell('逾期', this.overdue, this.overdue > 0 ? '#C0533F' : '#A8988A')
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 8 })
|
||||
|
||||
Divider()
|
||||
.strokeWidth(0.5)
|
||||
.color('#EFE6D8')
|
||||
.margin({ top: 8, bottom: 6 })
|
||||
|
||||
// ---- 今日日程 ----
|
||||
Text('今日日程')
|
||||
.fontSize(10)
|
||||
.fontColor('#A8988A')
|
||||
.width('100%')
|
||||
|
||||
Column({ space: 0 }) {
|
||||
if (this.hint !== '') {
|
||||
Text(this.hint)
|
||||
.fontSize(11)
|
||||
.fontColor('#C0533F')
|
||||
.maxLines(2)
|
||||
.width('100%')
|
||||
} else if (this.events().length === 0) {
|
||||
Text('今天没有安排')
|
||||
.fontSize(11)
|
||||
.fontColor('#A8988A')
|
||||
.width('100%')
|
||||
} else {
|
||||
ForEach(this.events(), (e: FbCardEvent) => {
|
||||
Row({ space: 6 }) {
|
||||
Column()
|
||||
.width(3)
|
||||
.height(13)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(e.color)
|
||||
.flexShrink(0)
|
||||
Text(e.time)
|
||||
.fontSize(10)
|
||||
.fontColor('#7E6B5A')
|
||||
.width(34)
|
||||
.flexShrink(0)
|
||||
Text(e.title)
|
||||
.fontSize(11)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height(21)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (e: FbCardEvent) => e.key)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 3 })
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 最近纪念日 ----
|
||||
if (this.days().length > 0) {
|
||||
Divider()
|
||||
.strokeWidth(0.5)
|
||||
.color('#EFE6D8')
|
||||
.margin({ top: 4, bottom: 5 })
|
||||
|
||||
ForEach(this.days(), (d: FbCardDay) => {
|
||||
Row({ space: 6 }) {
|
||||
Text(d.days === 0 ? '今天' : `${d.days} 天`)
|
||||
.fontSize(10)
|
||||
.fontColor(d.soon ? '#A84A28' : '#A8988A')
|
||||
.width(38)
|
||||
.flexShrink(0)
|
||||
Text(d.title)
|
||||
.fontSize(11)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height(18)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (d: FbCardDay) => d.key)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ left: 14, right: 14, top: 12, bottom: 10 })
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.onClick((): void => {
|
||||
// 整卡点击 → 打开日程(卡片上"今日日程"是主角)
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: { fbModule: 'calendar' }
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
* 因为「还有几天」是这个模块唯一真正要一眼看到的信息。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, FbTint, FB_MASK } from '../../common/Theme';
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbAnniversaryItem, FbKinChip } from '../../model/Types';
|
||||
import { parseDays } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FbSheetRing } from '../../components/FbSheetRing';
|
||||
import { FbSheetClose } from '../../components/FbSheetClose';
|
||||
import { FbDayEditSheet } from '../../components/FbDayEditSheet';
|
||||
import { FbRosterPerson, FbPersonRepo } from '../../data/FbPersonRepo';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
@@ -45,8 +49,29 @@ export struct FbDaysPage {
|
||||
@Local settingsOpen: boolean = false;
|
||||
|
||||
@Local all: FbAnniversaryItem[] = [];
|
||||
/** 类别筛选('' = 全部) */
|
||||
/** 列表筛选('' = 全部 / 'birthday' = 生日 / 'custom' = 自己加的) */
|
||||
@Local kindFilter: string = '';
|
||||
/**
|
||||
* 编辑 / 新建浮层是否打开。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20):「纪念日、新增纪念日、设置相关的功能。」
|
||||
* ⇒ 原来「添加」这颗按钮**没有 `onTap`**、列表行**没有 `onClick`**。
|
||||
*/
|
||||
@Local editOpen: boolean = false;
|
||||
@Local editingId: number = 0;
|
||||
@Local editingTitle: string = '';
|
||||
@Local editingKind: string = '';
|
||||
@Local editingNote: string = '';
|
||||
@Local editingRepeat: string = 'solar';
|
||||
@Local editingMonth: number = 1;
|
||||
@Local editingDay: number = 1;
|
||||
@Local editingPersonIds: number[] = [];
|
||||
/** 名册(编辑浮层里选关联人用) */
|
||||
@Local roster: FbRosterPerson[] = [];
|
||||
/** 「按分类看」浮层(设置菜单里那一条) */
|
||||
@Local kindSheetOpen: boolean = false;
|
||||
/** 一行提示(点了派生生日、或操作失败时) */
|
||||
@Local hint: string = '';
|
||||
|
||||
/**
|
||||
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
||||
@@ -64,6 +89,14 @@ export struct FbDaysPage {
|
||||
}
|
||||
|
||||
private backOnce(): boolean {
|
||||
if (this.editOpen) {
|
||||
this.editOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.kindSheetOpen) {
|
||||
this.kindSheetOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.settingsOpen) {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
@@ -74,6 +107,16 @@ export struct FbDaysPage {
|
||||
aboutToAppear(): void {
|
||||
FB_BACK.register('days', (): boolean => this.backOnce());
|
||||
this.load();
|
||||
this.loadRoster();
|
||||
}
|
||||
|
||||
/** 读名册(选关联人用)。失败不阻断页面:只是"关联人"点开没人可选 */
|
||||
private async loadRoster(): Promise<void> {
|
||||
try {
|
||||
this.roster = await FbPersonRepo.inst().list();
|
||||
} catch (err) {
|
||||
this.roster = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
@@ -101,6 +144,73 @@ export struct FbDaysPage {
|
||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 编辑浮层开着 ⇒ 底部悬浮快捷栏让位(与设置浮层不会同时开) */
|
||||
@Monitor('editOpen')
|
||||
onEditChanged(): void {
|
||||
fbAppState().sheetCount = this.editOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 「按分类看」浮层同理 */
|
||||
@Monitor('kindSheetOpen')
|
||||
onKindSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.kindSheetOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 打开「新建」 */
|
||||
private openNew(): void {
|
||||
const today: Date = new Date();
|
||||
this.editingId = 0;
|
||||
this.editingTitle = '';
|
||||
this.editingKind = '';
|
||||
this.editingNote = '';
|
||||
this.editingRepeat = 'solar';
|
||||
this.editingMonth = today.getMonth() + 1;
|
||||
this.editingDay = today.getDate();
|
||||
this.editingPersonIds = [];
|
||||
this.hint = '';
|
||||
this.editOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开某条(点列表行)。
|
||||
*
|
||||
* ⚠️ **只有自己添加的能改**:名册生日派生出来的那些行 `id` 是 0,
|
||||
* 拿它去打 `/days/0` 必然 404;而且改生日该去名册里改(那里是唯一真值)。
|
||||
* ⇒ 派生行点开只给一句提示,不去打开一个注定失败的编辑框。
|
||||
*/
|
||||
private openEdit(a: FbAnniversaryItem): void {
|
||||
if (a.source !== 'custom' || a.id <= 0) {
|
||||
this.hint = '「' + a.title + '」是从名册里的生日自动来的 —— 要改请去名册改那个人的生日。';
|
||||
return;
|
||||
}
|
||||
this.hint = '';
|
||||
this.editingId = a.id;
|
||||
this.editingTitle = a.title;
|
||||
this.editingKind = a.rawKind;
|
||||
this.editingNote = a.note;
|
||||
this.editingRepeat = a.repeatType;
|
||||
this.editingMonth = a.month;
|
||||
this.editingDay = a.day;
|
||||
this.editingPersonIds = a.personIds.slice();
|
||||
this.editOpen = true;
|
||||
}
|
||||
|
||||
/** 数据里出现过的分类名(去重),给「按分类看」用 */
|
||||
private kindNames(): string[] {
|
||||
const out: string[] = [];
|
||||
for (const a of this.all) {
|
||||
if (a.rawKind !== '' && out.indexOf(a.rawKind) < 0) {
|
||||
out.push(a.rawKind);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 某个分类名下的条数 */
|
||||
private countOfKind(name: string): number {
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.rawKind === name).length;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -124,7 +234,11 @@ export struct FbDaysPage {
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** —— 纯摆设。 */
|
||||
onTap: (): void => {
|
||||
this.openNew();
|
||||
}
|
||||
})
|
||||
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
|
||||
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
|
||||
@@ -140,6 +254,16 @@ export struct FbDaysPage {
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 一行提示(点了派生生日 / 操作失败)—— 就地说明,不弹 toast
|
||||
if (this.hint !== '') {
|
||||
Text(this.hint)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 })
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ---- 即将到来(30 天内)----
|
||||
@@ -175,8 +299,6 @@ export struct FbDaysPage {
|
||||
.onDidScroll(fbOnHScroll)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
@@ -186,11 +308,33 @@ export struct FbDaysPage {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 全部纪念日 ----
|
||||
/* ⚠️ 筛选维度用「来源」(自己加的 / 名册生日派生)而不是"节日/纪念日":
|
||||
服务端的 `kind` 是**自由文本**(用户在设置里能自建分类名),
|
||||
原来那三个胶囊拿 'anniversary' / 'holiday' 去比,
|
||||
而自定义条目存的是「结婚纪念日」这种原文 ⇒ 点了就是空列表。 */
|
||||
Row({ space: 8 }) {
|
||||
this.kindChip('全部', '')
|
||||
this.kindChip('生日', 'birthday')
|
||||
this.kindChip('纪念日', 'anniversary')
|
||||
this.kindChip('节日', 'holiday')
|
||||
this.kindChip('自己加的', 'custom')
|
||||
/* 从「设置 → 按分类看」筛过来时,这里给一个能取消的胶囊 */
|
||||
if (this.kindFilter.startsWith('kind:')) {
|
||||
Row({ space: 4 }) {
|
||||
Text('分类:' + this.kindFilter.substring(5))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
Image($r('app.media.fb_ic_close'))
|
||||
.width(10)
|
||||
.height(10)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(this.accent))
|
||||
}
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.backgroundColor(fbAlpha(this.accent, 0.1))
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.kindFilter = '';
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 14, bottom: 8 })
|
||||
@@ -216,6 +360,11 @@ export struct FbDaysPage {
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
/* 🔴 v65 修正:这行原来被贴在了**「即将到来」那条横向 Scroll** 上
|
||||
(内层横滑组件吃了一个 100+vp 的底部内边距 ⇒ 那块卡片底下多一截空白,
|
||||
而页面真正需要的留白反而没有)。挪回到页面根 `Column` 上。 */
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -248,12 +397,143 @@ export struct FbDaysPage {
|
||||
this.settingsOpen = false;
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
// 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应)
|
||||
// ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。
|
||||
/* v65:这条原来 `action === ''`(画灰、点了不响应)。
|
||||
「分类」在服务端是**自由文本**,「分类列表」本身没有 API,
|
||||
所以这里能做的、也确实是用户想要的,是"按已经出现过的分类挑一个看"。 */
|
||||
if (action === 'daykinds') {
|
||||
this.kindSheetOpen = true;
|
||||
this.settingsOpen = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 新建 / 编辑纪念日浮层(v65)----
|
||||
if (this.editOpen) {
|
||||
FbDayEditSheet({
|
||||
accent: this.accent,
|
||||
editingId: this.editingId,
|
||||
editingTitle: this.editingTitle,
|
||||
editingKind: this.editingKind,
|
||||
editingNote: this.editingNote,
|
||||
editingRepeat: this.editingRepeat,
|
||||
editingMonth: this.editingMonth,
|
||||
editingDay: this.editingDay,
|
||||
editingPersonIds: this.editingPersonIds,
|
||||
roster: this.roster,
|
||||
onClose: (): void => {
|
||||
this.editOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.editOpen = false;
|
||||
this.load();
|
||||
},
|
||||
onDeleted: (): void => {
|
||||
this.editOpen = false;
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 「按分类看」浮层(v65)----
|
||||
if (this.kindSheetOpen) {
|
||||
/* 遮罩与卡片必须**叠**(`Stack`),不能"排" —— 见 FbDayEditSheet 同一说明 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FB_MASK)
|
||||
.onClick((): void => {
|
||||
this.kindSheetOpen = false;
|
||||
})
|
||||
|
||||
Column() {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
|
||||
Column({ space: 12 }) {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('按分类看')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('挑一个分类,只看它下面的纪念日')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.kindSheetOpen = false;
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 2 }) {
|
||||
if (this.kindNames().length === 0) {
|
||||
Text('还没有带分类的纪念日')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.padding({ top: 12, bottom: 12 })
|
||||
} else {
|
||||
ForEach(this.kindNames(), (k: string) => {
|
||||
Row({ space: 8 }) {
|
||||
Text(k)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.countOfKind(k).toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 11, bottom: 11 })
|
||||
.onClick((): void => {
|
||||
/* ⚠️ 真的去筛(不是只弹一句"有 N 条"):
|
||||
值前缀 `kind:` 表示"按分类原文筛",见 `filtered()`。 */
|
||||
this.kindFilter = 'kind:' + k;
|
||||
this.hint = '';
|
||||
this.kindSheetOpen = false;
|
||||
})
|
||||
}, (k: string): string => 'ks_' + k)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.constraintSize({ maxHeight: 320 })
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.TopStart)
|
||||
|
||||
Text('分类名在网页端「纪念日 → 设置 → 纪念日分类」里配;App 这边只按已有分类筛。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -324,7 +604,12 @@ export struct FbDaysPage {
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
FbTag({ text: this.kindName(a.kind), tone: this.kindTone(a.kind) })
|
||||
/* ⚠️ 显示**服务端原文**(用户自建的分类名,如「结婚纪念日」);
|
||||
老数据/派生行没有 kind 时才回落到映射出来的类别名。 */
|
||||
FbTag({
|
||||
text: a.rawKind !== '' ? a.rawKind : this.kindName(a.kind),
|
||||
tone: this.kindTone(a.kind)
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
@@ -362,6 +647,11 @@ export struct FbDaysPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
/* 🔴 v65:整行可点 → 打开编辑(派生生日给一句提示,见 `openEdit`)。
|
||||
原来这里**没有 `onClick`** ⇒ 用户说"这些都需要增加"。 */
|
||||
.onClick((): void => {
|
||||
this.openEdit(a);
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
@@ -429,6 +719,18 @@ export struct FbDaysPage {
|
||||
if (this.kindFilter.length === 0) {
|
||||
return this.all;
|
||||
}
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.kind === this.kindFilter);
|
||||
if (this.kindFilter === 'custom') {
|
||||
// 「自己加的」= 服务端 `source === 'custom'`(派生生日是 birthday_solar/lunar)
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.source === 'custom');
|
||||
}
|
||||
if (this.kindFilter === 'birthday') {
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.kind === 'birthday');
|
||||
}
|
||||
// `kind:<分类原文>` —— 按用户自建的分类名筛
|
||||
if (this.kindFilter.startsWith('kind:')) {
|
||||
const name: string = this.kindFilter.substring(5);
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.rawKind === name);
|
||||
}
|
||||
return this.all;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
||||
import { FbLedgerEntrySheet } from '../../components/FbLedgerEntrySheet';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { FB_SET_LEDGER } from '../../model/FbSettingsMenus';
|
||||
|
||||
@@ -60,6 +61,22 @@ export struct FbLedgerPage {
|
||||
@Local syncNote: string = '';
|
||||
/** 当前账本 id(0 = 用第一本) */
|
||||
@Local bookId: number = 0;
|
||||
/**
|
||||
* 当前这本账的**服务端汇总**(分)。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20「还有这个财务的显示也有问题」):
|
||||
* 原来「资产 / 负债」是端上**自己按金额正负号重新加**的,
|
||||
* 而服务端的口径是「负债取绝对值」(`Ledger::summary`)——
|
||||
* 于是一条填成 `-500` 的欠款会同时满足"是负债"与"是负数",
|
||||
* 端上加出来是 `-500`、服务端算出来是 `500` ⇒
|
||||
* **净资产 ≠ 资产 − 负债**,三个数字互相矛盾。
|
||||
* ⇒ 三个数字一律用服务端给的(`summary` 里有 asset / debt / net)。
|
||||
*/
|
||||
@Local netNow: number = 0;
|
||||
@Local assetNow: number = 0;
|
||||
@Local debtNow: number = 0;
|
||||
/** 「记一笔」浮层是否打开 */
|
||||
@Local entryOpen: boolean = false;
|
||||
|
||||
/**
|
||||
* 页面出现:**两级加载**。
|
||||
@@ -78,6 +95,10 @@ export struct FbLedgerPage {
|
||||
}
|
||||
|
||||
private backOnce(): boolean {
|
||||
if (this.entryOpen) {
|
||||
this.entryOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.settingsOpen) {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
@@ -117,14 +138,11 @@ export struct FbLedgerPage {
|
||||
parseLedgerEntries,
|
||||
(d: FbLedgerDetail): void => {
|
||||
this.entries = d.entries;
|
||||
// ⚠️ 净资产写回**这本账**(页面的 `netCent()` 是从 `currentBook()` 上取的)。
|
||||
// 概览接口不返回 net,所以这里补上 —— 不新增页面字段,
|
||||
// 避免和已有的 `netCent()` 方法重名(实测撞过一次)。
|
||||
for (const bk of this.books) {
|
||||
if (bk.id === id) {
|
||||
bk.netCent = d.netCent;
|
||||
}
|
||||
}
|
||||
/* ⚠️ 净资产/资产/负债**都用服务端那份**(见 `netNow` 的说明)。
|
||||
原来写回 `books[i].netCent` 还有一个隐患:改数组元素不一定会触发刷新。 */
|
||||
this.netNow = d.netCent;
|
||||
this.assetNow = d.assetCent;
|
||||
this.debtNow = d.debtCent;
|
||||
},
|
||||
(note: string): void => {
|
||||
this.syncNote = note;
|
||||
@@ -143,6 +161,12 @@ export struct FbLedgerPage {
|
||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 「记一笔」浮层开着 ⇒ 底部悬浮快捷栏让位(与设置浮层不会同时开) */
|
||||
@Monitor('entryOpen')
|
||||
onEntrySheetChanged(): void {
|
||||
fbAppState().sheetCount = this.entryOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -170,7 +194,15 @@ export struct FbLedgerPage {
|
||||
FbPrimaryButton({
|
||||
label: '记一笔',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** —— 纯摆设。 */
|
||||
onTap: (): void => {
|
||||
if (this.currentId() <= 0) {
|
||||
this.syncNote = '还没有账本 —— 先在网页端建一本,或者在设置里新建。';
|
||||
return;
|
||||
}
|
||||
this.entryOpen = true;
|
||||
}
|
||||
})
|
||||
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
|
||||
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
|
||||
@@ -186,6 +218,16 @@ export struct FbLedgerPage {
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 一行提示(同步失败 / 没账本时点「记一笔」)
|
||||
if (this.syncNote !== '') {
|
||||
Text(this.syncNote)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 })
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
// ---- 账本切换(横向滚动条)----
|
||||
// ⚠️ 这层 `Scroll` **必须保留**:它是**横向**滚动(账本多时排不下)。
|
||||
// 它与"页面纵向滚动"是两码事,不能被拍平(拍平会整条挤在一起)。
|
||||
@@ -210,7 +252,11 @@ export struct FbLedgerPage {
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
/* 🔴 v65:切账本必须**重拉明细**。
|
||||
原来只改了 `bookId`,`entries` 还是上一本的
|
||||
⇒ 标题换了、账目和净资产仍旧是别的账本的(用户说的"显示有问题")。 */
|
||||
this.bookId = b.id;
|
||||
this.loadEntries(b.id);
|
||||
})
|
||||
}, (b: FbLedgerBook): string => 'bk_' + b.id.toString())
|
||||
}
|
||||
@@ -337,8 +383,6 @@ export struct FbLedgerPage {
|
||||
}, (e: FbLedgerEntry): string => 'le_' + e.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
@@ -353,6 +397,11 @@ export struct FbLedgerPage {
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
/* 🔴 v65 修正:这行原来被贴在了**每个类别分组的内层 Column** 上
|
||||
(内层吃了一个 100+vp 的底部内边距 ⇒ 每组账目下面多一截空白,
|
||||
而页面真正需要的留白反而没有)。挪回页面根 `Column`。 */
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -385,8 +434,27 @@ export struct FbLedgerPage {
|
||||
this.settingsOpen = false;
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
// 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应)
|
||||
// ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。
|
||||
/* 「共享给谁」「账本名字」**都没有 API**(服务端只开了
|
||||
建账本 / 记一笔 / 删账本),所以这里保持"暂未开放"的灰态,
|
||||
不接一个点了报错的入口。 */
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 记一笔浮层(v65)----
|
||||
if (this.entryOpen) {
|
||||
FbLedgerEntrySheet({
|
||||
accent: this.accent,
|
||||
ledgerId: this.currentId(),
|
||||
ledgerName: this.currentBook().name,
|
||||
onClose: (): void => {
|
||||
this.entryOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.entryOpen = false;
|
||||
// 重拉明细:金额与汇总都在里面,端上拼不出来
|
||||
this.loadEntries(this.currentId());
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
@@ -421,18 +489,13 @@ export struct FbLedgerPage {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text(fbMoney(e.amountCent))
|
||||
/* ⚠️ 金额按**服务器的负债语义**显示(`Ledger::isDebt` 同一份判断):
|
||||
负债类(贷款 / 信用卡 / 欠款)一律显示成负号 —— 用户可能填的是正数
|
||||
(「我欠 5000」),直接显示 `5000` 会让人以为是资产。 */
|
||||
Text(fbMoney(this.shownCent(e)))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(e.amountCent < 0 ? FbColors.red : FbColors.ink)
|
||||
|
||||
// 「盘点」:Web 端是每行一个入口,端上保留成一个轻按钮
|
||||
Text('盘点')
|
||||
.fontSize(10)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.fontColor(this.shownCent(e) < 0 ? FbColors.red : FbColors.ink)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 11, bottom: 11 })
|
||||
@@ -465,37 +528,43 @@ export struct FbLedgerPage {
|
||||
}
|
||||
|
||||
private netCent(): number {
|
||||
return this.currentBook().netCent;
|
||||
return this.netNow;
|
||||
}
|
||||
|
||||
/** 总资产(分)—— **服务端口径**(`Ledger::summary`),不是端上按正负号重加 */
|
||||
private assetCent(): number {
|
||||
let sum: number = 0;
|
||||
for (const e of this.entries) {
|
||||
if (e.amountCent > 0) {
|
||||
sum = sum + e.amountCent;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
return this.assetNow;
|
||||
}
|
||||
|
||||
/** 总负债(分,正数)—— 服务端按绝对值计入,见 `netNow` 的说明 */
|
||||
private debtCent(): number {
|
||||
let sum: number = 0;
|
||||
for (const e of this.entries) {
|
||||
if (e.amountCent < 0) {
|
||||
sum = sum + e.amountCent;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
return this.debtNow;
|
||||
}
|
||||
|
||||
private sumOf(items: FbLedgerEntry[]): number {
|
||||
let sum: number = 0;
|
||||
for (const e of items) {
|
||||
sum = sum + e.amountCent;
|
||||
sum = sum + this.shownCent(e);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** 这个类别是不是负债(与 `Ledger::KINDS` 的第 2 列一致) */
|
||||
private isDebtKind(kind: string): boolean {
|
||||
return kind === 'loan' || kind === 'credit' || kind === 'owe';
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条账目**该显示多少分**。
|
||||
*
|
||||
* ⚠️ 负债类一律取负:用户可能把欠款填成正数,直接显示会看成资产。
|
||||
* 服务端 `summary` 也是这个口径(负债按绝对值计入总负债)。
|
||||
*/
|
||||
private shownCent(e: FbLedgerEntry): number {
|
||||
const v: number = e.amountCent;
|
||||
return this.isDebtKind(e.kind) ? -Math.abs(v) : v;
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbLedgerEntry>[] {
|
||||
const keys: string[] = [];
|
||||
const buckets: FbLedgerEntry[][] = [];
|
||||
|
||||
@@ -19,6 +19,9 @@ import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FbAvatar, FbDot, FbEmptyBox, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
import { FbNoteSheet } from '../../components/FbNoteSheet';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { fbSheetTransition } from '../../common/FbAnim';
|
||||
|
||||
// ============================================================
|
||||
// 笔记
|
||||
@@ -40,15 +43,65 @@ export struct FbNotesPage {
|
||||
@Local all: FbNoteItem[] = [];
|
||||
/** 顶部提示 */
|
||||
@Local syncNote: string = '';
|
||||
/**
|
||||
* 笔记浮层是否打开(新建 / 看 / 改 / 删)。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20):「笔记现在也点不进去,记笔记也记不了。」
|
||||
* ⇒ 原来「记一条」按钮**没有 `onTap`**、卡片**没有 `onClick`**。
|
||||
*/
|
||||
@Local sheetOpen: boolean = false;
|
||||
@Local sheetId: number = 0;
|
||||
@Local sheetTitle: string = '';
|
||||
@Local sheetBody: string = '';
|
||||
@Local sheetTags: string = '';
|
||||
@Local sheetMine: boolean = true;
|
||||
|
||||
/**
|
||||
* 页面出现:读缓存 → 拉接口(本地优先)。
|
||||
* ⚠️ 不自己开库:`EntryAbility.onCreate` 已统一打开。
|
||||
*/
|
||||
aboutToAppear(): void {
|
||||
FB_BACK.register('notes', (): boolean => this.backOnce());
|
||||
this.load();
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
FB_BACK.unregister('notes');
|
||||
}
|
||||
|
||||
/** 返回手势:先关浮层(true = 已消费) */
|
||||
private backOnce(): boolean {
|
||||
if (this.sheetOpen) {
|
||||
this.sheetOpen = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 浮层开着 ⇒ 底部悬浮快捷栏让位 */
|
||||
@Monitor('sheetOpen')
|
||||
onSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.sheetOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
private openNew(): void {
|
||||
this.sheetId = 0;
|
||||
this.sheetTitle = '';
|
||||
this.sheetBody = '';
|
||||
this.sheetTags = '';
|
||||
this.sheetMine = true;
|
||||
this.sheetOpen = true;
|
||||
}
|
||||
|
||||
private openNote(n: FbNoteItem): void {
|
||||
this.sheetId = n.id;
|
||||
this.sheetTitle = n.title;
|
||||
this.sheetBody = n.body;
|
||||
this.sheetTags = n.tags.join(' ');
|
||||
this.sheetMine = n.mine;
|
||||
this.sheetOpen = true;
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
await loadModule<FbNoteItem[]>('notes', parseNotes,
|
||||
(rows: FbNoteItem[]): void => {
|
||||
@@ -60,29 +113,36 @@ export struct FbNotesPage {
|
||||
}
|
||||
|
||||
build() {
|
||||
Scroll() {
|
||||
Column() {
|
||||
this.topBar()
|
||||
/* 🔴 v65:外层补一层 `Stack` —— 笔记浮层要浮在页面之上
|
||||
(本页原来是 `Scroll` 直接作根,没有可以放浮层的容器)。 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Scroll() {
|
||||
Column() {
|
||||
this.topBar()
|
||||
|
||||
// 🔴 2026-10-19:模块名大标题已删(顶栏左上角就是「笔记」)
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.all.length.toString() + ' 条 · 分块 + 标签')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
// 🔴 2026-10-19:模块名大标题已删(顶栏左上角就是「笔记」)
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.all.length.toString() + ' 条 · 分块 + 标签')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '记一条',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** —— 纯摆设。 */
|
||||
onTap: (): void => {
|
||||
this.openNew();
|
||||
}
|
||||
})
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '记一条',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.all.length === 0) {
|
||||
FbEmptyBox({
|
||||
@@ -114,18 +174,45 @@ export struct FbNotesPage {
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
||||
.onDidScroll(fbOnScroll)
|
||||
|
||||
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
||||
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
||||
.onTouch(fbOnTouch)
|
||||
|
||||
// ---- 笔记浮层:新建 / 看 / 改 / 删(v65)----
|
||||
if (this.sheetOpen) {
|
||||
FbNoteSheet({
|
||||
accent: this.accent,
|
||||
editingId: this.sheetId,
|
||||
editingTitle: this.sheetTitle,
|
||||
editingBody: this.sheetBody,
|
||||
editingTags: this.sheetTags,
|
||||
mine: this.sheetMine,
|
||||
onClose: (): void => {
|
||||
this.sheetOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.sheetOpen = false;
|
||||
this.load();
|
||||
},
|
||||
onDeleted: (): void => {
|
||||
this.sheetOpen = false;
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
||||
.onDidScroll(fbOnScroll)
|
||||
|
||||
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
||||
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
||||
.onTouch(fbOnTouch)
|
||||
}
|
||||
|
||||
@Builder
|
||||
@@ -180,6 +267,10 @@ export struct FbNotesPage {
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
/* 🔴 v65:整卡可点 → 打开看/改(原来**没有 `onClick`** ⇒ "点不进去") */
|
||||
.onClick((): void => {
|
||||
this.openNote(n);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
||||
import { FbResourceSheet } from '../../components/FbResourceSheet';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { FB_SET_RESOURCES } from '../../model/FbSettingsMenus';
|
||||
|
||||
@@ -44,8 +45,24 @@ export struct FbResourcesPage {
|
||||
@Local settingsOpen: boolean = false;
|
||||
|
||||
@Local all: FbResourceItem[] = [];
|
||||
/** 当前筛选类别('' = 全部) */
|
||||
@Local kindFilter: string = '';
|
||||
/**
|
||||
* 当前筛选范围:'' = 全部 / 'shared' = 共享给我的 / 'mine' = 我名下的。
|
||||
*
|
||||
* 🔴 v65:原来这里是「证件 / 资料」两个类别 —— 服务端 `resources` 表
|
||||
* **没有类别列**(`app/Schema.php` 只有 title / content / owner_id),
|
||||
* 解析时 kind 只能全填 `other` ⇒ 这两个胶囊点了永远是空列表。
|
||||
* 改成按**权限/归属**筛:这两个维度服务端真的给(`perm` / `owner_id`)。
|
||||
*/
|
||||
@Local scopeFilter: string = '';
|
||||
|
||||
/** 资源浮层(新建 / 查看 / 编辑 / 删除)是否打开 */
|
||||
@Local sheetOpen: boolean = false;
|
||||
/** 浮层里那条(0 = 新建) */
|
||||
@Local sheetId: number = 0;
|
||||
@Local sheetTitle: string = '';
|
||||
@Local sheetContent: string = '';
|
||||
@Local sheetOwner: string = '';
|
||||
@Local sheetCanWrite: boolean = true;
|
||||
|
||||
/**
|
||||
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
||||
@@ -63,6 +80,10 @@ export struct FbResourcesPage {
|
||||
}
|
||||
|
||||
private backOnce(): boolean {
|
||||
if (this.sheetOpen) {
|
||||
this.sheetOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.settingsOpen) {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
@@ -100,6 +121,34 @@ export struct FbResourcesPage {
|
||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 资源浮层开着 ⇒ 底部悬浮快捷栏同样让位(两个浮层不会同时开) */
|
||||
@Monitor('sheetOpen')
|
||||
onResourceSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.sheetOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 打开「新增资源」 */
|
||||
private openNew(): void {
|
||||
this.sheetId = 0;
|
||||
this.sheetTitle = '';
|
||||
this.sheetContent = '';
|
||||
this.sheetOwner = '';
|
||||
this.sheetCanWrite = true;
|
||||
this.sheetOpen = true;
|
||||
}
|
||||
|
||||
/** 打开某条资源(只读共享进来的锁输入框,但内容照看) */
|
||||
private openDetail(r: FbResourceItem): void {
|
||||
this.sheetId = r.id;
|
||||
this.sheetTitle = r.name;
|
||||
this.sheetContent = r.content;
|
||||
this.sheetOwner = r.ownerName;
|
||||
/* `perm` 是服务端 `Resource::all` 里算好的:owner / write / read。
|
||||
⚠️ 只有 owner 与 write 能改(服务端 `Resource::canWrite` 同一口径)。 */
|
||||
this.sheetCanWrite = r.perm === 'owner' || r.perm === 'write';
|
||||
this.sheetOpen = true;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -126,7 +175,12 @@ export struct FbResourcesPage {
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** ——
|
||||
用户说的"新增加资源"没完成就是这条。 */
|
||||
onTap: (): void => {
|
||||
this.openNew();
|
||||
}
|
||||
})
|
||||
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
|
||||
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
|
||||
@@ -142,11 +196,11 @@ export struct FbResourcesPage {
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 类别筛选 ----
|
||||
// ---- 筛选(全部 / 共享给我 / 我名下)----
|
||||
Row({ space: 8 }) {
|
||||
this.kindChip('全部', '')
|
||||
this.kindChip('证件', 'document')
|
||||
this.kindChip('资料', 'material')
|
||||
this.scopeChip('全部', '')
|
||||
this.scopeChip('共享给我', 'shared')
|
||||
this.scopeChip('我名下', 'mine')
|
||||
Blank()
|
||||
Text('共享给我 ' + this.sharedCount().toString() + ' 项')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
@@ -216,8 +270,41 @@ export struct FbResourcesPage {
|
||||
this.settingsOpen = false;
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
// 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应)
|
||||
// ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。
|
||||
/* v65:两条都接上了(原来都是空串 ⇒ 画灰、点了不响应) */
|
||||
if (action === 'resourceShared') {
|
||||
this.scopeFilter = 'shared';
|
||||
this.settingsOpen = false;
|
||||
return;
|
||||
}
|
||||
if (action === 'resourceMine') {
|
||||
this.scopeFilter = 'mine';
|
||||
this.settingsOpen = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 资源浮层:新建 / 查看 / 编辑 / 删除(v65)----
|
||||
if (this.sheetOpen) {
|
||||
FbResourceSheet({
|
||||
accent: this.accent,
|
||||
editingId: this.sheetId,
|
||||
editingTitle: this.sheetTitle,
|
||||
editingContent: this.sheetContent,
|
||||
ownerName: this.sheetOwner,
|
||||
canWrite: this.sheetCanWrite,
|
||||
onClose: (): void => {
|
||||
this.sheetOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.sheetOpen = false;
|
||||
this.load();
|
||||
},
|
||||
onDeleted: (): void => {
|
||||
this.sheetOpen = false;
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
@@ -227,13 +314,13 @@ export struct FbResourcesPage {
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 一项资源卡 */
|
||||
/** 一项资源卡(整卡可点 → 打开详情/编辑) */
|
||||
@Builder
|
||||
resourceCard(r: FbResourceItem) {
|
||||
Row({ space: 12 }) {
|
||||
// 类别图标底
|
||||
// 图标底:共享来的用「共享」图标,自己名下的用「锁」
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Image(this.kindIcon(r.kind))
|
||||
Image(r.shared ? $r('app.media.fb_ic_share') : $r('app.media.fb_ic_lock'))
|
||||
.width(19)
|
||||
.height(19)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
@@ -254,26 +341,26 @@ export struct FbResourcesPage {
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (r.shared) {
|
||||
FbTag({ text: '已共享', tone: 'green' })
|
||||
FbTag({ text: r.perm === 'write' ? '可改' : '只读', tone: 'green' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 正文预览:资源的正文就是"编号、有效期、放在哪" —— 比附件数有用得多
|
||||
if (r.content !== '') {
|
||||
Text(r.content)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
Row({ space: 10 }) {
|
||||
Text('归属 ' + r.ownerName)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
Row({ space: 3 }) {
|
||||
Image($r('app.media.fb_ic_resources'))
|
||||
.width(11)
|
||||
.height(11)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
Text(r.fileCount.toString() + ' 个附件')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
Text(r.updatedAt)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
@@ -295,18 +382,23 @@ export struct FbResourcesPage {
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
/* 🔴 v65:整卡可点 → 打开详情(原来列表项**没有 `onClick`** ⇒ "点不进去") */
|
||||
.onClick((): void => {
|
||||
this.openDetail(r);
|
||||
})
|
||||
}
|
||||
|
||||
/** 筛选胶囊(全部 / 共享给我 / 我名下) */
|
||||
@Builder
|
||||
kindChip(label: string, key: string) {
|
||||
scopeChip(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.kindFilter === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.kindFilter === key ? this.accent : FbColors.inputBg)
|
||||
.fontColor(this.scopeFilter === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.scopeFilter === key ? this.accent : FbColors.inputBg)
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.kindFilter = key;
|
||||
this.scopeFilter = key;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,21 +419,15 @@ export struct FbResourcesPage {
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
private kindIcon(kind: string): Resource {
|
||||
if (kind === 'document') {
|
||||
return $r('app.media.fb_ic_lock');
|
||||
}
|
||||
if (kind === 'material') {
|
||||
return $r('app.media.fb_ic_book');
|
||||
}
|
||||
return $r('app.media.fb_ic_resources');
|
||||
}
|
||||
|
||||
private filtered(): FbResourceItem[] {
|
||||
if (this.kindFilter.length === 0) {
|
||||
if (this.scopeFilter.length === 0) {
|
||||
return this.all;
|
||||
}
|
||||
return this.all.filter((r: FbResourceItem): boolean => r.kind === this.kindFilter);
|
||||
if (this.scopeFilter === 'shared') {
|
||||
// 共享给我的 = 权限不是 owner 的那些
|
||||
return this.all.filter((r: FbResourceItem): boolean => r.shared);
|
||||
}
|
||||
return this.all.filter((r: FbResourceItem): boolean => !r.shared);
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbResourceItem>[] {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
* 看板名仍保留(卡片上那个小色块标签),点它可以切到别的看板。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK } from '../../common/Theme';
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbTaskItem } from '../../model/Types';
|
||||
import { FbBoardOpt, FbGroup, FbTaskItem } from '../../model/Types';
|
||||
import { fbGroupTask } from '../../model/DemoData';
|
||||
import { parseTasks } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
@@ -20,8 +20,14 @@ import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
||||
import { FbSheetRing } from '../../components/FbSheetRing';
|
||||
import { FbSheetClose } from '../../components/FbSheetClose';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { FB_SET_TASKS } from '../../model/FbSettingsMenus';
|
||||
import { FbTaskEditSheet } from '../../components/FbTaskEditSheet';
|
||||
import { setTaskStatus } from '../../data/FbTaskRepo';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { FbApiResult } from '../../data/FbHttp';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTasksPage {
|
||||
@@ -33,7 +39,6 @@ export struct FbTasksPage {
|
||||
@BuilderParam topBar: () => void;
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private boards: string[] = [];
|
||||
/** 模块主题色(任务 = 赭红) */
|
||||
private accent: string = '#A8574F';
|
||||
/**
|
||||
@@ -48,6 +53,36 @@ export struct FbTasksPage {
|
||||
@Local tasks: FbTaskItem[] = [];
|
||||
/** 当前筛选的看板('' = 全部) */
|
||||
@Local boardFilter: string = '';
|
||||
/**
|
||||
* 状态筛选('' = 全部;'done' = 只看已完成)。
|
||||
*
|
||||
* 🔴 v65:设置菜单里的「已完成」原来 `action === ''` ⇒ 点了不响应。
|
||||
* 网页版是 `?view=done`(`Task::doneList`),端上不换接口,
|
||||
* 直接在已拉到的整份列表上筛(数据本来就是全量,没必要多打一次网络)。
|
||||
*/
|
||||
@Local statusFilter: string = '';
|
||||
/**
|
||||
* 新建 / 编辑任务浮层是否打开。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20):「任务也是一样啊,任务也点不进去,也没法那个完成,
|
||||
* 新建任务、设置这些东西都这都没有。」
|
||||
* ⇒ 原来「新建任务」按钮**没有 `onTap`**、列表项**没有 `onClick`**,
|
||||
* 勾完成的那个圈也**没有热区** —— 三件事一件都没接。现在都接上了。
|
||||
*/
|
||||
@Local editOpen: boolean = false;
|
||||
/** 正在编辑的任务(0 = 新建) */
|
||||
@Local editingId: number = 0;
|
||||
@Local editingTitle: string = '';
|
||||
@Local editingNote: string = '';
|
||||
/** `2026-10-25 18:00`;空 = 没有期限 */
|
||||
@Local editingDueAt: string = '';
|
||||
@Local editingBoardId: number = 0;
|
||||
/** 正在改状态的那条(防连点打重复请求) */
|
||||
@Local busyId: number = 0;
|
||||
/** 「分类与看板」浮层是否打开 */
|
||||
@Local boardSheetOpen: boolean = false;
|
||||
/** 操作失败时的一行红字(如勾完成失败) */
|
||||
@Local opNote: string = '';
|
||||
|
||||
/**
|
||||
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
||||
@@ -65,6 +100,16 @@ export struct FbTasksPage {
|
||||
}
|
||||
|
||||
private backOnce(): boolean {
|
||||
/* v65:返回手势要**先关新的浮层**,再关设置菜单 ——
|
||||
顺序按"后打开的在前",否则按一次返回会跳关两层。 */
|
||||
if (this.editOpen) {
|
||||
this.editOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.boardSheetOpen) {
|
||||
this.boardSheetOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.settingsOpen) {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
@@ -102,6 +147,103 @@ export struct FbTasksPage {
|
||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 新建/编辑浮层开着 ⇒ 底部悬浮快捷栏也要让位(与 `settingsOpen` 不会同时开) */
|
||||
@Monitor('editOpen')
|
||||
onEditChanged(): void {
|
||||
fbAppState().sheetCount = this.editOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 「分类与看板」浮层同理 */
|
||||
@Monitor('boardSheetOpen')
|
||||
onBoardSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.boardSheetOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 打开「新建」浮层(清空回填字段) */
|
||||
private openNew(): void {
|
||||
this.editingId = 0;
|
||||
this.editingTitle = '';
|
||||
this.editingNote = '';
|
||||
this.editingDueAt = '';
|
||||
this.editingBoardId = 0;
|
||||
this.opNote = '';
|
||||
this.editOpen = true;
|
||||
}
|
||||
|
||||
/** 打开「编辑」浮层(把这条任务的字段回填进去) */
|
||||
private openEdit(t: FbTaskItem): void {
|
||||
this.editingId = t.id;
|
||||
this.editingTitle = t.title;
|
||||
this.editingNote = t.note;
|
||||
this.editingDueAt = t.dueAt;
|
||||
this.editingBoardId = t.boardId;
|
||||
this.opNote = '';
|
||||
this.editOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 勾选 / 取消完成。
|
||||
*
|
||||
* 🔴 **必须走 `/tasks/<id>/status`**:服务端 `PATCH /tasks/<id>` 会把
|
||||
* 请求里的 status **原样忽略**(见 `FbTaskRepo` 文件头的说明)
|
||||
* ⇒ 走 PATCH 会"看起来保存成功,但状态没变"。
|
||||
*
|
||||
* ⚠️ 已完成 → 取消完成统一回 `todo`(不回 `doing`:用户只想撤销一次误勾,
|
||||
* 把它标成"进行中"是替用户做了他没表达的决定)。
|
||||
*/
|
||||
private async toggleDone(t: FbTaskItem): Promise<void> {
|
||||
if (this.busyId !== 0) {
|
||||
return;
|
||||
}
|
||||
this.busyId = t.id;
|
||||
this.opNote = '';
|
||||
const next: string = t.status === 'done' ? 'todo' : 'done';
|
||||
const res: FbApiResult = await setTaskStatus(FB_TOKEN_STORE.get(), t.id, next);
|
||||
this.busyId = 0;
|
||||
if (!res.ok) {
|
||||
this.opNote = res.error !== '' ? res.error : '改状态失败,请稍后再试。';
|
||||
return;
|
||||
}
|
||||
// 本地先把这条改掉(列表立刻有反馈),再重拉一遍对齐服务端派生字段(逾期等)
|
||||
this.tasks = this.tasks.map((x: FbTaskItem): FbTaskItem => {
|
||||
if (x.id !== t.id) {
|
||||
return x;
|
||||
}
|
||||
const y: FbTaskItem = {
|
||||
id: x.id, title: x.title, status: next, board: x.board, boardColor: x.boardColor,
|
||||
dueText: x.dueText, overdue: next === 'done' ? false : x.overdue,
|
||||
assignee: x.assignee, childDone: x.childDone, childTotal: x.childTotal,
|
||||
boardId: x.boardId, note: x.note, dueAt: x.dueAt
|
||||
};
|
||||
return y;
|
||||
});
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** 看板列表(从已加载的任务里反推:`/tasks` 的 boards 里没任务的那些画出来也没用) */
|
||||
private boardOpts(): FbBoardOpt[] {
|
||||
const out: FbBoardOpt[] = [];
|
||||
const seen: number[] = [];
|
||||
for (const t of this.tasks) {
|
||||
if (seen.indexOf(t.boardId) >= 0) {
|
||||
continue;
|
||||
}
|
||||
seen.push(t.boardId);
|
||||
out.push({ id: t.boardId, name: t.board, color: t.boardColor });
|
||||
}
|
||||
// 没归看板的「其他」放最后(它不是一个真的看板)
|
||||
return out.sort((a: FbBoardOpt, b: FbBoardOpt): number => {
|
||||
const ka: number = a.id === 0 ? 999999 : a.id;
|
||||
const kb: number = b.id === 0 ? 999999 : b.id;
|
||||
return ka - kb;
|
||||
});
|
||||
}
|
||||
|
||||
/** 某个看板下的任务数(「分类与看板」浮层里显示) */
|
||||
private countOf(boardName: string): number {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.board === boardName).length;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -129,7 +271,11 @@ export struct FbTasksPage {
|
||||
FbPrimaryButton({
|
||||
label: '新建任务',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** —— 纯摆设。 */
|
||||
onTap: (): void => {
|
||||
this.openNew();
|
||||
}
|
||||
})
|
||||
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
|
||||
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
|
||||
@@ -145,13 +291,43 @@ export struct FbTasksPage {
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 操作失败的一行红字(如勾完成失败)—— 不弹 toast,就地说明
|
||||
if (this.opNote !== '') {
|
||||
Text(this.opNote)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 6 })
|
||||
}
|
||||
|
||||
// ---- 看板筛选条(横向滚动)----
|
||||
Scroll() {
|
||||
Row({ space: 8 }) {
|
||||
this.boardChip('全部', '')
|
||||
ForEach(this.boards, (b: string) => {
|
||||
this.boardChip(b, b)
|
||||
}, (b: string): string => 'bd_' + b)
|
||||
/* v65:看板从已加载的任务里反推(原来这个数组**从来没被赋值** ⇒
|
||||
这条筛选条永远是空的,只剩一个「全部」) */
|
||||
ForEach(this.boardOpts(), (b: FbBoardOpt) => {
|
||||
this.boardChip(b.name, b.name)
|
||||
}, (b: FbBoardOpt): string => 'bd_' + b.id.toString())
|
||||
// 「只看已完成」是设置菜单打开的筛选 ⇒ 这里给个能取消的胶囊
|
||||
if (this.statusFilter === 'done') {
|
||||
Row({ space: 4 }) {
|
||||
Text('只看已完成')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
Image($r('app.media.fb_ic_close'))
|
||||
.width(10)
|
||||
.height(10)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(this.accent))
|
||||
}
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.backgroundColor(fbAlpha(this.accent, 0.1))
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.statusFilter = '';
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
}
|
||||
@@ -222,17 +398,155 @@ export struct FbTasksPage {
|
||||
this.settingsOpen = false;
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
// 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应)
|
||||
// ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。
|
||||
/* v65:这两条原来 `action === ''`(画灰、点了不响应)。
|
||||
现在都接上了:
|
||||
· taskdone → 只看已完成(对应网页版 `?view=done`)
|
||||
· taskboards → 看板列表(挑一个看板只看它;增删改看板仍在网页端) */
|
||||
if (action === 'taskdone') {
|
||||
this.statusFilter = 'done';
|
||||
this.settingsOpen = false;
|
||||
return;
|
||||
}
|
||||
if (action === 'taskboards') {
|
||||
this.boardSheetOpen = true;
|
||||
this.settingsOpen = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 新建 / 编辑任务浮层(v65)----
|
||||
if (this.editOpen) {
|
||||
FbTaskEditSheet({
|
||||
accent: this.accent,
|
||||
boards: this.boardOpts(),
|
||||
editingId: this.editingId,
|
||||
editingTitle: this.editingTitle,
|
||||
editingNote: this.editingNote,
|
||||
editingDueAt: this.editingDueAt,
|
||||
editingBoardId: this.editingBoardId,
|
||||
onClose: (): void => {
|
||||
this.editOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.editOpen = false;
|
||||
// 服务端会派生逾期/看板色,保存后重拉一遍
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 「分类与看板」浮层(v65)----
|
||||
if (this.boardSheetOpen) {
|
||||
/* 🔴 遮罩与卡片必须**叠**(`Stack`),不能当 `Column` 的子项"排" ——
|
||||
排会把卡片挤出屏幕(本项目 2026-10-20 真机事故,见 FbTaskEditSheet 同一说明)。 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FB_MASK)
|
||||
.onClick((): void => {
|
||||
this.boardSheetOpen = false;
|
||||
})
|
||||
|
||||
Column() {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
|
||||
Column({ space: 12 }) {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('切换看板')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('挑一个看板,只看它的任务')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.boardSheetOpen = false;
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 2 }) {
|
||||
this.boardRow('全部', '', '', this.tasks.length)
|
||||
ForEach(this.boardOpts(), (b: FbBoardOpt) => {
|
||||
this.boardRow(b.name, b.name, b.color, this.countOf(b.name))
|
||||
}, (b: FbBoardOpt): string => 'bs_' + b.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.constraintSize({ maxHeight: 340 })
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.TopStart)
|
||||
|
||||
Text('增删改看板、改分类,仍在网页端「任务 → 分类与看板」里做。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(2)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 「分类与看板」浮层里的一行:色点 + 名字 + 条数(点它切筛选) */
|
||||
@Builder
|
||||
boardRow(label: string, key: string, color: string, count: number) {
|
||||
Row({ space: 8 }) {
|
||||
FbDot({ dia: 8, fillC: color !== '' ? color : this.accent })
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.boardFilter === key ? this.accent : FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(count.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
if (this.boardFilter === key) {
|
||||
Image($r('app.media.fb_ic_check'))
|
||||
.width(12)
|
||||
.height(12)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(this.accent))
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 6, right: 6, top: 11, bottom: 11 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
this.boardFilter = key;
|
||||
this.boardSheetOpen = false;
|
||||
})
|
||||
}
|
||||
|
||||
/** 看板筛选胶囊 */
|
||||
@Builder
|
||||
boardChip(label: string, key: string) {
|
||||
@@ -265,17 +579,30 @@ export struct FbTasksPage {
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 一张任务卡 */
|
||||
/** 一张任务卡(整卡可点 → 打开编辑;左边的圈单独点 → 勾完成) */
|
||||
@Builder
|
||||
taskCard(t: FbTaskItem) {
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 10 }) {
|
||||
// 状态圈:完成=实心+勾、进行中=实心+白点、待办=空心
|
||||
FbCheckCircle({
|
||||
dia: 16,
|
||||
done: t.status === 'done',
|
||||
doing: t.status === 'doing',
|
||||
color: this.accent
|
||||
/* 状态圈 + 一块透明热区。
|
||||
🔴 v65:原来这个圈只有 16vp 且**没有 `onClick`** ⇒
|
||||
「没法那个完成」就是这条 —— 点它什么都不发生。
|
||||
⇒ 外面包一层 32×32 的热区(手指点得中),并接上改状态。
|
||||
⚠️ 内层有点击时父级的整卡点击**不会**再触发(ArkUI 由最内层消费),
|
||||
所以勾完成不会顺带弹出编辑浮层。 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
FbCheckCircle({
|
||||
dia: 16,
|
||||
done: t.status === 'done',
|
||||
doing: t.status === 'doing',
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width(32)
|
||||
.height(32)
|
||||
.flexShrink(0)
|
||||
.onClick((): void => {
|
||||
this.toggleDone(t);
|
||||
})
|
||||
|
||||
Text(t.title)
|
||||
@@ -336,16 +663,42 @@ export struct FbTasksPage {
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
/* 🔴 v65:整卡可点 → 打开编辑(原来列表项**没有 `onClick`** ⇒ "点不进去") */
|
||||
.onClick((): void => {
|
||||
this.openEdit(t);
|
||||
})
|
||||
}
|
||||
|
||||
private filtered(): FbTaskItem[] {
|
||||
if (this.boardFilter.length === 0) {
|
||||
return this.tasks;
|
||||
let list: FbTaskItem[] = this.tasks;
|
||||
if (this.boardFilter.length > 0) {
|
||||
list = list.filter((t: FbTaskItem): boolean => t.board === this.boardFilter);
|
||||
}
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.board === this.boardFilter);
|
||||
/* v65:设置里的「已完成」(对应网页版 `?view=done`)。
|
||||
⚠️ 只看已完成时**不再按状态分组**(一份列表里只有一个组,分组头就没意义了),
|
||||
见 `groups()`。 */
|
||||
if (this.statusFilter === 'done') {
|
||||
list = list.filter((t: FbTaskItem): boolean => t.status === 'done');
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbTaskItem>[] {
|
||||
if (this.statusFilter === 'done') {
|
||||
const items: FbTaskItem[] = this.filtered();
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const one: FbGroup<FbTaskItem> = {
|
||||
key: 'done',
|
||||
title: '已完成',
|
||||
items: items,
|
||||
count: items.length.toString() + ' 项'
|
||||
};
|
||||
const only: FbGroup<FbTaskItem>[] = [];
|
||||
only.push(one);
|
||||
return only;
|
||||
}
|
||||
return fbGroupTask(this.filtered());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 时间轴节点分「计划 / 实际」两阶段 —— 与 Web 端一致(v41 加的四种动作按钮)。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK } from '../../common/Theme';
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbTripItem, FbTripNode } from '../../model/Types';
|
||||
import { fbMoney } from '../../model/DemoData';
|
||||
@@ -25,6 +25,12 @@ import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { FB_SET_TRIPS } from '../../model/FbSettingsMenus';
|
||||
/* v65:题图要走媒体代理(`Image()` 加不了自定义请求头,令牌得用 URL query 带过去) */
|
||||
import { fbImageSrc } from '../../data/FbAttachQueue';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { fetchTripNodes } from '../../data/FbTripRepo';
|
||||
import { FbTripNewSheet } from '../../components/FbTripNewSheet';
|
||||
import { FbTripNodeSheet } from '../../components/FbTripNodeSheet';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTripsPage {
|
||||
@@ -66,6 +72,27 @@ export struct FbTripsPage {
|
||||
private accent: string = '#3F7D5A';
|
||||
/** 当前展开的旅程 id(0 = 都没展开) */
|
||||
@Local expandedId: number = 0;
|
||||
/**
|
||||
* 「新建旅程」浮层是否打开(v65)。
|
||||
*
|
||||
* 🔴 原先这颗按钮**没有 `onTap`** ⇒ 点了什么都不发生(用户:
|
||||
* 「新建旅程啊…这些东西都还没完成」)。现在接上了。
|
||||
*/
|
||||
@Local newOpen: boolean = false;
|
||||
/** 正在编辑的旅程(0 = 新建)。点卡片的「编辑」用 */
|
||||
@Local editingId: number = 0;
|
||||
@Local editingTitle: string = '';
|
||||
@Local editingSummary: string = '';
|
||||
/** 行程节点正在加载(展开时拉 `/trips/<id>/nodes`) */
|
||||
@Local nodesLoading: boolean = false;
|
||||
/** 节点加载失败的说明(空 = 没失败) */
|
||||
@Local nodesNote: string = '';
|
||||
/** 哪一趟旅程的节点是「当前这份 `nodes`」(避免 A 的节点画到 B 上) */
|
||||
@Local nodesTripId: number = 0;
|
||||
/** 题图加载失败集合(一张图坏了不该反复重试,会让列表一直闪) */
|
||||
@Local coverFailedIds: number[] = [];
|
||||
/** 「加一段行程」浮层是否打开,以及它属于哪一趟旅程(0 = 关着) */
|
||||
@Local nodeSheetTripId: number = 0;
|
||||
|
||||
/**
|
||||
* 页面出现:先缓存、再网络(与观影/名册同一套"本地优先"流程)。
|
||||
@@ -84,10 +111,25 @@ export struct FbTripsPage {
|
||||
}
|
||||
|
||||
private backOnce(): boolean {
|
||||
/* v65:返回手势要**先关新的浮层**,再关设置菜单 ——
|
||||
顺序按"层级由浅到深"(后打开的在前),否则按一次返回会跳关两层。 */
|
||||
if (this.nodeSheetTripId > 0) {
|
||||
this.nodeSheetTripId = 0;
|
||||
return true;
|
||||
}
|
||||
if (this.newOpen) {
|
||||
this.newOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.settingsOpen) {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
}
|
||||
// 行程展开着时,返回先收起它(否则用户只能靠再点一次卡片)
|
||||
if (this.expandedId !== 0) {
|
||||
this.expandedId = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,6 +161,23 @@ export struct FbTripsPage {
|
||||
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建/编辑浮层开着 ⇒ 底部悬浮快捷栏也要让位。
|
||||
*
|
||||
* ⚠️ 与 `settingsOpen` 各管一个 @Monitor:两个浮层**不会同时开**
|
||||
* (新建时设置菜单已关),所以这里直接按 `newOpen` 赋值即可。
|
||||
*/
|
||||
@Monitor('newOpen')
|
||||
onNewChanged(): void {
|
||||
fbAppState().sheetCount = this.newOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 「加一段行程」浮层同样要让悬浮栏让位 */
|
||||
@Monitor('nodeSheetTripId')
|
||||
onNodeSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.nodeSheetTripId > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -144,7 +203,15 @@ export struct FbTripsPage {
|
||||
FbPrimaryButton({
|
||||
label: '新建旅程',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** —— 纯摆设,
|
||||
点了什么都不发生(用户报的"新建旅程没完成"就是这条)。 */
|
||||
onTap: (): void => {
|
||||
this.editingId = 0;
|
||||
this.editingTitle = '';
|
||||
this.editingSummary = '';
|
||||
this.newOpen = true;
|
||||
}
|
||||
})
|
||||
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
|
||||
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
|
||||
@@ -221,92 +288,258 @@ export struct FbTripsPage {
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 新建 / 编辑旅程浮层(v65)----
|
||||
if (this.newOpen) {
|
||||
FbTripNewSheet({
|
||||
accent: this.accent,
|
||||
editingId: this.editingId,
|
||||
editingTitle: this.editingTitle,
|
||||
editingSummary: this.editingSummary,
|
||||
onClose: (): void => {
|
||||
this.newOpen = false;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
this.newOpen = false;
|
||||
// 保存后重新拉一遍列表(服务端的城市/时间是派生的,本地拼不出来)
|
||||
this.expandedId = 0;
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 加一段行程浮层(v65)----
|
||||
if (this.nodeSheetTripId > 0) {
|
||||
FbTripNodeSheet({
|
||||
tripId: this.nodeSheetTripId,
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.nodeSheetTripId = 0;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
const id: number = this.nodeSheetTripId;
|
||||
this.nodeSheetTripId = 0;
|
||||
// 加完重拉节点(服务端会重算派生时间,列表也要跟着变)
|
||||
this.load();
|
||||
this.loadNodes(id);
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 一张旅程卡:标题 + 时间 + 进度;展开后接行程时间轴 */
|
||||
/**
|
||||
* 一张旅程卡:**题图 + 标题 + 时间/城市/段数 + 简介 + 同行人**。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20):「旅程的列表也不太对呀,应该有,题图啊、标题啊、
|
||||
* 然后城市啊,应该跟网页版的题图是类似的。」
|
||||
* ⇒ 照 `templates/trips/_list_body.php` 的 `trip-card` 重做:
|
||||
* 网页版是**横排**(左题图 132×96 + 右 `trip-main`)。
|
||||
* 手机上按同样比例缩到 96×72(4:3),右边留足文字宽度。
|
||||
*
|
||||
* ⚠️ 题图比例:网页版列表卡是 132×96 ≈ 4:3,而**详情页**的 hero 是 16:9。
|
||||
* 这里按列表卡的 4:3 走(与用户要的"跟网页版类似"一致)。
|
||||
*/
|
||||
@Builder
|
||||
tripCard(t: FbTripItem) {
|
||||
Column({ space: 0 }) {
|
||||
// 卡头
|
||||
Row({ space: 10 }) {
|
||||
// 左侧色条(模块色的竖条,一眼区分旅程)
|
||||
Column()
|
||||
.width(4)
|
||||
.height(38)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(this.accent)
|
||||
// ---- 卡头:左题图 + 右内容(横排,同网页版)----
|
||||
Row({ space: 12 }) {
|
||||
// 题图:有图放真图,没图放占位(网页版是 🧳)
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (t.coverKey !== '') {
|
||||
Image(fbImageSrc(t.coverKey, FB_TOKEN_STORE.get()))
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.objectFit(ImageFit.Cover)
|
||||
// ⚠️ 不给 alt 图标:旅程封面是实景照,失败时给纯色底比给个
|
||||
// "行李箱图标"更不刺眼(网页版失败时也是底色调)
|
||||
.onError((): void => {
|
||||
this.markCoverFailed(t.id);
|
||||
})
|
||||
}
|
||||
// 有图但加载失败时,也别留一块透明 —— 统一给底色 + 图标
|
||||
if (t.coverKey === '' || this.coverFailed(t.id)) {
|
||||
Column({ space: 4 }) {
|
||||
Image($r('app.media.fb_ic_trips'))
|
||||
.width(22)
|
||||
.height(22)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor('#F1E7D6')
|
||||
}
|
||||
}
|
||||
.width(96)
|
||||
.height(72)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.clip(true)
|
||||
.flexShrink(0)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
// 右侧:标题 / meta / 简介
|
||||
Column({ space: 4 }) {
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
// meta 一行:时间 · 📍城市 · N 段行程
|
||||
Row({ space: 10 }) {
|
||||
if (t.dateRange !== '') {
|
||||
Text(t.dateRange)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
if (t.city !== '') {
|
||||
Text('📍 ' + t.city)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
} else {
|
||||
Column().layoutWeight(1)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 段数 + 同行人数(网页版是一排 meta,这里小屏分两行更清楚)
|
||||
Row({ space: 8 }) {
|
||||
Text(t.dateRange)
|
||||
Text(t.nodeCount.toString() + ' 段行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(t.nodeCount > 0 ? this.accent : FbColors.faint)
|
||||
if (t.doneCount > 0) {
|
||||
Text('已完成 ' + t.doneCount.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
if (t.attendeeCount > 0) {
|
||||
Text('· ' + t.attendeeCount.toString() + ' 人同行')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 简介:网页版截 80 字,这里两行截断(手机一行更窄)
|
||||
if (t.summary !== '') {
|
||||
Text(t.summary)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
Text(t.sub)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
.lineHeight(17)
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
if (t.nodeCount > 0) {
|
||||
Text(t.doneCount.toString() + '/' + t.nodeCount.toString())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.accent)
|
||||
Text('行程')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
} else {
|
||||
FbTag({ text: '还没行程', tone: 'muted' })
|
||||
}
|
||||
.flexShrink(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.onClick((): void => {
|
||||
// 点卡片主体 = 展开/收起行程(网页版是"进详情",手机上一屏放不下,
|
||||
// 就地展开更顺手;编辑走下面那颗小按钮)
|
||||
const was: boolean = this.isExpanded(t.id);
|
||||
this.expandedId = was ? 0 : t.id;
|
||||
if (!was) {
|
||||
this.loadNodes(t.id);
|
||||
}
|
||||
.alignItems(HorizontalAlign.End)
|
||||
})
|
||||
|
||||
// ---- 卡尾操作条:编辑(网页版进详情页才有编辑,这里给个显式入口)----
|
||||
Row({ space: 14 }) {
|
||||
Text(this.isExpanded(t.id) ? '收起行程' : '查看行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
Column().layoutWeight(1)
|
||||
Text('编辑')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.openEdit(t);
|
||||
})
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(14)
|
||||
.height(14)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
.rotate({ angle: this.isExpanded(t.id) ? 90 : 0 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 12 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
// 展开/收起:只改本地状态,不发请求
|
||||
this.expandedId = this.isExpanded(t.id) ? 0 : t.id;
|
||||
})
|
||||
|
||||
// 展开区:行程时间轴
|
||||
// ---- 展开区:行程时间轴 ----
|
||||
if (this.isExpanded(t.id)) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: FbMetric.cardPad, right: FbMetric.cardPad })
|
||||
|
||||
if (this.nodes.length === 0) {
|
||||
Column() {
|
||||
Text('这个旅程还没有排行程')
|
||||
if (this.nodesLoading) {
|
||||
Row({ space: 8 }) {
|
||||
LoadingProgress()
|
||||
.width(16)
|
||||
.height(16)
|
||||
.color(this.accent)
|
||||
Text('正在取行程…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
} else if (this.nodesNote !== '') {
|
||||
/* 拉失败时**说实话**,不假装"还没排行程" ——
|
||||
后者会让用户以为自己的行程丢了。 */
|
||||
Column({ space: 6 }) {
|
||||
Text(this.nodesNote)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
Text('点这里重试')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.onClick((): void => {
|
||||
this.loadNodes(t.id);
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding(FbMetric.cardPad)
|
||||
} else if (this.nodes.length === 0) {
|
||||
Column({ space: 8 }) {
|
||||
Text('这个旅程还没有排行程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
this.addNodeBtn(t.id)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding(FbMetric.cardPad)
|
||||
} else {
|
||||
Column({ space: 0 }) {
|
||||
// 分两段列:「实际」(已发生的)在前,「计划」在后 —— 与 Web 端顺序一致
|
||||
this.stageBlock('实际', 'actual')
|
||||
this.stageBlock('计划', 'plan')
|
||||
// 已有节点时,加号排在最后(像网页版时间轴的末尾)
|
||||
this.addNodeBtn(t.id)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: FbMetric.cardPad })
|
||||
@@ -320,6 +553,94 @@ export struct FbTripsPage {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 打开编辑浮层(预填当前值) */
|
||||
private openEdit(t: FbTripItem): void {
|
||||
this.editingId = t.id;
|
||||
this.editingTitle = t.title;
|
||||
this.editingSummary = t.summary;
|
||||
this.newOpen = true;
|
||||
}
|
||||
|
||||
/** 展开区底部那颗「+ 加一段行程」 */
|
||||
@Builder
|
||||
addNodeBtn(tripId: number) {
|
||||
Row({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_plus'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.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)
|
||||
.margin({ top: 6 })
|
||||
.onClick((): void => {
|
||||
this.nodeSheetTripId = tripId;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉某趟旅程的行程节点。
|
||||
*
|
||||
* 🔴 v65:这是端上原先**最大**的缺口 —— `nodes` 一直是空数组,
|
||||
* 所以每趟旅程展开后永远显示"还没有排行程"。
|
||||
* 用户原话:「现在这个旅程下面也没有这个行程啊」。
|
||||
*
|
||||
* ⚠️ 三个状态要分清(否则用户会以为数据丢了):
|
||||
* ① 正在拉 → 转圈;② 拉失败 → 明说"没取到" + 可重试;
|
||||
* ③ 拉成功但确实没有 → 才说"还没排行程"。
|
||||
*/
|
||||
private async loadNodes(tripId: number): Promise<void> {
|
||||
if (this.nodesLoading) {
|
||||
return;
|
||||
}
|
||||
this.nodesLoading = true;
|
||||
this.nodesNote = '';
|
||||
this.nodesTripId = tripId;
|
||||
const rows: FbTripNode[] = await fetchTripNodes(FB_TOKEN_STORE.get(), tripId);
|
||||
this.nodesLoading = false;
|
||||
/* ⚠️ 用户可能在等待期间又点了别的旅程 ⇒ 只认"当前还展开着的那一趟",
|
||||
否则 A 的节点会画到 B 的展开区里。 */
|
||||
if (this.expandedId !== tripId) {
|
||||
return;
|
||||
}
|
||||
this.nodes = rows;
|
||||
/* 拉回来是空数组有两种可能:真的没有 / 请求失败。`fetchTripNodes` 失败也回 [],
|
||||
这里无法区分 ⇒ 由下面 `node_count` 兜一道:
|
||||
列表里说有 N 段、这里却是 0 ⇒ 那就是没取到,要明说。 */
|
||||
if (rows.length === 0) {
|
||||
const cur: FbTripItem | null = this.tripById(tripId);
|
||||
if (cur !== null && cur.nodeCount > 0) {
|
||||
this.nodesNote = '行程没取到(网络或权限问题),列表里显示有 '
|
||||
+ cur.nodeCount.toString() + ' 段。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tripById(id: number): FbTripItem | null {
|
||||
for (const t of this.trips) {
|
||||
if (t.id === id) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private markCoverFailed(id: number): void {
|
||||
if (this.coverFailedIds.indexOf(id) < 0) {
|
||||
this.coverFailedIds = this.coverFailedIds.concat([id]);
|
||||
}
|
||||
}
|
||||
|
||||
private coverFailed(id: number): boolean {
|
||||
return this.coverFailedIds.indexOf(id) >= 0;
|
||||
}
|
||||
|
||||
/** 一段阶段块(实际 / 计划)+ 它下面的节点时间轴 */
|
||||
@Builder
|
||||
stageBlock(title: string, stage: string) {
|
||||
|
||||
Reference in New Issue
Block a user