Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbMovieReviewSheet.ets
T
laoyang 6a8a97286a 观影详情卡:对齐、去「写评价」、补编辑入口与完整编辑 (#80)
用户:评价与评论和上面的观影照片对齐吗?下面的写评价不要了,三个字。
另外一个,是不是得有个地方放编辑按钮啊?观影其实可以编辑……后面会补几张照片。
但编辑这个功能应该还是有的。

① 对齐:FbSectionTitle 自带 padding left:4,简介/观影照片是裸 Text 无内边距,
   实测评价与评论起点 111 vs 观影照片 99(差 4vp)。用 margin left:-4 抵消。
   不改用裸 Text —— 那样会丢掉全站统一的字号/字重/字距/胶囊按钮口径。

② 去「写评价」:FbMovieReviewSheet 加 hideFormTitle;无评价时整行不画,
   但有评价时保留那一行(右边的修改/删除/收起按钮在那)。

③ 编辑入口 + 完整编辑(用户选定:按钮放卡片最底部、做完整编辑):
   - 服务端 PATCH /api/v1/movies/<id> 本来就有(movieData 支持缺省回退原值),
     这轮只补端上缺的那层;
   - FbMovieRepo 新增 updateMovie() → PATCH,成功后重拉列表回写本地库;
   - FbMovieNewSheet 加 editingId/editingDetail,aboutToAppear 走 prefill 回填,
     submit 按模式分派 POST/PATCH;标题/按钮文案随模式切换;
   - 宿主(首页 + 电影页)详情卡 onEdit → 关卡 + 同一张表单编辑;
   - 编辑时不发 poster_key(发空串会清掉已有海报);
     海报区显示「当前海报 / 不选新图就保持不变」,避免用户以为图丢了白传一次。

两个真坑:
 1) if/else if 条件里不能调方法:curPoster() 探针打出来是对的,
    界面却一直走 else —— ArkUI V2 只对字段/参数建依赖。改读 @Local 字段。
 2) Row { Text().width(100%); Row {...} } 会内层挤出:posterField 原来就是这个结构,
    标签吃满整行 ⇒ 内层塌成 0 高、完全不渲染(症状是图/字凭空消失)。
    按全局表单规范改两排。

验证(真机 5NC0225B27004718):评价与评论 x 起点 98 vs 观影照片 99 已对齐;
「写评价」不再出现;编辑按钮在 y=1987;点开标题为「编辑观影记录」,
片名/时间(10月9日 16:20)/简介全部回填;海报区三行文案齐全,
缩略图区域色数 13567(真图)。hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 08:44:36 +08:00

