feat(app): album banner picker in edit form — show current banner, pick & upload on save

- api_client:uploadAlbumAssetFromFile 泛化(cover 为薄壳)+ fetchAlbumAssetUrl
  (GET /albums/{id}/assets → 指定 kind 全局行 URL)
- 编辑页 Details:封面下方新增「宣传图」3:1 选择器——编辑模式异步加载现有
  banner 预览(失败静默);保存成功后上传(同封面:失败不阻塞表单结果)
- 未保存变更判断纳入 banner;l10n zh/en
- 测试适配:编辑用例 pumpForm 后清空 banner 现状预检请求(397/397 全绿)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 11:07:54 +08:00
parent 3d45350711
commit c2143e1f94
10 changed files with 288 additions and 39 deletions
+35 -4
View File
@@ -330,17 +330,48 @@ class ApiClient {
/// 一步上传相册封面:选图 → presign → PUT → complete。
/// [filePath] 是本机源文件路径(由 singleImagePicker 返回)。
/// 返回封面短期 URL(仅用于即时预览;列表走 AlbumSummary.cover)。
Future<String> uploadAlbumCoverFromFile(
Future<String> uploadAlbumCoverFromFile(String albumId, String filePath) =>
uploadAlbumAssetFromFile(
albumId,
kind: assetKindCover,
filePath: filePath,
);
/// 资产现状(listAlbumAssetStatus):指定 kind 的全局行(locale null)URL;
/// 未设置返回 null。banner 预览用(语言行不区分——编辑页只管全局)。
Future<String?> fetchAlbumAssetUrl(
String albumId,
String filePath,
) async {
String kind, {
String? locale,
}) async {
final resp = await _send(
() => _dio.get<Map<String, dynamic>>('/v1/albums/$albumId/assets'),
);
final items = (resp.data!['items'] as List<dynamic>)
.cast<Map<String, dynamic>>();
for (final item in items) {
if (item['kind'] == kind && item['locale'] == locale) {
return item['url'] as String;
}
}
return null;
}
/// 一步上传相册资产(banner/cover/logo):选图 → presign → PUT → complete。
/// [filePath] 是本机源文件路径(由 singleImagePicker 返回)。
/// 返回资产短期 URL(仅用于即时预览;现状经 [fetchAlbumAssetUrl] 查询)。
Future<String> uploadAlbumAssetFromFile(
String albumId, {
required String kind,
required String filePath,
}) async {
final file = File(filePath);
final bytes = await file.length();
final filename = filePath.split(Platform.pathSeparator).last;
final contentType = _guessContentType(filename);
final init = await initAlbumAsset(
albumId,
kind: assetKindCover,
kind: kind,
filename: filename,
bytes: bytes,
contentType: contentType,