用户 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"等假绿陷阱)。
⚠️ 设备仍断连,真机未验收。
220 lines
7.5 KiB
Plaintext
220 lines
7.5 KiB
Plaintext
/**
|
||
* 家书 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 '自家的';
|
||
}
|