v77 旅程:列表只留整卡入口 + 动作区改成 2×2 + 地址不截断 + 题图

用户 2026-10-22 的五条:

① 列表卡去掉「看行程」「编辑」——「点进去之后会有编辑,直接点进去
   看行程就可以」。卡尾整行删除,整卡可点变唯一入口(标题行尾留一个 ›
   作提示);openEdit() 一并删掉,不留死代码。

② 详情页动作区重排——「排列得不好看,乱七八糟的」。旧版是四颗彩色实心
   胶囊分两行、右端对不齐。现在 ☰ 挪到标题行右侧,四颗动作做成 2×2
   等宽(layoutWeight(1));主操作保持实心,编辑/删除改浅底同色字。
   配色与顺序仍是 v41 硬要求,一个 hex 都没改。

③ 出行方式:十种方式**不能增删**(网页版也只是只读 + "常用信息",
   而端上加行程表单里根本没有车次/车牌,那条备忘在端上无从使用)
   ⇒ 方向反过来,给加行程表单补上「车次/航班号」(火车/飞机/轮船/
   公交/地铁)和「车牌」(自驾,从车辆库选一台,服务端按 vehicle_id
   join 出车牌)。没有这两项,"出行方式"在端上就只剩一个图标。

④ 行程详情里的地址显示不全——「从哪到哪,如果地址特别长就显示不全」。
   原来出发地与到达地挤在一条 Row 里各 maxLines(1) + 省略号;
   改成「从」「到」上下两行、整段铺开不截断。

⑤ 旅程题图:端上原来根本没有上传链路(服务端只有观影的 poster 槽位)。
   服务端已加 POST /trips/<id>/cover;端上新建/编辑浮层加 16:9 题图选区,
   流程「先建 → 拿 id → 再传图」,传图失败记 id 再点只补传(不会建出
   两趟一样的旅程)。详情页空题图那块也能直接点开去加。

顺带把 multipart 上传收敛成一份(data/FbUpload.ets):原来头像 / 海报 /
观影照片各抄了一遍,旅程题图会是第四份。
This commit is contained in:
2026-10-22 10:15:03 +08:00
parent 79e942d0e4
commit c7bd26dd1b
9 changed files with 743 additions and 318 deletions
@@ -27,10 +27,9 @@ import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
import { FbSheetRing } from './FbSheetRing';
import { FbRosterPerson, FbJobRow, FbContactRow } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiOptions, FbApiResult, fbApi, fbApiBase } from '../data/FbHttp';
import { FbApiOptions, FbApiResult, fbApi } from '../data/FbHttp';
import { fbUploadOne } from '../data/FbUpload';
import { FbAvatar } from './Widgets';
import { http } from '@kit.NetworkKit';
import { fileIo } from '@kit.CoreFileKit';
import { picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@@ -1256,55 +1255,17 @@ export struct FbPersonEditSheet {
}
/**
* 真正上传(手工拼 multipart)。
* 真正上传头像。
*
* ⚠️ HarmonyOS 的 `http` 没有现成的 multipart 封装 ⇒ 手写 boundary 与分片。
* 结构就是标准 multipart/form-data:`--boundary\r\n头\r\n\r\n体\r\n--boundary--`。
* 🔴 v77(2026-10-22):multipart 的实现**搬去了 `data/FbUpload.ets`**。
* 在这之前这段代码在三个地方各有一份(头像 / 海报 / 观影照片),
* 而旅程题图又要用第四份 —— 再抄一遍必然漂移。
* ⇒ 现在只有一份 `fbUploadOne()`,这里只是带上门路与字段名。
* ⚠️ 字段名 `avatar` 必须与服务端读的那个键一致(`ApiHandler::people`)。
* @returns 空串 = 成功;否则是错误说明
*/
private async uploadAvatar(id: number, uri: string, token: string): Promise<string> {
const url: string = fbApiBase() + '/api/v1/people/' + id.toString() + '/avatar';
// 读文件(picker 给的是 file:// URI,用 fileIo 读成 ArrayBuffer)
const file: fileIo.File = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
const stat: fileIo.Stat = fileIo.statSync(file.fd);
const buf: ArrayBuffer = new ArrayBuffer(stat.size);
fileIo.readSync(file.fd, buf);
fileIo.closeSync(file);
const boundary: string = '----FbBoundary' + Date.now().toString();
const head: string = '--' + boundary + '\r\n'
+ 'Content-Disposition: form-data; name="avatar"; filename="avatar.jpg"\r\n'
+ 'Content-Type: image/jpeg\r\n\r\n';
const tail: string = '\r\n--' + boundary + '--\r\n';
const req: http.HttpRequest = http.createHttp();
try {
const resp: http.HttpResponse = await req.request(url, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
},
// ⚠️ 顺序必须是 head(字符串) → 文件(ArrayBuffer) → tail(字符串),
// 服务端按这个顺序解析出一个 avatar 分片。
extraData: [head, buf, tail],
connectTimeout: 20000,
readTimeout: 30000,
expectDataType: http.HttpDataType.STRING
});
if (resp.responseCode >= 200 && resp.responseCode < 300) {
return '';
}
const body: string = typeof resp.result === 'string' ? resp.result : '';
return 'HTTP ' + resp.responseCode.toString() + ' ' + body.substring(0, 120);
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.warn(DOMAIN, TAG, '头像上传异常:%{public}s', JSON.stringify(e));
return '网络异常';
} finally {
req.destroy();
}
return await fbUploadOne('/api/v1/people/' + id.toString() + '/avatar', 'avatar', uri, token);
}
/**