Files
FamilyBook_harmonyos/entry/src/main/ets/data/FbTripSettingsRepo.ets
T
laoyang 58f5509c1c 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"等假绿陷阱)。
⚠️ 设备仍断连,真机未验收。
2026-10-20 16:36:30 +08:00

220 lines
7.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 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 '自家的';
}