v66 续:旅程三项设置(车辆管理 / 出行方式 / 节点类型)
用户 2026-10-20:「旅程的……车辆管理、出行方式、节点设置,这些东西都还没完成。」
这三项的 `action` 一直是**空串**(点了没反应),是本次批量需求里最后一块。
三项的真实能力并不一样,按真实情况做,不摆假入口:
· **车辆管理**:`vehicles` 表 + `Vehicle::save/destroy` 早就有,缺的只是 API
⇒ 新增 `GET/POST/PATCH/DELETE /api/v1/vehicles`;端上列表 + 内嵌表单
(车型 / 车牌 / 自家·租的·借的 / 车主或租车公司 / 订单号 / 默认车 / 备注)
· **节点类型**:`node_types` 表 + `NodeType::save/destroy` 同理
⇒ 新增 `GET/POST/PATCH/DELETE /api/v1/nodetypes`;改名会**级联**到已有行程
(`trip_nodes.node_type` 存的是名字,服务端 `NodeType::save` 里做)
· 内置类型**不画删除按钮**(服务端会拒,画了就是"点了没反应")
· **出行方式**:**枚举常量**(`Trip::TRAVEL_MODES`),没有"管理"这回事
⇒ 只做一份只读对照表 + "加行程时选",**不做可编辑表单**
(反向验证里有一条专门锁"这个浮层里不许出现保存/删除")
几个实现上的要点:
· 两条新路由是**平级资源**(`/vehicles`、`/nodetypes`),不是 `trips/xxx` 子路由 ——
`Api::dispatch()` 只解析三段,写成四段会落到「trips 的第 0 条」= 列表
· 删不掉(还有 N 段行程在用 / 内置类型)回 **422 + 服务端原话**,不是 500:
那是"现在不能删",不是"服务器出错"
· 表单输入框**不按值传 value**:`@Builder` 的值参数不参与状态观察,
而这个浮层是"列表 + 内嵌表单"(点另一行的「编辑」时表单已经渲染着)
⇒ 会显示上一辆车的内容。改成传 key、builder 内部直接读 `this.fXxx`
· 三个浮层都遵守浮层铁律(根 `Stack` 叠放 + `hitTestBehavior(Transparent)`),
各挂一个 `@Monitor` 让底部悬浮栏让位;`backOnce()` 按层级顺序关
服务端(`app/ApiHandler.php`)已随 `familybook_push.sh` 部署,线上核对:
`/api/v1/ping` 的 features 里 `trips_vehicles` / `trips_nodetypes` 均为 1,
两个端点无令牌时回 **401 unauthorized**(说明路由存在,而不是 404 not_found)。
验证:`hvigorw assembleHap` BUILD SUCCESSFUL;静态守卫 769 条全绿;
反向验证 116 条注入全部能让守卫变红(含"字段名拼错""按值传 value"等假绿陷阱)。
⚠️ 设备仍断连,真机未验收。
This commit is contained in:
@@ -0,0 +1,835 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 旅程设置三项(鸿蒙端)
|
||||
*
|
||||
* 对应网页版 `trips/vehicles.php` / `trips/types.php` / `trips/modes.php`。
|
||||
*
|
||||
* 🔴 v66(用户 2026-10-20):「旅程的……车辆管理、出行方式、节点设置,
|
||||
* 这些东西都还没完成。」—— 网页端早就有,端上这三个菜单项的 `action`
|
||||
* 一直是**空串**(点了没反应)。
|
||||
*
|
||||
* 三个浮层一个文件,因为它们是**同一件事的三个页签**(同一套版面、同一套读写):
|
||||
* · `FbVehicleSheet` 车辆:增 / 改 / 删(列表 + 内嵌表单)
|
||||
* · `FbNodeTypeSheet` 节点类型:增 / 改 / 删
|
||||
* · `FbTravelModesSheet` 出行方式:**只读枚举**(服务端根本没有这张表)
|
||||
*
|
||||
* ⚠️ 三个都遵守本项目浮层铁律:根是 `Stack({ alignContent: Center })`
|
||||
* (遮罩与卡片"叠"、不能"排"),外层 `hitTestBehavior(Transparent)`,
|
||||
* 写操作带 `saving` 防连点。见 `pitfalls.md` 的 v64 真机事故。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
||||
import { FbSheetClose } from './FbSheetClose';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
import { FbNodeTypeItem, FbVehicleItem } from '../model/Types';
|
||||
import {
|
||||
deleteNodeType, deleteVehicle, FB_TRAVEL_MODES, FbNodeTypeDraft, FbTravelModeDef,
|
||||
FbVehicleDraft, fbVehicleTypeName, fetchNodeTypes, fetchVehicles, saveNodeType, saveVehicle
|
||||
} from '../data/FbTripSettingsRepo';
|
||||
|
||||
/** 删除红(与 `color.json` 的 red 一致;`fbAlpha` 只吃 hex,不吃 Resource) */
|
||||
const DEL_RED: string = '#C0533F';
|
||||
|
||||
/** 车辆来源三项(`Vehicle::TYPES` 的镜像) */
|
||||
const VTYPES: string[] = ['own', 'rental', 'borrowed'];
|
||||
|
||||
@ComponentV2
|
||||
export struct FbVehicleSheet {
|
||||
@Param accent: string = '#3F7D5A';
|
||||
@Event onClose: () => void = (): void => {};
|
||||
/** 有改动就通知父页面(父页面据此不必重拉,车不影响旅程列表;留着备扩展) */
|
||||
@Event onChanged: () => void = (): void => {};
|
||||
|
||||
@Local items: FbVehicleItem[] = [];
|
||||
@Local loading: boolean = true;
|
||||
@Local note: string = '';
|
||||
/** 正在编辑的车(0 = 新建表单收起) */
|
||||
@Local editingId: number = 0;
|
||||
@Local formOpen: boolean = false;
|
||||
@Local saving: boolean = false;
|
||||
/** 待确认删除的车 id(0 = 没有) */
|
||||
@Local confirmId: number = 0;
|
||||
|
||||
// ---- 表单字段 ----
|
||||
@Local fName: string = '';
|
||||
@Local fPlate: string = '';
|
||||
@Local fVtype: string = 'own';
|
||||
@Local fVendor: string = '';
|
||||
@Local fOrderNo: string = '';
|
||||
@Local fDefault: boolean = false;
|
||||
@Local fNote: string = '';
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
this.loading = true;
|
||||
const rows: FbVehicleItem[] = await fetchVehicles(FB_TOKEN_STORE.get());
|
||||
this.items = rows;
|
||||
this.loading = false;
|
||||
if (rows.length === 0 && this.note === '') {
|
||||
this.note = '还没有登记车辆。自家的、租的、借的都可以记在这里。';
|
||||
}
|
||||
}
|
||||
|
||||
private openNew(): void {
|
||||
this.editingId = 0;
|
||||
this.formOpen = true;
|
||||
this.note = '';
|
||||
this.confirmId = 0;
|
||||
this.fName = '';
|
||||
this.fPlate = '';
|
||||
this.fVtype = 'own';
|
||||
this.fVendor = '';
|
||||
this.fOrderNo = '';
|
||||
this.fDefault = false;
|
||||
this.fNote = '';
|
||||
}
|
||||
|
||||
private openEdit(v: FbVehicleItem): void {
|
||||
this.editingId = v.id;
|
||||
this.formOpen = true;
|
||||
this.note = '';
|
||||
this.confirmId = 0;
|
||||
this.fName = v.name;
|
||||
this.fPlate = v.plate;
|
||||
this.fVtype = v.vtype;
|
||||
this.fVendor = v.vendor;
|
||||
this.fOrderNo = v.orderNo;
|
||||
this.fDefault = v.isDefault;
|
||||
this.fNote = v.note;
|
||||
}
|
||||
|
||||
private async save(): Promise<void> {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
const name: string = this.fName.trim();
|
||||
if (name === '') {
|
||||
this.note = '车型名不能为空。';
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const d: FbVehicleDraft = {
|
||||
plate: this.fPlate.trim(),
|
||||
name: name,
|
||||
vtype: this.fVtype,
|
||||
vendor: this.fVendor.trim(),
|
||||
orderNo: this.fOrderNo.trim(),
|
||||
isDefault: this.fDefault,
|
||||
note: this.fNote.trim()
|
||||
};
|
||||
const res: FbApiResult = await saveVehicle(FB_TOKEN_STORE.get(), d, this.editingId);
|
||||
this.saving = false;
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
||||
return;
|
||||
}
|
||||
this.formOpen = false;
|
||||
this.note = '';
|
||||
this.onChanged();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async remove(id: number): Promise<void> {
|
||||
const res: FbApiResult = await deleteVehicle(FB_TOKEN_STORE.get(), id);
|
||||
this.confirmId = 0;
|
||||
if (!res.ok) {
|
||||
// 服务端的话已经是人话("还有 N 段行程在用这辆车"),原样显示
|
||||
this.note = res.error !== '' ? res.error : '删除失败。';
|
||||
return;
|
||||
}
|
||||
this.note = '';
|
||||
this.onChanged();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
/** 一辆车的一行 */
|
||||
@Builder
|
||||
rowOf(v: FbVehicleItem) {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(v.plate !== '' ? v.plate : v.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.flexShrink(1)
|
||||
if (v.plate !== '') {
|
||||
Text(v.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
} else {
|
||||
Blank()
|
||||
}
|
||||
if (v.isDefault) {
|
||||
Text('默认')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
.backgroundColor(fbAlpha(this.accent, 0.12))
|
||||
.borderRadius(4)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Row({ space: 8 }) {
|
||||
Text(fbVehicleTypeName(v.vtype) + (v.vendor !== '' ? ' · ' + v.vendor : ''))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
|
||||
if (this.confirmId === v.id) {
|
||||
Text('确认删除')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#FFFFFF')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.backgroundColor(DEL_RED)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.remove(v.id);
|
||||
})
|
||||
Text('取消')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.onClick((): void => {
|
||||
this.confirmId = 0;
|
||||
})
|
||||
} else {
|
||||
Text('编辑')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.onClick((): void => {
|
||||
this.openEdit(v);
|
||||
})
|
||||
Text('删除')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(DEL_RED)
|
||||
.onClick((): void => {
|
||||
this.confirmId = v.id;
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/**
|
||||
* 一行输入框(**builder 里直接读 `this.fXxx`**,不要按值传进来)。
|
||||
*
|
||||
* 🔴 按值传的 `@Builder` 参数**不参与状态观察**:点另一辆车的「编辑」时表单
|
||||
* 已经渲染着,按值传的 `value` 不会更新 ⇒ 输入框还显示上一辆车的内容。
|
||||
* ⇒ 传一个 `key`,由 builder 自己 switch 到对应字段(读的是 `this.`)。
|
||||
*/
|
||||
@Builder
|
||||
field(key: string, label: string, placeholder: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(52)
|
||||
|
||||
TextInput({ text: this.fieldValue(key), placeholder: placeholder })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(40)
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.onChange((v: string): void => {
|
||||
this.applyField(key, v);
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
/** 取某个字段的当前值(`field()` 用它;读的是 `this.`,依赖才会被登记) */
|
||||
private fieldValue(key: string): string {
|
||||
if (key === 'plate') { return this.fPlate; }
|
||||
if (key === 'vendor') { return this.fVendor; }
|
||||
if (key === 'order') { return this.fOrderNo; }
|
||||
if (key === 'note') { return this.fNote; }
|
||||
return this.fName;
|
||||
}
|
||||
|
||||
/** 写某个字段(同时清掉错误提示) */
|
||||
private applyField(key: string, v: string): void {
|
||||
if (key === 'plate') { this.fPlate = v; return; }
|
||||
if (key === 'vendor') { this.fVendor = v; return; }
|
||||
if (key === 'order') { this.fOrderNo = v; return; }
|
||||
if (key === 'note') { this.fNote = v; return; }
|
||||
this.fName = v;
|
||||
this.note = '';
|
||||
}
|
||||
|
||||
build() {
|
||||
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: 10 }) {
|
||||
// ---- 标题栏 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('车辆管理')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('自家的、租的、借的都能记;财务里的「车子」是资产,不混在这里')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.maxLines(2)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.onClose();
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 8 }) {
|
||||
if (this.loading) {
|
||||
Text('正在取车辆…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
} else if (this.items.length === 0) {
|
||||
Text('还没有登记车辆')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
} else {
|
||||
ForEach(this.items, (v: FbVehicleItem) => {
|
||||
this.rowOf(v)
|
||||
}, (v: FbVehicleItem) => v.id.toString())
|
||||
}
|
||||
|
||||
// ---- 内嵌表单(点「添加」或某行的「编辑」才出来)----
|
||||
if (this.formOpen) {
|
||||
Column({ space: 8 }) {
|
||||
this.field('name', '车型', '如「大众朗逸」')
|
||||
this.field('plate', '车牌', '可空(借的车常记不全)')
|
||||
|
||||
Row({ space: 6 }) {
|
||||
Text('来源')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(52)
|
||||
ForEach(VTYPES, (k: string) => {
|
||||
Text(fbVehicleTypeName(k))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.fVtype === k ? '#FFFFFF' : FbColors.muted)
|
||||
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
|
||||
.backgroundColor(this.fVtype === k ? this.accent : FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.fVtype = k;
|
||||
})
|
||||
}, (k: string) => k)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
this.field('vendor', '车主/租车', '租车公司 或 车主(可空)')
|
||||
this.field('order', '订单号', '租车订单号(可空)')
|
||||
this.field('note', '备注', '可空')
|
||||
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 1 }) {
|
||||
Text('设为默认车')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('加行程选车时预选它(全家只有一台)')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.fDefault })
|
||||
.selectedColor(this.accent)
|
||||
.onChange((on: boolean): void => {
|
||||
this.fDefault = on;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Row() {
|
||||
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '添加这辆车'))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98))
|
||||
.onClick((): void => {
|
||||
this.save();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(10)
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
if (this.note !== '') {
|
||||
Text(this.note)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
|
||||
// ---- 底部:添加(关闭在右上角 ✕)----
|
||||
if (!this.formOpen) {
|
||||
Row() {
|
||||
Text('添加一辆')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor('#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, 0.98))
|
||||
.onClick((): void => {
|
||||
this.openNew();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
.constraintSize({ maxHeight: '84%' })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
export struct FbNodeTypeSheet {
|
||||
@Param accent: string = '#3F7D5A';
|
||||
@Event onClose: () => void = (): void => {};
|
||||
@Event onChanged: () => void = (): void => {};
|
||||
|
||||
@Local items: FbNodeTypeItem[] = [];
|
||||
@Local loading: boolean = true;
|
||||
@Local note: string = '';
|
||||
@Local editingId: number = 0;
|
||||
@Local formOpen: boolean = false;
|
||||
@Local saving: boolean = false;
|
||||
@Local confirmId: number = 0;
|
||||
@Local fName: string = '';
|
||||
@Local fIcon: string = '';
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.items = await fetchNodeTypes(FB_TOKEN_STORE.get());
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private openNew(): void {
|
||||
this.editingId = 0;
|
||||
this.formOpen = true;
|
||||
this.note = '';
|
||||
this.confirmId = 0;
|
||||
this.fName = '';
|
||||
this.fIcon = '';
|
||||
}
|
||||
|
||||
private openEdit(t: FbNodeTypeItem): void {
|
||||
this.editingId = t.id;
|
||||
this.formOpen = true;
|
||||
this.note = '';
|
||||
this.confirmId = 0;
|
||||
this.fName = t.name;
|
||||
this.fIcon = t.icon;
|
||||
}
|
||||
|
||||
private async save(): Promise<void> {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
const name: string = this.fName.trim();
|
||||
if (name === '') {
|
||||
this.note = '类型名字不能为空。';
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const d: FbNodeTypeDraft = { name: name, icon: this.fIcon.trim() };
|
||||
const res: FbApiResult = await saveNodeType(FB_TOKEN_STORE.get(), d, this.editingId);
|
||||
this.saving = false;
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
||||
return;
|
||||
}
|
||||
this.formOpen = false;
|
||||
this.note = '';
|
||||
this.onChanged();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async remove(id: number): Promise<void> {
|
||||
const res: FbApiResult = await deleteNodeType(FB_TOKEN_STORE.get(), id);
|
||||
this.confirmId = 0;
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '删除失败。';
|
||||
return;
|
||||
}
|
||||
this.note = '';
|
||||
this.onChanged();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
@Builder
|
||||
rowOf(t: FbNodeTypeItem) {
|
||||
Row({ space: 8 }) {
|
||||
Text(t.icon !== '' ? t.icon : '·')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.width(22)
|
||||
.textAlign(TextAlign.Center)
|
||||
Column({ space: 1 }) {
|
||||
Text(t.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
if (t.isSystem || t.usedCount > 0) {
|
||||
Text(t.isSystem ? '内置类型(可改名,不能删)' : ('已被 ' + t.usedCount.toString() + ' 段行程用过'))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
if (this.confirmId === t.id) {
|
||||
Text('确认删除')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#FFFFFF')
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.backgroundColor(DEL_RED)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.remove(t.id);
|
||||
})
|
||||
Text('取消')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.onClick((): void => {
|
||||
this.confirmId = 0;
|
||||
})
|
||||
} else {
|
||||
Text('改')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.onClick((): void => {
|
||||
this.openEdit(t);
|
||||
})
|
||||
// 🔴 内置类型直接不画删除 —— 画了再被服务端拒是"点了没反应"
|
||||
if (!t.isSystem) {
|
||||
Text('删')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(DEL_RED)
|
||||
.onClick((): void => {
|
||||
this.confirmId = t.id;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 10, right: 10, top: 9, bottom: 9 })
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
/** 一行输入框(同 `FbVehicleSheet.field`:builder 里直接读 `this.`,别按值传) */
|
||||
@Builder
|
||||
field(key: string, label: string, placeholder: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(40)
|
||||
TextInput({ text: key === 'icon' ? this.fIcon : this.fName, placeholder: placeholder })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(40)
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.onChange((v: string): void => {
|
||||
if (key === 'icon') {
|
||||
this.fIcon = v;
|
||||
return;
|
||||
}
|
||||
this.fName = v;
|
||||
this.note = '';
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
build() {
|
||||
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: 10 }) {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('节点类型')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('加行程时选的那个「游玩 / 用餐 / 赶路…」;改名会同步到已有行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.maxLines(2)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.onClose();
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 8 }) {
|
||||
if (this.loading) {
|
||||
Text('正在取类型…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
} else {
|
||||
ForEach(this.items, (t: FbNodeTypeItem) => {
|
||||
this.rowOf(t)
|
||||
}, (t: FbNodeTypeItem) => t.id.toString())
|
||||
}
|
||||
|
||||
if (this.formOpen) {
|
||||
Column({ space: 8 }) {
|
||||
this.field('name', '名称', '如「拍照」')
|
||||
this.field('icon', '图标', '一个表情,如 📷')
|
||||
Row() {
|
||||
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '添加类型'))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98))
|
||||
.onClick((): void => {
|
||||
this.save();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(10)
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
if (this.note !== '') {
|
||||
Text(this.note)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
|
||||
if (!this.formOpen) {
|
||||
Row() {
|
||||
Text('添加类型')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor('#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, 0.98))
|
||||
.onClick((): void => {
|
||||
this.openNew();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
.constraintSize({ maxHeight: '84%' })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 出行方式:**只读**。
|
||||
*
|
||||
* 🔴 为什么没有增删改:出行方式是服务端的**枚举常量**
|
||||
* (`Trip::TRAVEL_MODES`),加一项要改代码、要重新部署 ——
|
||||
* 端上给它做一个"可新增"的表单只会让人以为能加。
|
||||
* ⇒ 这里就是一份对照表 + 一句"在加行程时选"。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbTravelModesSheet {
|
||||
@Param accent: string = '#3F7D5A';
|
||||
@Event onClose: () => void = (): void => {};
|
||||
|
||||
build() {
|
||||
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: 10 }) {
|
||||
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.onClose();
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_TRAVEL_MODES, (m: FbTravelModeDef) => {
|
||||
Text(m.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.padding({ left: 11, right: 11, top: 6, bottom: 6 })
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.margin({ right: 6, bottom: 6 })
|
||||
}, (m: FbTravelModeDef) => m.key)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text('这类方式没有车牌/车次信息;自驾想记车牌请到「车辆管理」里登记,'
|
||||
+ '车次/航班号在加行程时填。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(4)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 旅程三项设置的**写操作**(鸿蒙端)
|
||||
*
|
||||
* 🔴 v66(用户 2026-10-20):「旅程的……车辆管理、出行方式、节点设置,
|
||||
* 这些东西都还没完成。」
|
||||
*
|
||||
* 三件事的实际情况并不一样,这里按真实情况办:
|
||||
* · **车辆管理** → `vehicles` 表 + `Vehicle::save/destroy`,端上补增删改;
|
||||
* · **节点类型** → `node_types` 表 + `NodeType::save/destroy`,端上补增删改;
|
||||
* · **出行方式** → **枚举常量**(`Trip::TRAVEL_MODES`),根本没有"管理"这回事
|
||||
* ⇒ 只做一份只读说明 + 提示"在行程里选",**不摆假入口**。
|
||||
*
|
||||
* 服务端契约(v66 新增,与网页版 `trips/vehicles.php` / `trips/types.php` 同一套模型):
|
||||
* GET /api/v1/vehicles 车辆列表(含 used_count)
|
||||
* POST /api/v1/vehicles 新增
|
||||
* PATCH /api/v1/vehicles/<id> 改
|
||||
* DELETE /api/v1/vehicles/<id> 删
|
||||
* GET /api/v1/nodetypes 节点类型列表
|
||||
* POST /api/v1/nodetypes 新增
|
||||
* PATCH /api/v1/nodetypes/<id> 改(**改名会级联到已有行程**,见服务端注释)
|
||||
* DELETE /api/v1/nodetypes/<id> 删(内置的 / 还在用的会被拒)
|
||||
*
|
||||
* ⚠️ 删不掉的两种情况**不是错误**,是服务端的保护:
|
||||
* · 车辆:"还有 N 段行程在用这辆车"
|
||||
* · 类型:内置类型 / 还有 N 段行程在用
|
||||
* ⇒ 端上把 `error` 原文显示出来即可(服务端那句话已经是人话)。
|
||||
*/
|
||||
|
||||
import { fbApi, FbApiResult } from './FbHttp';
|
||||
import { FbNodeTypeItem, FbVehicleItem } from '../model/Types';
|
||||
|
||||
/** 新建/修改车辆时提交的字段(与网页版 `vehicles.php` 的表单对齐) */
|
||||
export interface FbVehicleDraft {
|
||||
plate: string;
|
||||
name: string;
|
||||
/** `own` / `rental` / `borrowed` */
|
||||
vtype: string;
|
||||
vendor: string;
|
||||
orderNo: string;
|
||||
isDefault: boolean;
|
||||
note: string;
|
||||
}
|
||||
|
||||
/** 新建/修改节点类型时提交的字段 */
|
||||
export interface FbNodeTypeDraft {
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/** 字符串读值(服务端可能给 null) */
|
||||
function s(v: Object | null | undefined): string {
|
||||
return typeof v === 'string' ? v : '';
|
||||
}
|
||||
|
||||
/** 数字读值 */
|
||||
function n(v: Object | null | undefined): number {
|
||||
return typeof v === 'number' ? v : 0;
|
||||
}
|
||||
|
||||
/** 车辆列表 → 端上类型 */
|
||||
export function parseVehicles(json: string): FbVehicleItem[] {
|
||||
let rows: Object[] = [];
|
||||
try {
|
||||
const v: Object = JSON.parse(json) as Object;
|
||||
if (Array.isArray(v)) {
|
||||
rows = v as Object[];
|
||||
}
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
const out: FbVehicleItem[] = [];
|
||||
for (const r of rows) {
|
||||
const o: Record<string, Object> = r as Record<string, Object>;
|
||||
const it: FbVehicleItem = {
|
||||
id: n(o['id'] as number),
|
||||
plate: s(o['plate'] as string),
|
||||
name: s(o['name'] as string),
|
||||
vtype: s(o['vtype'] as string) !== '' ? s(o['vtype'] as string) : 'own',
|
||||
vendor: s(o['vendor'] as string),
|
||||
orderNo: s(o['order_no'] as string),
|
||||
isDefault: n(o['is_default'] as number) > 0,
|
||||
note: s(o['note'] as string),
|
||||
// ⚠️ 服务端不一定给 `used_count`(老服务端没有这条接口时会 404,
|
||||
// 这里拿不到就按 0 处理;删的时候仍由服务端把关)
|
||||
usedCount: n(o['used_count'] as number)
|
||||
};
|
||||
out.push(it);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 节点类型列表 → 端上类型 */
|
||||
export function parseNodeTypes(json: string): FbNodeTypeItem[] {
|
||||
let rows: Object[] = [];
|
||||
try {
|
||||
const v: Object = JSON.parse(json) as Object;
|
||||
if (Array.isArray(v)) {
|
||||
rows = v as Object[];
|
||||
}
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
const out: FbNodeTypeItem[] = [];
|
||||
for (const r of rows) {
|
||||
const o: Record<string, Object> = r as Record<string, Object>;
|
||||
const it: FbNodeTypeItem = {
|
||||
id: n(o['id'] as number),
|
||||
name: s(o['name'] as string),
|
||||
icon: s(o['icon'] as string),
|
||||
isSystem: n(o['is_system'] as number) > 0,
|
||||
usedCount: n(o['used_count'] as number)
|
||||
};
|
||||
if (it.name !== '') {
|
||||
out.push(it);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 取车辆列表 */
|
||||
export async function fetchVehicles(token: string): Promise<FbVehicleItem[]> {
|
||||
const r: FbApiResult = await fbApi('/api/v1/vehicles', { token: token });
|
||||
if (!r.ok) {
|
||||
return [];
|
||||
}
|
||||
return parseVehicles(r.data);
|
||||
}
|
||||
|
||||
/** 新增 / 修改一辆车(`id > 0` = 改) */
|
||||
export async function saveVehicle(token: string, d: FbVehicleDraft,
|
||||
id: number = 0): Promise<FbApiResult> {
|
||||
/* ⚠️ 请求体一律 `Record<string, Object> = {}` 再逐键赋值 ——
|
||||
ArkTS 的 `arkts-no-untyped-obj-literals` 不认 Record 字面量
|
||||
(与 `FbTripRepo.createTrip` 同一写法)。 */
|
||||
const body: Record<string, Object> = {};
|
||||
body['plate'] = d.plate;
|
||||
body['name'] = d.name;
|
||||
body['vtype'] = d.vtype;
|
||||
body['vendor'] = d.vendor;
|
||||
body['order_no'] = d.orderNo;
|
||||
body['is_default'] = d.isDefault ? 1 : 0;
|
||||
body['note'] = d.note;
|
||||
const path: string = id > 0 ? '/api/v1/vehicles/' + id.toString() : '/api/v1/vehicles';
|
||||
return await fbApi(path, {
|
||||
method: id > 0 ? 'PATCH' : 'POST',
|
||||
token: token,
|
||||
body: body
|
||||
});
|
||||
}
|
||||
|
||||
/** 删一辆车(服务端会拦"还有行程在用") */
|
||||
export async function deleteVehicle(token: string, id: number): Promise<FbApiResult> {
|
||||
return await fbApi('/api/v1/vehicles/' + id.toString(), { method: 'DELETE', token: token });
|
||||
}
|
||||
|
||||
/** 取节点类型列表 */
|
||||
export async function fetchNodeTypes(token: string): Promise<FbNodeTypeItem[]> {
|
||||
const r: FbApiResult = await fbApi('/api/v1/nodetypes', { token: token });
|
||||
if (!r.ok) {
|
||||
return [];
|
||||
}
|
||||
return parseNodeTypes(r.data);
|
||||
}
|
||||
|
||||
/** 新增 / 修改一种节点类型(`id > 0` = 改;改名会级联到已有行程) */
|
||||
export async function saveNodeType(token: string, d: FbNodeTypeDraft,
|
||||
id: number = 0): Promise<FbApiResult> {
|
||||
const body: Record<string, Object> = {};
|
||||
body['name'] = d.name;
|
||||
body['icon'] = d.icon;
|
||||
const path: string = id > 0 ? '/api/v1/nodetypes/' + id.toString() : '/api/v1/nodetypes';
|
||||
return await fbApi(path, {
|
||||
method: id > 0 ? 'PATCH' : 'POST',
|
||||
token: token,
|
||||
body: body
|
||||
});
|
||||
}
|
||||
|
||||
/** 删一种节点类型(内置 / 还在用的会被拒) */
|
||||
export async function deleteNodeType(token: string, id: number): Promise<FbApiResult> {
|
||||
return await fbApi('/api/v1/nodetypes/' + id.toString(), { method: 'DELETE', token: token });
|
||||
}
|
||||
|
||||
/**
|
||||
* 出行方式(**只读枚举**,与 `app/Trip.php` 的 `TRAVEL_MODES` **同一套 key**)。
|
||||
*
|
||||
* ⚠️ 它不是一张表,没有增删改 —— 「出行方式设置」这项在端上就是一份说明:
|
||||
* 列出这十种、并告诉用户"在加行程时选"。
|
||||
* 🔴 不要照"车辆管理"的样子给它做一个能新增的表单:服务端根本没这回事,
|
||||
* 做出来只是又一个"点了没用"的入口。
|
||||
*/
|
||||
export interface FbTravelModeDef {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const FB_TRAVEL_MODES: FbTravelModeDef[] = [
|
||||
{ key: 'walk', name: '步行' },
|
||||
{ key: 'drive', name: '自驾' },
|
||||
{ key: 'bus', name: '公交' },
|
||||
{ key: 'subway', name: '地铁' },
|
||||
{ key: 'train', name: '火车' },
|
||||
{ key: 'plane', name: '飞机' },
|
||||
{ key: 'ship', name: '轮船' },
|
||||
{ key: 'taxi', name: '打车' },
|
||||
{ key: 'bike', name: '骑行' },
|
||||
{ key: 'other', name: '其他' }
|
||||
];
|
||||
|
||||
/** 车辆来源的中文名(`Vehicle::TYPES` 的镜像) */
|
||||
export function fbVehicleTypeName(vtype: string): string {
|
||||
if (vtype === 'rental') {
|
||||
return '租的';
|
||||
}
|
||||
if (vtype === 'borrowed') {
|
||||
return '借的';
|
||||
}
|
||||
return '自家的';
|
||||
}
|
||||
@@ -70,9 +70,11 @@ export const FB_SET_TASKS: FbSettingItem[] = [
|
||||
|
||||
/** 旅程(templates/trips/_settings_menu.php) */
|
||||
export const FB_SET_TRIPS: FbSettingItem[] = [
|
||||
{ icon: 'nav', label: '车辆管理', hint: '自驾时的车与油耗', action: '' },
|
||||
{ icon: 'trips', label: '出行方式', hint: '飞机 / 高铁 / 自驾…', action: '' },
|
||||
{ icon: 'dot', label: '节点类型', hint: '行程节点的类型', action: '' }
|
||||
{ icon: 'nav', label: '车辆管理', hint: '自家的 / 租的 / 借的车', action: 'vehicles' },
|
||||
/* ⚠️ 出行方式是**枚举常量**(`Trip::TRAVEL_MODES`),没有增删改 ——
|
||||
这个浮层只是一份对照表 + "加行程时选",**不做成可编辑的表单**。 */
|
||||
{ icon: 'trips', label: '出行方式', hint: '飞机 / 高铁 / 自驾…(只读)', action: 'travelmodes' },
|
||||
{ icon: 'dot', label: '节点类型', hint: '游玩 / 用餐 / 赶路…(可自定义)', action: 'nodetypes' }
|
||||
];
|
||||
|
||||
/** 资源(网页版没有独立菜单;这里只放端上真做得到的两个筛选)
|
||||
|
||||
@@ -475,6 +475,55 @@ export interface FbTripNode {
|
||||
expenseCent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一辆"出行用的车"(`vehicles` 表)。
|
||||
*
|
||||
* 🔴 v66(用户 2026-10-20):「车辆管理、出行方式、节点设置,这些东西都还没完成。」
|
||||
* 网页端早就有这块(`trips/vehicles.php`),端上一直没有入口 ——
|
||||
* 表在 `Schema.php` 里、模型方法(`Vehicle::save/destroy`)也齐,
|
||||
* 缺的只是 **API 路由**(v66 补了 `/api/v1/vehicles`)。
|
||||
*
|
||||
* ⚠️ 与财务里的「车」是**两回事**(服务端注释写得很清楚):
|
||||
* 财务那条 `kind='car'` 的账目记的是"这台车值多少钱",没有车牌车型;
|
||||
* 这里的车才有车牌、车型、是自家/租的/借的。
|
||||
* 用户 2026-10-05 明确要求选车下拉里**不要**混进财务的车。
|
||||
*/
|
||||
export interface FbVehicleItem {
|
||||
id: number;
|
||||
/** 车牌(借的/租的可能没记全,允许空) */
|
||||
plate: string;
|
||||
/** 车型名,如「大众朗逸」 */
|
||||
name: string;
|
||||
/** `own` 自家的 / `rental` 租的 / `borrowed` 借的 */
|
||||
vtype: string;
|
||||
/** 租车公司 或 车主 */
|
||||
vendor: string;
|
||||
/** 租车订单号 */
|
||||
orderNo: string;
|
||||
/** 默认车(新建行程选车时预选它;服务端保证全家只有一台) */
|
||||
isDefault: boolean;
|
||||
note: string;
|
||||
/** 已被多少段行程引用(>0 时服务端不许删,端上先灰掉删除按钮) */
|
||||
usedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一种行程节点类型(`node_types` 表,可自定义)。
|
||||
*
|
||||
* ⚠️ `isSystem` 的内置类型**不许删**(服务端会拒),端上就别画删除按钮 ——
|
||||
* 画了再报错是"点了没反应 + 弹一句看不懂的话"。
|
||||
*/
|
||||
export interface FbNodeTypeItem {
|
||||
id: number;
|
||||
name: string;
|
||||
/** 一个表情符号(可空) */
|
||||
icon: string;
|
||||
/** 内置类型:不许删,允许改名/改图标 */
|
||||
isSystem: boolean;
|
||||
/** 已被多少段行程用过(>0 时服务端不许删) */
|
||||
usedCount: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 日程(calendar)
|
||||
// ============================================================
|
||||
|
||||
@@ -31,6 +31,8 @@ import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { fetchTripNodes } from '../../data/FbTripRepo';
|
||||
import { FbTripNewSheet } from '../../components/FbTripNewSheet';
|
||||
import { FbTripNodeSheet } from '../../components/FbTripNodeSheet';
|
||||
/* v66:旅程三项设置(车辆 / 出行方式 / 节点类型) */
|
||||
import { FbNodeTypeSheet, FbTravelModesSheet, FbVehicleSheet } from '../../components/FbTripsSettingsSheets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTripsPage {
|
||||
@@ -94,6 +96,15 @@ export struct FbTripsPage {
|
||||
/** 「加一段行程」浮层是否打开,以及它属于哪一趟旅程(0 = 关着) */
|
||||
@Local nodeSheetTripId: number = 0;
|
||||
|
||||
/**
|
||||
* 🔴 v66(用户 2026-10-20):「旅程的……车辆管理、出行方式、节点设置,
|
||||
* 这些东西都还没完成。」
|
||||
* 三项设置各管一个开关(同时只会开一个 —— 设置菜单点一项就关菜单再开它)。
|
||||
*/
|
||||
@Local vehiclesOpen: boolean = false;
|
||||
@Local travelModesOpen: boolean = false;
|
||||
@Local nodeTypesOpen: boolean = false;
|
||||
|
||||
/**
|
||||
* 页面出现:先缓存、再网络(与观影/名册同一套"本地优先"流程)。
|
||||
*
|
||||
@@ -117,6 +128,20 @@ export struct FbTripsPage {
|
||||
this.nodeSheetTripId = 0;
|
||||
return true;
|
||||
}
|
||||
/* v66:三项设置浮层。⚠️ 内部还有"表单展开 / 待确认删除"两层,
|
||||
按返回先由浮层自己处理(组件的遮罩 onClick 也会关)—— 这里只负责关整层。 */
|
||||
if (this.vehiclesOpen) {
|
||||
this.vehiclesOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.travelModesOpen) {
|
||||
this.travelModesOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.nodeTypesOpen) {
|
||||
this.nodeTypesOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (this.newOpen) {
|
||||
this.newOpen = false;
|
||||
return true;
|
||||
@@ -178,6 +203,28 @@ export struct FbTripsPage {
|
||||
fbAppState().sheetCount = this.nodeSheetTripId > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三个设置浮层同样要让悬浮栏让位。
|
||||
*
|
||||
* ⚠️ 三个 `@Monitor` 各写一个(V2 里一个 `@Monitor` 只能盯一个变量);
|
||||
* 它们**不会同时为真**(设置菜单点一项就关菜单、只开一个浮层),
|
||||
* 所以每次都直接赋值即可 —— 不用"有几个开着就加几"。
|
||||
*/
|
||||
@Monitor('vehiclesOpen')
|
||||
onVehiclesChanged(): void {
|
||||
fbAppState().sheetCount = this.vehiclesOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
@Monitor('travelModesOpen')
|
||||
onTravelModesChanged(): void {
|
||||
fbAppState().sheetCount = this.travelModesOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
@Monitor('nodeTypesOpen')
|
||||
onNodeTypesChanged(): void {
|
||||
fbAppState().sheetCount = this.nodeTypesOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
build() {
|
||||
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
|
||||
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
|
||||
@@ -282,8 +329,18 @@ export struct FbTripsPage {
|
||||
this.settingsOpen = false;
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
// 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应)
|
||||
// ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。
|
||||
/* 🔴 v66:三项设置终于接上了(原来这里的注释写着"目前都还没有")。
|
||||
⚠️ 先关设置菜单再开目标浮层:两个浮层叠在一起时,
|
||||
`sheetCount` 会被后赋值的那个覆盖,悬浮栏让位就乱了。
|
||||
(本页每个 @Monitor 都是"直接赋值",不是累加。) */
|
||||
this.settingsOpen = false;
|
||||
if (action === 'vehicles') {
|
||||
this.vehiclesOpen = true;
|
||||
} else if (action === 'travelmodes') {
|
||||
this.travelModesOpen = true;
|
||||
} else if (action === 'nodetypes') {
|
||||
this.nodeTypesOpen = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
@@ -327,6 +384,37 @@ export struct FbTripsPage {
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 旅程三项设置浮层(v66)----
|
||||
if (this.vehiclesOpen) {
|
||||
FbVehicleSheet({
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.vehiclesOpen = false;
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
if (this.travelModesOpen) {
|
||||
FbTravelModesSheet({
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.travelModesOpen = false;
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
if (this.nodeTypesOpen) {
|
||||
FbNodeTypeSheet({
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.nodeTypesOpen = false;
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
Reference in New Issue
Block a user