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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user