805 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 观影影评 + 评论浮层(鸿蒙端,工单 #27)
*
* 对应网页版 `templates/movies/show.php` 的「评价与评论」区块。
*
* 用户 2026-10-28 原话:「鸿蒙端对齐网页端」。观影详情浮层原来只能看
* 一个平均分(`listRating`),不能写影评、不能评论。这个浮层把
* 影评(打分 + 观后感受 + 正文)与评论/回复补上。
*
* ⚠️ 每人对每部电影**只有一条影评**(再次提交即修改,服务端 `MovieReview::save`
* 的 isUpdate 语义);评论是**不限条数**的,可在任一条影评下回复。
*
* ⚠️ 只有「能写的人」(访客/只读账号)隐藏发表入口 —— 与 Web 端同口径。
*/
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
import { FbAvatar } from './Widgets';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiResult } from '../data/FbHttp';
import {
FbMovieComment, FbMovieReview, FbMovieReviews,
FbMovieRepo,
} from '../data/FbMovieRepo';
import { FbAppState, fbAppState } from '../common/Store';
/** 打分的一颗星 */
function starOn(i: number, cur: number): boolean {
return i <= cur;
}
@ComponentV2
export struct FbMovieReviewSheet {
/** 观影记录 id */
@Param movieId: number = 0;
/** 电影名(显示在标题旁) */
@Param movieTitle: string = '';
/** 观影模块色 */
@Param accent: string = '#7A4E8C';
/**
* 内嵌模式(2026-11-06)。
*
* 🔴 用户原话:「电影弹出框,评价与评论放在最下面,直接显示出来,
* 可以直接评论,不用再打开了。」
* ⇒ 本组件现在有两种形态:
* · `embedded = false`(默认,老形态):自带遮罩 + 圆角卡 + 标题栏,
* 作为独立浮层弹出(`bindSheet` 之类);
* · `embedded = true`:**只出内容**(我的评价 + 全部影评 + 评论),
* 不画遮罩、不画圆角卡、不画标题栏 —— 由 `FbMovieDetailSheet`
* 在它自己的 Scroll 里直接渲染。
*
* ⚠️ 为什么不另写一份「内嵌版」:影评/评论的表单、打分、感受多选、
* 回复树有 300+ 行,抄一份必然出现"改一处漏一处"(本项目已多次踩过)。
* 加一个开关,两条路径共用同一套逻辑与 DOM。
*/
@Param embedded: boolean = false;
/**
* 隐藏表单顶部的「写评价 / 我的评价」标题行(2026-11-06)。
*
* 🔴 用户:「下面的写评价不要了,三个字。」
* ⇒ 内嵌到详情卡后,上面已经有「评价与评论」这个区块标题,
* 再出现一行「写评价」是重复的。
*
* ⚠️ 只隐藏**标题文字那一段**,右边「修改 / 删除 / 收起」三颗动作按钮
* 仍然保留 —— 有评价时用户就是靠它们改和删,删掉整行会失去功能。
* 没有评价时那一行本来就只剩标题,于是整行自然消失。
*/
@Param hideFormTitle: boolean = false;
/** 保存/删除后通知宿主(列表的平均分可能要刷新) */
@Event onChanged: () => void = (): void => {};
@Event onClose: () => void = (): void => {};
/** 我的影评(null = 还没写) */
@Local mine: FbMovieReview | null = null;
/** 全影评 + 评论 */
@Local reviews: FbMovieReview[] = [];
@Local count: number = 0;
@Local avg: number = 0;
/** 感受目录:key -> emoji + ' ' + label */
@Local catalog: Record<string, string> = {};
/** 写影评表单态 */
@Local rating: number = 0;
@Local pickedFeelings: string[] = [];
@Local draft: string = '';
@Local ratingPickOpen: boolean = false;
@Local saving: boolean = false;
/**
* 我的评价表单是否展开(2026-11-06)。
*
* 🔴 用户选择:「已评过=收起,没评过=展开」。
* · 已经写过的人:只看到评分摘要,想改再点「修改」展开 ——
* 否则每次打开详情卡都被一大块表单占住,评论被挤到很下面;
* · 还没写的人:直接看到表单,进来就能评。
*
* ⚠️ 初始值给 `false`,由 `load()` 拿到 `mine` 之后再定
* (数据还没回来时不知道写没写过,不能提前猜)。
* ⚠️ 是 `@Local` 而不是从这里反推 `this.mine !== null`:
* 用户点「修改」要能把它展开,而 `mine` 在**保存成功后**才会变,
* 两者不是同一个状态。
*/
@Local myFormOpen: boolean = false;
/** 评论输入(针对某条影评) */
@Local commentTarget: number = 0;
@Local commentDraft: string = '';
@Local replyingTo: number = 0; // 回复某条评论的 id;0 = 直接评论影评
@Local busy: boolean = false;
@Local note: string = '';
@Local loading: boolean = true;
/** 我是不是访客(只读)—— 拿 AppState 判断 */
private app: FbAppState = fbAppState();
aboutToAppear(): void {
this.load();
}
private canWrite(): boolean {
return this.app.session.me.canWrite;
}
private async load(): Promise<void> {
this.loading = true;
const r: FbMovieReviews = await FbMovieRepo.inst().reviews(this.movieId, FB_TOKEN_STORE.get());
this.loading = false;
this.reviews = r.reviews;
this.mine = r.mine;
this.count = r.count;
this.avg = r.avg;
this.catalog = r.catalog;
if (this.mine !== null) {
this.rating = this.mine.rating;
this.pickedFeelings = this.mine.feelings.slice();
this.draft = this.mine.content;
}
/* 「已评过=收起,没评过=展开」(用户 2026-11-06 选定)。
⚠️ 只在**首次加载**时决定,不能每次 load 都重置:
保存成功后 `load()` 会再跑一遍,若无条件按 `mine !== null` 赋 false,
用户刚写完评价、表单立刻收起来 —— 看着像"没保存成功"。
⇒ 用 `loading` 之前那个「首轮」标记控制(见 myFormTouched)。 */
if (!this.myFormTouched) {
this.myFormOpen = this.mine === null;
this.myFormTouched = true;
}
}
/**
* 表单开合是否已被"用户意图"或"首轮加载"定过。
*
* 为什么要这个标记:`load()` 在保存/删除后都会重跑,
* 若无条件按 `mine` 重置 `myFormOpen`,刚写完评价表单就自己收起,
* 用户会以为没保存成功。⇒ 只有第一轮才按"写没写过"决定。
*/
@Local myFormTouched: boolean = false;
private catalogKeys(): string[] {
return Object.keys(this.catalog);
}
private featText(key: string): string {
const v: string | undefined = this.catalog[key];
return v !== undefined ? v : key;
}
/** 打星(1-10,点第 n 颗 = 选 n 分) */
private pickRating(n: number): void {
if (this.rating === n) {
this.rating = 0;
} else {
this.rating = n;
}
}
/** 点一个感受 chip(多选,最多 6 个,服务端 FEELING_MAX) */
private toggleFeeling(key: string): void {
const i: number = this.pickedFeelings.indexOf(key);
if (i >= 0) {
const next: string[] = [];
for (const f of this.pickedFeelings) {
if (f !== key) {
next.push(f);
}
}
this.pickedFeelings = next;
} else if (this.pickedFeelings.length < 6) {
this.pickedFeelings = this.pickedFeelings.concat([key]);
}
}
private async saveReview(): Promise<void> {
if (this.saving) {
return;
}
if (this.rating < 1 || this.rating > 10) {
this.note = '请先打一个 1-10 的评分。';
return;
}
if (this.draft.trim() === '') {
this.note = '评价内容不能为空。';
return;
}
this.saving = true;
this.note = '';
const res: FbApiResult = await FbMovieRepo.inst().saveReview(
this.movieId, FB_TOKEN_STORE.get(), this.rating, this.draft.trim(), this.pickedFeelings);
this.saving = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '没发出去,请稍后再试。';
return;
}
/* 保存成功 ⇒ 收起表单(用户刚写完,接着就是想看结果/评论,
而不是继续盯着输入框)。⚠️ 必须在 `load()` 之后置,否则会被
load 里"首轮"判断覆盖。 */
this.myFormOpen = false;
await this.load();
this.onChanged();
}
private async deleteReview(): Promise<void> {
if (this.busy) {
return;
}
this.busy = true;
this.note = '';
const res: FbApiResult = await FbMovieRepo.inst().deleteReview(this.movieId, FB_TOKEN_STORE.get());
this.busy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '没删掉,请稍后再试。';
return;
}
/* 删掉之后「我的评价」不存在了 ⇒ 表单重新展开,让用户能再写一个
(否则会停在一个空的摘要块上)。清空草稿与打分,别把旧内容带回来。 */
this.rating = 0;
this.pickedFeelings = [];
this.draft = '';
this.myFormOpen = true;
await this.load();
this.onChanged();
}
/** 发一条评论(或回复) */
private async submitComment(): Promise<void> {
if (this.busy) {
return;
}
const txt: string = this.commentDraft.trim();
if (txt === '') {
this.note = '评论内容不能为空。';
return;
}
this.busy = true;
this.note = '';
const res: FbApiResult = await FbMovieRepo.inst().addReviewComment(
this.movieId, FB_TOKEN_STORE.get(), this.commentTarget, txt, this.replyingTo);
this.busy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '没发出去,请稍后再试。';
return;
}
this.commentTarget = 0;
this.commentDraft = '';
this.replyingTo = 0;
await this.load();
this.onChanged();
}
/** 删评论 */
private async deleteComment(cid: number): Promise<void> {
if (this.busy) {
return;
}
this.busy = true;
const res: FbApiResult = await FbMovieRepo.inst().deleteReviewComment(
this.movieId, FB_TOKEN_STORE.get(), cid);
this.busy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '没删掉,请稍后再试。';
return;
}
await this.load();
}
/** 打开「写影评」表单(已有影评时就是编辑) */
private openReviewForm(): void {
this.note = '';
// 已有影评 → 回填;没有 → 空表单
if (this.mine !== null) {
this.rating = this.mine.rating;
this.pickedFeelings = this.mine.feelings.slice();
this.draft = this.mine.content;
} else {
this.rating = 0;
this.pickedFeelings = [];
this.draft = '';
}
}
build() {
/* 🔴 用户 2026-11-06:「评价与评论放在最下面,直接显示出来,可以直接评论,
不用再打开了。」
⇒ 内嵌模式只出内容(发表遮罩/圆角卡/标题栏),由详情卡在自己的
Scroll 里直接渲染。内容与独立模式**同一份** `reviewContent()`,
不抄第二份 —— 影评/评论那 300 行抄一份必然改一处漏一处。 */
if (this.embedded) {
this.reviewContent(true)
} else {
Stack({ alignContent: Alignment.Center }) {
Column()
.width('100%')
.height('100%')
.backgroundColor(FB_MASK)
.onClick((): void => {
this.onClose();
})
Column() {
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
Column({ space: 0 }) {
// ---- 标题栏 ----
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('评价与评论' + (this.movieTitle !== '' ? ' · ' + this.movieTitle : ''))
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
Text(this.summaryText())
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accent
})
}
.width('100%')
.padding({ bottom: 8 })
.alignItems(VerticalAlign.Center)
if (this.note !== '') {
Text(this.note)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
.padding({ top: 6, bottom: 6 })
.maxLines(3)
}
Scroll() {
this.reviewContent(false)
}
.constraintSize({ maxHeight: 520 })
.width('100%')
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
}
}
/** 标题下方那句统计(独立模式用;内嵌模式由详情卡的区块标题承担) */
private summaryText(): string {
if (this.loading) {
return '正在加载…';
}
if (this.count > 0) {
return this.count.toString() + ' 条评价 · 均分 ' + this.avg.toFixed(1);
}
return '还没有评价,等你来评一个';
}
/**
* 评价与评论的正文(两种形态共用)。
*
* @param inner 是否为内嵌模式 —— 内嵌时不加额外上下留白
* (宿主已经在卡片里排好了间距,再补会多一截空)。
*/
@Builder
reviewContent(inner: boolean) {
Column({ space: 12 }) {
// ---- 我的影评(写 / 改 / 删)----
if (this.canWrite()) {
this.myReviewBlock()
}
// ---- 全部影评 + 评论 ----
if (this.reviews.length === 0 && !this.loading) {
Text('还没有评价')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width('100%')
} else {
ForEach(this.reviews, (rv: FbMovieReview) => {
this.reviewBlock(rv)
}, (rv: FbMovieReview): string => 'mvr_' + rv.id.toString())
}
}
.width('100%')
.padding({ top: inner ? 0 : 4, bottom: inner ? 0 : 20 })
.alignItems(HorizontalAlign.Start)
}
/**
* 我的影评区块。
*
* 两个态(用户 2026-11-06 选定「已评过=收起,没评过=展开」):
* · `myFormOpen === false` 且已写过 → **摘要态**:评分 + 感受 + 正文摘要
* +「修改 / 删除」,只占几行,把空间让给下面的评论;
* · `myFormOpen === true` 或无评价 → **表单态**:打分 + 感受 + 正文 + 发表。
*/
@Builder
myReviewBlock() {
if (this.mine !== null && !this.myFormOpen) {
this.myReviewSummary()
} else {
this.myReviewForm()
}
}
/** 收起态:我写过的评价摘要 + 改/删 */
@Builder
myReviewSummary() {
Column({ space: 6 }) {
Row({ space: 8 }) {
Text('我的评价')
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Bold)
.fontColor(this.accent)
.letterSpacing(0.6)
Blank()
Text('修改')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.onClick((): void => {
this.myFormOpen = true;
})
Text('删除')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.padding({ left: 6, right: 2, top: 4, bottom: 4 })
.onClick((): void => {
this.deleteReview();
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (this.mine !== null) {
Row({ space: 8 }) {
Text('★ ' + this.mine.rating.toFixed(0))
.fontSize(FbMetric.fontTiny)
.fontColor('#C08A2E')
if (this.mine.feelings.length > 0) {
Text(this.mine.feelings.map((k: string): string => this.featText(k)).join(' · '))
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (this.mine.content !== '') {
Text(this.mine.content)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
}
}
.width('100%')
.padding(12)
.backgroundColor(fbAlpha(this.accent, 0.06))
.borderRadius(FbMetric.radiusS)
.alignItems(HorizontalAlign.Start)
}
/** 展开态:写 / 改评价的表单 */
@Builder
myReviewForm() {
Column({ space: 8 }) {
/* 🔴 用户 2026-11-06:「下面的写评价不要了,三个字。」
⇒ 整行只在「有评价(要显示改/删/收起)」或「没要求隐藏标题」时才画。
内嵌 + 未评过 = 整行不存在,直接从「评分」开始,不浪费一行高度。
⚠️ 有评价时**不能**整行删掉:修改/删除/收起三颗按钮就在这行右边。 */
if (this.mine !== null || !this.hideFormTitle) {
Row({ space: 8 }) {
if (!this.hideFormTitle) {
Text(this.mine !== null ? '我的评价' : '写评价')
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Bold)
.fontColor(this.accent)
.letterSpacing(0.6)
}
Blank()
if (this.mine !== null) {
Text('收起')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.padding({ left: 6, right: 4, top: 4, bottom: 4 })
.onClick((): void => {
this.myFormOpen = false;
})
Text('删除')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.padding({ left: 8, right: 2, top: 4, bottom: 4 })
.onClick((): void => {
this.deleteReview();
})
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
// 打分(1-10 颗星)
Column({ space: 6 }) {
Row({ space: 8 }) {
Text('评分')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Row({ space: 2 }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (i: number) => {
Text(starOn(i, this.rating) ? '★' : '☆')
.fontSize(18)
.fontColor(starOn(i, this.rating) ? '#C08A2E' : FbColors.line2)
.onClick((): void => {
this.pickRating(i);
})
}, (i: number): string => 'star_' + i.toString())
}
Text(this.rating > 0 ? this.rating.toString() + ' 分' : '未评分')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
}
.alignItems(VerticalAlign.Center)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 感受多选
if (this.catalogKeys().length > 0) {
Column({ space: 6 }) {
Text('观后感受(可多选)')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.catalogKeys(), (k: string) => {
Text(this.featText(k))
.fontSize(FbMetric.fontTiny)
.fontColor(this.pickedFeelings.indexOf(k) >= 0 ? '#FFFFFF' : FbColors.ink)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(this.pickedFeelings.indexOf(k) >= 0
? this.accent : FbColors.inputBg)
.borderRadius(FbMetric.radiusPill)
.margin({ right: 6, bottom: 6 })
.onClick((): void => {
this.toggleFeeling(k);
})
}, (k: string): string => 'feel_' + k)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
// 正文
TextArea({ text: this.draft, placeholder: '写下你对这部电影的评价…' })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(90)
.width('100%')
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
.onChange((v: string): void => {
this.draft = v;
})
Text(this.mine !== null ? '保存修改' : '发表评价')
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.height(42)
.borderRadius(FbMetric.radiusS)
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.4 : 0.96))
.onClick((): void => {
this.saveReview();
})
}
.width('100%')
.padding({ bottom: 10 })
.alignItems(HorizontalAlign.Start)
}
/** 一条影评(含评论树) */
@Builder
reviewBlock(rv: FbMovieReview) {
Column({ space: 8 }) {
// 影评头
Row({ space: 8 }) {
FbAvatar({ name: rv.name, color: rv.color, dia: 30 })
Column({ space: 2 }) {
Text(rv.name)
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
Row({ space: 6 }) {
Text('★ ' + rv.rating.toFixed(0))
.fontSize(FbMetric.fontTiny)
.fontColor('#C08A2E')
if (rv.feelings.length > 0) {
Text(rv.feelings.map((k: string): string => this.featText(k).split(' ')[0]).join(''))
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
}
}
.alignItems(VerticalAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
if (rv.mine) {
Text('我写的')
.fontSize(10)
.fontColor(FbColors.faint)
}
}
.width('100%')
.alignItems(VerticalAlign.Top)
Text(rv.content)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.width('100%')
// 评论列表
if (rv.comments.length > 0) {
Column({ space: 6 }) {
ForEach(rv.comments, (c: FbMovieComment) => {
this.commentRow(rv.id, c)
}, (c: FbMovieComment): string => 'c_' + c.id.toString())
}
.width('100%')
.padding({ left: 8 })
}
// 评论入口
if (this.canWrite()) {
Row({ space: 8 }) {
Text('发表评论')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.borderRadius(FbMetric.radiusPill)
.backgroundColor(fbAlpha(this.accent, 0.08))
.onClick((): void => {
this.commentTarget = rv.id;
this.replyingTo = 0;
this.commentDraft = '';
})
}
.width('100%')
}
// 评论输入框(针对这条影评展开时)
if (this.commentTarget === rv.id) {
Column({ space: 6 }) {
TextArea({ text: this.commentDraft, placeholder: this.replyingTo > 0 ? '写下回复…' : '写下评论…' })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(60)
.width('100%')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.onChange((v: string): void => {
this.commentDraft = v;
})
Row({ space: 8 }) {
Text('取消')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.onClick((): void => {
this.commentTarget = 0;
this.commentDraft = '';
this.replyingTo = 0;
})
Blank()
Text(this.busy ? '发不出…' : '发送')
.fontSize(FbMetric.fontTiny)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(FbMetric.radiusPill)
.backgroundColor(this.accent)
.onClick((): void => {
this.submitComment();
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 8 })
}
Divider()
.color(FbColors.line)
.margin({ top: 4 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
/** 一条评论(含回复) */
@Builder
commentRow(reviewId: number, c: FbMovieComment) {
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(c.name)
.fontSize(FbMetric.fontTiny)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.muted)
if (c.replyTo !== '') {
Text('回复 ' + c.replyTo)
.fontSize(10)
.fontColor(FbColors.faint)
}
Blank()
if (c.mine) {
Text('删')
.fontSize(10)
.fontColor(FbColors.red)
.padding({ left: 4, right: 2 })
.onClick((): void => {
this.deleteComment(c.id);
})
}
if (this.canWrite()) {
Text('回复')
.fontSize(10)
.fontColor(this.accent)
.padding({ left: 4, right: 2 })
.onClick((): void => {
this.commentTarget = reviewId;
this.replyingTo = c.id;
this.commentDraft = '';
})
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(c.content)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.ink)
.width('100%')
// 回复
if (c.replies.length > 0) {
Column({ space: 4 }) {
ForEach(c.replies, (r: FbMovieComment) => {
this.commentRow(reviewId, r)
}, (r: FbMovieComment): string => 'cr_' + r.id.toString())
}
.width('100%')
.padding({ left: 10 })
}
}
.width('100%')
}
}