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 '自家的';
|
|||
|
|
}
|