/** * 家书 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/ 改 * DELETE /api/v1/vehicles/ 删 * GET /api/v1/nodetypes 节点类型列表 * POST /api/v1/nodetypes 新增 * PATCH /api/v1/nodetypes/ 改(**改名会级联到已有行程**,见服务端注释) * DELETE /api/v1/nodetypes/ 删(内置的 / 还在用的会被拒) * * ⚠️ 删不掉的两种情况**不是错误**,是服务端的保护: * · 车辆:"还有 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 = r as Record; 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 = r as Record; 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 { 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 { /* ⚠️ 请求体一律 `Record = {}` 再逐键赋值 —— ArkTS 的 `arkts-no-untyped-obj-literals` 不认 Record 字面量 (与 `FbTripRepo.createTrip` 同一写法)。 */ const body: Record = {}; 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 { return await fbApi('/api/v1/vehicles/' + id.toString(), { method: 'DELETE', token: token }); } /** 取节点类型列表 */ export async function fetchNodeTypes(token: string): Promise { 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 { const body: Record = {}; 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 { 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 '自家的'; }