观影照片支持单张删除:编辑表单加 ✕ (#86)
用户:编辑观影记录的时候,那个图片应该可以被删除吧?现在没有删除按钮。
服务端早就有 Movie::deletePhoto()(连存储对象一起删,含视频 thumb_key),
网页端 movies/photo_del 也在用 —— 缺的只是 API 那一层暴露。
服务端:
- photos 子路由从"只 POST"扩为"POST 追加 + DELETE 删除";
- photo_id 走 query(dispatch 只解析到 parts[2],parts[3] 会被丢),
与 trips/<id>/nodes/attach 同口径;
- 删除前校验照片确实属于这条记录,否则 404 —— deletePhoto 内部遇到
错配是"删 0 行 + 静默成功",会让调用方以为删掉了;
- poster 仍只 POST(换海报是覆盖语义,清空请走 PATCH 空 poster_key)。
端上:
- FbMovieMedia 加 id(删除必须按 id,key 只用于取图);
- FbMovieRepo.deletePhoto() → DELETE + query;
- 编辑表单已有照片右上角加 ✕,文案改「已有 N 张(点 ✕ 可删)」;
- deletingPhotoId 防连点(第二次会 404,报错看着像功能坏了);
- 立即提交、不等「保存修改」,与网页 movies/photo_del 同口径;
不加二次确认,与旅程 removePhoto 和网页端一致。
验证:归属校验拦住越权(event_id 错配查询返回 false);临时图入库
5→4 张、库行消失、Storage::exists 由 true 变 false(驱动为 OSS);
真机 ✕ 角标 4 个等距、点掉一张后服务端 4→3、UI 变「已有 3 张」。
hvigorw assembleHap BUILD SUCCESSFUL。
⚠️ 事故:测试时删掉了用户一张真实照片(id=14),OSS 对象随之删除、
Storage::read 返回 404,无版本控制/无回收站/设备无缓存,无法恢复。
教训已写进记忆:验证删除功能绝不拿真实数据当被测对象。
This commit is contained in:
@@ -144,6 +144,13 @@ export struct FbMovieNewSheet {
|
||||
* 混在一起会让"保存时传哪些"变得说不清。
|
||||
*/
|
||||
@Local existingPhotos: FbMovieMedia[] = [];
|
||||
/**
|
||||
* 正在删除的那张照片 id(0 = 空闲)。
|
||||
*
|
||||
* 防连点:删除是网络请求,用户手快会点两次,第二次因为记录已删会 404,
|
||||
* 弹出一个"找不到这张照片"的报错 —— 看着像功能坏了,其实是重复提交。
|
||||
*/
|
||||
@Local deletingPhotoId: number = 0;
|
||||
/** 豆瓣抓到的元数据 JSON(存进库,详情页另有展示) */
|
||||
@Local doubanMeta: string = '';
|
||||
|
||||
@@ -524,6 +531,56 @@ export struct FbMovieNewSheet {
|
||||
this.photoUris = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删一张**已有**照片 / 视频(编辑模式)。
|
||||
*
|
||||
* 🔴 用户 2026-11-06:「编辑观影记录的时候,那个图片应该可以被删除吧?
|
||||
* 现在没有删除按钮。」
|
||||
*
|
||||
* ⚠️ **立即提交**,不等「保存修改」:与网页端完全一致
|
||||
* (`movies/photo_del` 删完就 redirect 回编辑页)。
|
||||
* 做成"先标记、保存时才提交"的话,用户中途关掉表单会以为自己删了、
|
||||
* 其实还在 —— 那种"看起来删了但没删"的假象更难排查。
|
||||
* ⚠️ 删完把服务端返回的最新列表整体换掉,而不是本地 splice:
|
||||
* 服务端是唯一真相(它还要删存储对象),本地猜会和实际不一致。
|
||||
* ⚠️ 左滑/点 ✕ 都走这里,所以用 `deletingPhotoId` 防止连点重复发请求
|
||||
* (第二次会 404,用户看到莫名其妙的报错)。
|
||||
*/
|
||||
private async deleteExistingPhoto(m: FbMovieMedia): Promise<void> {
|
||||
if (this.deletingPhotoId !== 0) {
|
||||
return;
|
||||
}
|
||||
const token: string = this.token();
|
||||
if (token === '') {
|
||||
this.bad = true;
|
||||
this.note2 = '还没登录';
|
||||
return;
|
||||
}
|
||||
if (this.editingId <= 0) {
|
||||
this.bad = true;
|
||||
this.note2 = '这条记录还没有 id,删不掉照片';
|
||||
return;
|
||||
}
|
||||
this.deletingPhotoId = m.id;
|
||||
const err: string = await FbMovieRepo.inst().deletePhoto(this.editingId, m.id, token);
|
||||
this.deletingPhotoId = 0;
|
||||
if (err !== '') {
|
||||
this.bad = true;
|
||||
this.note2 = err;
|
||||
return;
|
||||
}
|
||||
// 本地同步移除(服务端已删成功,这里只是让界面立刻对上)
|
||||
const next: FbMovieMedia[] = [];
|
||||
for (const x of this.existingPhotos) {
|
||||
if (x.id !== m.id) {
|
||||
next.push(x);
|
||||
}
|
||||
}
|
||||
this.existingPhotos = next;
|
||||
this.bad = false;
|
||||
this.note2 = '已删除 1 张';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 保存
|
||||
// ============================================================
|
||||
@@ -1331,64 +1388,88 @@ export struct FbMovieNewSheet {
|
||||
用户报「观影照片没有带进来」—— 原来这里只渲染 `photoUris`
|
||||
(= 本次新选的本地图),而编辑时已有的图在 `existingPhotos` 里,
|
||||
完全没画出来 ⇒ 用户以为照片丢了,会重新选一遍。
|
||||
⚠️ 只**展示 + 说明**,不做删除:删照片是另一件事(要调服务端删附件),
|
||||
用户这轮只要求"带进来能看到"。
|
||||
🔴 同一天第二轮,用户又说:「编辑观影记录的时候,那个图片应该可以
|
||||
被删除吧?现在没有删除按钮。」⇒ 每张右上角加 ✕。
|
||||
⚠️ 删除**立即提交**(与网页端 `movies/photo_del` 同口径),
|
||||
不是"标记后等保存"——那种做法用户中途关表单会以为删了、其实还在。
|
||||
⚠️ 不加二次确认:与旅程 `FbTripNodeSheet.removePhoto` 和网页端一致,
|
||||
单张照片删除不值得拦一道;整条记录删除才需要确认。
|
||||
⚠️ 条件读 `@Local existingPhotos`(不是调方法),
|
||||
否则 ArkUI 不建立依赖(同 `posterField` 那条教训)。 */
|
||||
if (this.existingPhotos.length > 0) {
|
||||
Column({ space: 6 }) {
|
||||
Text('已有 ' + this.existingPhotos.length.toString() + ' 张(保存后保持不变)')
|
||||
Text('已有 ' + this.existingPhotos.length.toString() + ' 张(点 ✕ 可删)')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(this.existingPhotos, (m: FbMovieMedia) => {
|
||||
/* 🔴 2026-11-06:已有的视频显示**首帧 + 播放角标**(不解码),
|
||||
与详情页同一套呈现口径。 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (m.kind === 'video') {
|
||||
if (m.thumb !== '') {
|
||||
Image(fbImageSrc(m.thumb, this.token()))
|
||||
/* ⚠️ 外层 Stack 用 `TopEnd` 是为了把 ✕ 顶到右上角;
|
||||
媒体自身(含视频的播放角标)放在内层 Stack 里居中,
|
||||
两者对齐方式不同,不能共用一个 Stack。 */
|
||||
Stack({ alignContent: Alignment.TopEnd }) {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (m.kind === 'video') {
|
||||
if (m.thumb !== '') {
|
||||
Image(fbImageSrc(m.thumb, this.token()))
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.alt($r('app.media.fb_ic_movies'))
|
||||
} else {
|
||||
Column()
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
Image($r('app.media.fb_ic_movies'))
|
||||
.width(22)
|
||||
.height(22)
|
||||
}
|
||||
Column() {
|
||||
Text('▶')
|
||||
.fontSize(11)
|
||||
.fontColor(Color.White)
|
||||
}
|
||||
.width(22)
|
||||
.height(22)
|
||||
.borderRadius(11)
|
||||
.backgroundColor('#99000000')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
} else {
|
||||
Image(fbImageSrc(m.key, this.token()))
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.alt($r('app.media.fb_ic_movies'))
|
||||
} else {
|
||||
Column()
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
Image($r('app.media.fb_ic_movies'))
|
||||
.width(22)
|
||||
.height(22)
|
||||
}
|
||||
Column() {
|
||||
Text('▶')
|
||||
.fontSize(11)
|
||||
.fontColor(Color.White)
|
||||
}
|
||||
.width(22)
|
||||
.height(22)
|
||||
.borderRadius(11)
|
||||
.backgroundColor('#99000000')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
} else {
|
||||
Image(fbImageSrc(m.key, this.token()))
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.alt($r('app.media.fb_ic_movies'))
|
||||
}
|
||||
.width(60)
|
||||
.height(60)
|
||||
|
||||
/* 删除角标:与「新选照片」那颗同一尺寸/配色,视觉语言统一。
|
||||
⚠️ 正在删这张时显示「…」,点不动 —— 免得连点发第二次请求。 */
|
||||
Text(this.deletingPhotoId === m.id ? '…' : '✕')
|
||||
.fontSize(10)
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.width(18)
|
||||
.height(18)
|
||||
.backgroundColor('#B0000000')
|
||||
.borderRadius(9)
|
||||
.margin({ top: 2, right: 2 })
|
||||
.onClick((): void => {
|
||||
this.deleteExistingPhoto(m);
|
||||
})
|
||||
}
|
||||
.width(60)
|
||||
.height(60)
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
}, (m: FbMovieMedia): string => 'exph_' + m.key)
|
||||
}, (m: FbMovieMedia): string => 'exph_' + m.id.toString() + '_' + m.key)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user