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:
2026-10-20 16:36:30 +08:00
parent b3e1330244
commit 58f5509c1c
5 changed files with 1198 additions and 5 deletions
@@ -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)
}
}