用户:① 同看的人,按照头像加名字组成标签的形式进行展示。
② 确认观影照片是否允许上传视频,不允许则需要改,要求能上传视频,
视频需要转码,参考旅程中的行程做法。
① 同看的人改头像胶囊(#83 追加)
- 原来一行纯文字「已选 2 人:杨永全、红围脖」,人多了被 maxLines(1) 截断;
- 改成每人一颗胶囊:头像(FbAvatar,有图显示图/没图回落色块字) + 名字 + 小叉,
Flex(wrap) 流式换行(Row 不换行会横向溢出,本项目 v76 同款事故);
- ⚠️ FbAvatar 必须传 imageUrl,否则有头像的人也退化成色块字
(首页观影卡 2026-10-19 踩过同一个坑)。
② 观影照片支持视频(#84)
事实核查:原来不允许。POST /movies/<id>/photos 走 Upload::image(),
只收 JPG/PNG/GIF/WebP,传视频直接拒。旅程走 Attachment::save(),
内部对 video/* 调 Video::toH264() —— 观影要支持就得接上这条路。
服务端:
- Schema VERSION 35→36,event_photos 加 kind/thumb_key/mime 三列;
kind 默认 image(老数据全是照片,默认值即正确,不需要回填 UPDATE);
建表语句同步改,新库直接是新结构。
- 上传按 MIME 分流:视频走 toH264 + 新增的 Video::poster() 抽首帧,
图片仍走 Upload::image()(裁剪压缩行为完全不变)。
- Video::poster() 取第 1 秒(手机视频开头常是黑场),失败不阻断上传。
- deletePhoto / destroy 补删 thumb_key,否则删视频留孤儿封面。
- addPhotos 兼容裸键数组与结构化数组,只有一条 INSERT 路径。
端上:
- 选择器 IMAGE_TYPE → IMAGE_VIDEO_TYPE(与旅程 FbTripNodeSheet 对齐);
海报选择器保持 IMAGE_TYPE(海报必须是图)。
- FbMovieDetail.photos 从 string[] 升到 FbMovieMedia[](带 kind/thumb/mime),
靠类型检查确保所有读取点都改到。
- 网格里视频显示首帧+播放角标(不解码,一屏 9 个视频各挂解码器必卡死);
全屏查看器用 Video 播放。⚠️ VideoOptions 只有 src/currentProgressRate/
previewUri/controller,controls/autoPlay 是属性方法不是构造参数。
验证:造 H.265 视频实测 needsTranscode=true、toH264 成功转成 h264/yuv420p、
poster 抽出 13KB 封面;完整上传后接口返回 5 条(4 image + 1 video 带 thumb);
端上网格出现 ▶ 角标、点击进全屏查看器且视频在播。
迁移已跑(35→36),4 个 PHP 文件 md5 逐个核对一致。测试数据已清理。
hvigorw assembleHap BUILD SUCCESSFUL。
This commit is contained in:
@@ -43,11 +43,13 @@
|
||||
import { FbColors, FbMetric, fbAlpha } from '../common/Theme';
|
||||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
||||
import { FbCinemaItem, FbDoubanFetch, FbDoubanSuggest, FbMovieAttendeeRow, FbMovieDetail, FbMovieDraft,
|
||||
FbMovieRepo } from '../data/FbMovieRepo';
|
||||
FbMovieMedia, FbMovieRepo } from '../data/FbMovieRepo';
|
||||
import { FbRosterPerson, FbPersonRepo } from '../data/FbPersonRepo';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { fbApiBase, fbImageUrl } from '../data/FbHttp';
|
||||
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||
/* 🔴 2026-11-06:同看的人改成「头像 + 名字」胶囊,需要 FbAvatar */
|
||||
import { FbAvatar } from './Widgets';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FbSheetClose, FbSheetTitleBar } from './FbSheetClose';
|
||||
import { FbCinemaPickSheet } from './FbCinemaPickSheet';
|
||||
@@ -141,7 +143,7 @@ export struct FbMovieNewSheet {
|
||||
* ⚠️ 与 `photoUris` 分开存:那个是"待上传的本地 uri",语义不同,
|
||||
* 混在一起会让"保存时传哪些"变得说不清。
|
||||
*/
|
||||
@Local existingPhotos: string[] = [];
|
||||
@Local existingPhotos: FbMovieMedia[] = [];
|
||||
/** 豆瓣抓到的元数据 JSON(存进库,详情页另有展示) */
|
||||
@Local doubanMeta: string = '';
|
||||
|
||||
@@ -498,7 +500,12 @@ export struct FbMovieNewSheet {
|
||||
private async pickPhotos(): Promise<void> {
|
||||
try {
|
||||
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
||||
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||
/* 🔴 2026-11-06:从 `IMAGE_TYPE` 改成 `IMAGE_VIDEO_TYPE` ——
|
||||
用户:「确认一下观影照片是不是允许上传视频,如果不允许,需要修改,
|
||||
要求能够上传视频,视频需要转码,参考旅程中的行程做法。」
|
||||
旅程那边就是 `IMAGE_VIDEO_TYPE`(见 `FbTripNodeSheet.pickPhotos`),
|
||||
这里对齐。转码在服务端做(`Video::toH264`),端上只负责把文件传上去。 */
|
||||
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
|
||||
opts.maxSelectNumber = 9;
|
||||
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
||||
const sel: picker.PhotoSelectResult = await p.select(opts);
|
||||
@@ -1022,38 +1029,115 @@ export struct FbMovieNewSheet {
|
||||
.fontColor(this.accentColor())
|
||||
.width('100%')
|
||||
|
||||
/* 第二排:已选人 + 「选择」 */
|
||||
Row({ space: 8 }) {
|
||||
Text(this.pickedLabel())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.picked.length > 0 ? FbColors.ink : FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
Text('选择')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accentColor())
|
||||
/* 第二排:已选的人 —— **头像 + 名字的胶囊标签**,流式换行 */
|
||||
Column({ space: 8 }) {
|
||||
if (this.picked.length === 0) {
|
||||
/* 没人选:还是一行可点的空框(点它去选) */
|
||||
Row({ space: 8 }) {
|
||||
Text('还没有选人')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.layoutWeight(1)
|
||||
Text('选择')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accentColor())
|
||||
}
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
this.openPersonPick();
|
||||
})
|
||||
} else {
|
||||
/* 🔴 用户 2026-11-06:「同看的人,按照头像加名字组成标签的形式进行展示。」
|
||||
⇒ 每人一颗胶囊:头像(有图显示图、没图回落色块字)+ 名字 + 小叉(删掉)。
|
||||
用 `Flex(wrap)` 而不是 `Row`:人多时 `Row` 不换行会横向溢出(本项目 v76 的坑)。 */
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(this.pickedPeople(), (p: FbRosterPerson) => {
|
||||
Row({ space: 6 }) {
|
||||
FbAvatar({
|
||||
name: p.shown !== '' ? p.shown : p.name,
|
||||
color: p.avatarColor !== '' ? p.avatarColor : this.accentColor(),
|
||||
dia: 24,
|
||||
/* ⚠️ 必须传 imageUrl:不传的话有头像的人也退化成色块字
|
||||
(首页观影卡踩过同一个坑,见 FbMovieDetailSheet 的注释) */
|
||||
imageUrl: p.avatar !== '' ? fbImageSrc(p.avatar, this.token()) : ''
|
||||
})
|
||||
Text(p.shown !== '' ? p.shown : p.name)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
/* 小叉:从已选里移除(不影响名册,只是这次不放他) */
|
||||
Text('✕')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.padding({ left: 2, right: 2 })
|
||||
.onClick((): void => {
|
||||
this.picked = this.picked.filter((x: number): boolean => x !== p.id);
|
||||
})
|
||||
}
|
||||
.padding({ left: 4, right: 8, top: 3, bottom: 3 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (p: FbRosterPerson): string => 'pk_' + p.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
/* 底部「+ 选择」:加人 */
|
||||
Row({ space: 4 }) {
|
||||
Text('+ 选择')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accentColor())
|
||||
}
|
||||
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.border({ width: 1, color: fbAlpha(this.accentColor(), 0.4) })
|
||||
.onClick((): void => {
|
||||
this.openPersonPick();
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
if (this.roster.length === 0) {
|
||||
this.bad = true;
|
||||
this.note2 = '名册还没同步,同看人稍后可以在网页端补';
|
||||
return;
|
||||
}
|
||||
this.personPickOpen = true;
|
||||
})
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/**
|
||||
* 已选的同看人(按 `picked` 的顺序映射成完整人物对象)。
|
||||
*
|
||||
* ⚠️ 返回数组而不是在模板里内联 find:模板里做复杂查询会让
|
||||
* `ForEach` 的依赖追踪变模糊(本项目反复踩过),这里一次算清。
|
||||
* ⚠️ 名单里找不到的 id(人被删了)直接跳过,不占位。
|
||||
*/
|
||||
private pickedPeople(): FbRosterPerson[] {
|
||||
const out: FbRosterPerson[] = [];
|
||||
for (const id of this.picked) {
|
||||
const p: FbRosterPerson | undefined =
|
||||
this.roster.find((x: FbRosterPerson): boolean => x.id === id);
|
||||
if (p !== undefined) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 打开「选同看人」弹框(名册没同步时给提示,不硬开一个空列表) */
|
||||
private openPersonPick(): void {
|
||||
if (this.roster.length === 0) {
|
||||
this.bad = true;
|
||||
this.note2 = '名册还没同步,同看人稍后可以在网页端补';
|
||||
return;
|
||||
}
|
||||
this.personPickOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 海报(1 张;可从豆瓣抓、也可自己选)。
|
||||
*
|
||||
@@ -1259,15 +1343,52 @@ export struct FbMovieNewSheet {
|
||||
.width('100%')
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(this.existingPhotos, (k: string) => {
|
||||
Image(fbImageSrc(k, this.token()))
|
||||
.width(60)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.alt($r('app.media.fb_ic_movies'))
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
}, (k: string): string => 'exph_' + k)
|
||||
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()))
|
||||
.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)
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
}, (m: FbMovieMedia): string => 'exph_' + m.key)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user