2026-10-20 15:27:32 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 「新建旅程」浮层(鸿蒙端)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 对应网页版 `templates/trips/form.php`(新建那一路)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么单独做这一批:端上原来的「新建旅程」按钮**根本没接 `onTap`**
|
|
|
|
|
|
* (`FbTripsPage` 里 `FbPrimaryButton({ label: '新建旅程', ... })` 没有回调),
|
|
|
|
|
|
* 点了什么都不发生 —— 用户报的"新建旅程没完成"就是这一条。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 表单只放**网页版真有的三项**:
|
2026-10-22 10:15:03 +08:00
|
|
|
|
* 标题 / 简介 / 是否公开(具体城市与起止时间**不在**这里 ——
|
|
|
|
|
|
* 服务端 `Trip::create()` 显式忽略它们,城市与时间一律从行程节点**派生**,
|
|
|
|
|
|
* 见 `Trip::derived()`)。
|
2026-10-20 15:27:32 +08:00
|
|
|
|
*
|
2026-10-22 10:15:03 +08:00
|
|
|
|
* 🔴🔴 v77(用户 2026-10-22):「新建旅程那里不是有题图吗?
|
|
|
|
|
|
* 为什么这里没有增加题图的功能呢?」
|
|
|
|
|
|
* ⇒ 补上题图。之前没做是因为**服务端没有旅程的图片上传槽位**
|
|
|
|
|
|
* (只有观影的 `/movies/<id>/poster`),现在服务端加了
|
|
|
|
|
|
* `POST /api/v1/trips/<id>/cover`(槽位 `cover`,1920×1080 / 16:9),
|
|
|
|
|
|
* 与网页版 `trips/form.php` 走**同一条** `Upload::image` 管线。
|
|
|
|
|
|
* ⚠️ 端上**不裁剪**:裁/缩/转 JPEG 全在服务端,否则"改造过的客户端"
|
|
|
|
|
|
* 能存进非 16:9 的图,网页版与端上显示比例就不一致了。
|
|
|
|
|
|
* ⚠️ 新建时还没有 id ⇒ 流程是「先建 → 拿 id → 再传图」。
|
|
|
|
|
|
* 传图失败**不算创建失败**(记录已经落库了),所以要把 id 记住,
|
|
|
|
|
|
* 再点一次只重传题图,不会建出两趟一样的旅程。
|
|
|
|
|
|
* ⚠️ 暂时**不给"移除题图"**:服务端还没有"清空题图"的端点,
|
|
|
|
|
|
* 摆一颗点了不生效的按钮是本项目的红线。
|
2026-10-20 15:27:32 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
|
|
|
|
|
import { FbSheetClose } from './FbSheetClose';
|
|
|
|
|
|
import { FbSheetRing } from './FbSheetRing';
|
|
|
|
|
|
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
2026-10-22 10:15:03 +08:00
|
|
|
|
import { createTrip, updateTrip, uploadTripCover, tripIdOf, FbTripDraft } from '../data/FbTripRepo';
|
2026-10-20 15:27:32 +08:00
|
|
|
|
import { FbApiResult } from '../data/FbHttp';
|
2026-10-22 10:15:03 +08:00
|
|
|
|
import { fbImageSrc } from '../data/FbAttachQueue';
|
|
|
|
|
|
import { picker } from '@kit.CoreFileKit';
|
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
|
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbTripNewSheet {
|
|
|
|
|
|
/** 模块主题色(旅程 = 绿) */
|
|
|
|
|
|
@Param accent: string = '#3F7D5A';
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 编辑模式传进来的旅程(`null`/`undefined` = 新建)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 用 `id` 判断而不是靠"标题是否为空" —— 空标题是**合法**的中间状态
|
|
|
|
|
|
* (用户刚清空输入框,还没打完),那时不该被当成"新建"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Param editingId: number = 0;
|
|
|
|
|
|
@Param editingTitle: string = '';
|
|
|
|
|
|
@Param editingSummary: string = '';
|
|
|
|
|
|
@Param editingPublic: boolean = true;
|
2026-10-22 10:15:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 编辑时已有的题图(**裸 key**,如 `covers/xxx.jpg`;空串 = 还没设题图)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 传裸 key 而不是完整 URL:完整 URL 带令牌、会过期,
|
|
|
|
|
|
* 而且端上统一走 `fbImageSrc()` 拼(见 `FbAttachQueue`)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Param editingCoverKey: string = '';
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
|
|
|
|
|
@Event onClose: () => void = (): void => {};
|
|
|
|
|
|
/** 保存成功:父页面据此刷新列表 */
|
|
|
|
|
|
@Event onSaved: () => void = (): void => {};
|
|
|
|
|
|
|
|
|
|
|
|
@Local title: string = '';
|
|
|
|
|
|
@Local summary: string = '';
|
|
|
|
|
|
@Local isPublic: boolean = true;
|
|
|
|
|
|
@Local saving: boolean = false;
|
|
|
|
|
|
/** 错误/提示(红字) */
|
|
|
|
|
|
@Local note: string = '';
|
2026-10-22 10:15:03 +08:00
|
|
|
|
/** 刚选的题图 URI(`file://…`;空串 = 这次没换图) */
|
|
|
|
|
|
@Local pickedCover: string = '';
|
|
|
|
|
|
/** 老题图加载失败(别每次都重试) */
|
|
|
|
|
|
@Local coverFailed: boolean = false;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 已经建好的旅程 id(新建流程用)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么要记住:题图上传失败时记录**已经落库**了,
|
|
|
|
|
|
* 这时再点一次保存不能再 `POST /trips`(会建出两趟一样的旅程),
|
|
|
|
|
|
* 只能接着补传图 —— 所以要把 id 存下来。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local createdId: number = 0;
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
|
|
|
|
this.title = this.editingTitle;
|
|
|
|
|
|
this.summary = this.editingSummary;
|
|
|
|
|
|
this.isPublic = this.editingPublic;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
build() {
|
|
|
|
|
|
/* 🔴🔴 根必须是 `Stack`(见 `pitfalls.md` 的"全屏遮罩不能当 Column 子项排")。
|
|
|
|
|
|
`Column { 整高遮罩; 卡片 }` 会把卡片挤出屏幕 ⇒ 浮层"点了没反应"。 */
|
|
|
|
|
|
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: 12 }) {
|
|
|
|
|
|
// ---- 标题栏(统一版式:左标题 + 右 ✕)----
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text(this.editingId > 0 ? '编辑旅程' : '新建旅程')
|
|
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
Text('先起个名字;城市与时间等加了行程自然就有了')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.accent)
|
|
|
|
|
|
.maxLines(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.flexShrink(1)
|
|
|
|
|
|
|
|
|
|
|
|
FbSheetClose({
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.onClose();
|
|
|
|
|
|
},
|
|
|
|
|
|
color: this.accent
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
2026-10-22 10:15:03 +08:00
|
|
|
|
/* ---- 可滚区:题图 + 名字 + 简介 + 公开 ----
|
|
|
|
|
|
⚠️ v77 加了题图(16:9 ≈ 186vp 高)之后,整张浮层在小屏上会顶出
|
|
|
|
|
|
屏幕(键盘一弹更糟)⇒ 与「加一段行程」浮层同一套处理:
|
|
|
|
|
|
中间这块给 `Scroll` + `maxHeight`,底部保存按钮永远可见。
|
|
|
|
|
|
⚠️ 不写 `maxHeight` 的话,"内容不多"时浮层还是按内容自适应高度,
|
|
|
|
|
|
不会变成一块留白的死板子。 */
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column({ space: 12 }) {
|
|
|
|
|
|
// ---- 题图(v77:点整块选区,选完立刻有预览)----
|
|
|
|
|
|
this.coverPicker()
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
2026-10-22 10:15:03 +08:00
|
|
|
|
// ---- 旅程名(必填)----
|
|
|
|
|
|
this.textField('旅程名', '如「青岛·崂山 两日」', this.title,
|
|
|
|
|
|
(v: string): void => {
|
|
|
|
|
|
this.title = v;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
})
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
2026-10-22 10:15:03 +08:00
|
|
|
|
// ---- 简介 ----
|
|
|
|
|
|
this.areaField('简介', '这趟出行想记的几句话(可空)', this.summary,
|
|
|
|
|
|
(v: string): void => {
|
|
|
|
|
|
this.summary = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 是否公开 ----
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text('在列表里公开')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
Text('不公开的旅程不会出现在列表里')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
|
|
|
|
|
|
Toggle({ type: ToggleType.Switch, isOn: this.isPublic })
|
|
|
|
|
|
.selectedColor(this.accent)
|
|
|
|
|
|
.onChange((on: boolean): void => {
|
|
|
|
|
|
this.isPublic = on;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
.padding({ top: 2, bottom: 2 })
|
2026-10-20 15:27:32 +08:00
|
|
|
|
}
|
2026-10-22 10:15:03 +08:00
|
|
|
|
.width('100%')
|
2026-10-20 15:27:32 +08:00
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
2026-10-22 10:15:03 +08:00
|
|
|
|
.constraintSize({ maxHeight: 430 })
|
2026-10-20 15:27:32 +08:00
|
|
|
|
.width('100%')
|
2026-10-22 10:15:03 +08:00
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.align(Alignment.TopStart)
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
|
|
|
|
|
// ---- 错误 / 提示 ----
|
|
|
|
|
|
if (this.note !== '') {
|
|
|
|
|
|
Text(this.note)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.red)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.maxLines(3)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 底部:保存(关闭统一在右上角 ✕)----
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '建好这趟旅程'))
|
|
|
|
|
|
.fontSize(FbMetric.fontBody)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height(44)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98))
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.save();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('92%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.hitTestBehavior(HitTestMode.Transparent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 提交。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 保存中要**挡住重复点击**:连点两下会建出两趟一模一样的旅程
|
|
|
|
|
|
* (服务端不去重)。`this.saving` 在这里是真正的功能,不是装饰。
|
2026-10-22 10:15:03 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 🔴 v77 的两段式:**先落库、再传图**。
|
|
|
|
|
|
* ① 新建:`POST /trips` 拿到 id(记进 `createdId`)→ 再 `POST /trips/<id>/cover`;
|
|
|
|
|
|
* ② 编辑:`PATCH /trips/<id>` → 再传图。
|
|
|
|
|
|
* ⚠️ 传图失败**不撤销**旅程(记录已经存在,删掉反而更糟),
|
|
|
|
|
|
* 而是把错误说清楚 + 记住 id ⇒ 用户再点一次只重传图。
|
2026-10-20 15:27:32 +08:00
|
|
|
|
*/
|
|
|
|
|
|
private async save(): Promise<void> {
|
|
|
|
|
|
if (this.saving) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const title: string = this.title.trim();
|
|
|
|
|
|
if (title === '') {
|
|
|
|
|
|
this.note = '旅程得有个名字。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.saving = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
const d: FbTripDraft = {
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
summary: this.summary.trim(),
|
|
|
|
|
|
isPublic: this.isPublic
|
|
|
|
|
|
};
|
2026-10-22 10:15:03 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---- ① 旅程本体:上次只差传图(`createdId > 0`)就不要再建一条 ---- */
|
|
|
|
|
|
let id: number = this.editingId > 0 ? this.editingId : this.createdId;
|
|
|
|
|
|
if (id <= 0) {
|
|
|
|
|
|
const res: FbApiResult = await createTrip(token, d);
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.saving = false;
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
id = tripIdOf(res);
|
|
|
|
|
|
this.createdId = id;
|
|
|
|
|
|
} else if (this.editingId > 0) {
|
|
|
|
|
|
/* 编辑:标题 / 简介 / 公开每次都要重写(用户可能刚改过) */
|
|
|
|
|
|
const res: FbApiResult = await updateTrip(token, this.editingId, d);
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.saving = false;
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-20 15:27:32 +08:00
|
|
|
|
}
|
2026-10-22 10:15:03 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---- ② 题图(这次没换图就跳过)---- */
|
|
|
|
|
|
if (id > 0 && this.pickedCover !== '') {
|
|
|
|
|
|
const err: string = await uploadTripCover(token, id, this.pickedCover);
|
|
|
|
|
|
if (err !== '') {
|
|
|
|
|
|
this.saving = false;
|
|
|
|
|
|
this.note = '旅程已经保存好了,但题图没传上去:' + err + '。再点一次可以只重传题图。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.saving = false;
|
2026-10-20 15:27:32 +08:00
|
|
|
|
this.onSaved();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 10:15:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 这张图到底有没有(决定显示预览还是"点这里选题图")。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 优先级是**刚选的** > 服务端已有的:刚选的那张一定是用户眼前想要的,
|
|
|
|
|
|
* 此时老图还没被替换(要等保存后才传上去)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private hasCover(): boolean {
|
|
|
|
|
|
if (this.pickedCover !== '') {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.editingCoverKey !== '' && !this.coverFailed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选题图(系统图库选择器)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 用 `photoViewPicker` 而不是相机:选图不需要额外权限,
|
|
|
|
|
|
* 也不会因为没授相机权限卡住(与换头像同一套)。
|
|
|
|
|
|
* ⚠️ 这里**只记 URI、不上传** —— 新建时还没有 id,
|
|
|
|
|
|
* 统一在 `save()` 里"先建后传"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async pickCover(): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
|
|
|
|
|
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
|
|
|
|
|
opts.maxSelectNumber = 1;
|
|
|
|
|
|
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
|
|
|
|
|
const sel: picker.PhotoSelectResult = await p.select(opts);
|
|
|
|
|
|
if (sel.photoUris.length === 0) {
|
|
|
|
|
|
return; // 用户取消了
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pickedCover = sel.photoUris[0];
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e: BusinessError = err as BusinessError;
|
|
|
|
|
|
hilog.warn(0x0000, 'FamilyBook', '选题图失败:%{public}s', JSON.stringify(e));
|
|
|
|
|
|
this.note = '选图失败,请再试一次。';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 题图选择块(16:9,与 `ImageSlot` 的 `cover` 槽位一致)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 整块可点:手机上"点一张大图换一张大图"比"去找一个小按钮"自然得多。
|
|
|
|
|
|
* ⚠️ 预览用 `Cover`:槽位是按 16:9 裁好的,`Cover` 正好铺满不留边
|
|
|
|
|
|
* (只有查看器那种"要看清整张"的场合才用 `Contain`)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
coverPicker() {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('题图')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
Text(this.hasCover() ? '换一张' : '选一张')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.accent)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#F1E7D6')
|
|
|
|
|
|
|
|
|
|
|
|
if (this.pickedCover !== '') {
|
|
|
|
|
|
Image(this.pickedCover)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
|
|
|
} else if (this.editingCoverKey !== '' && !this.coverFailed) {
|
|
|
|
|
|
Image(fbImageSrc(this.editingCoverKey, FB_TOKEN_STORE.get()))
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
|
|
|
.onError((): void => {
|
|
|
|
|
|
this.coverFailed = true;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.hasCover()) {
|
|
|
|
|
|
Column({ space: 4 }) {
|
|
|
|
|
|
Text('+')
|
|
|
|
|
|
.fontSize(20)
|
|
|
|
|
|
.fontColor('#A8988A')
|
|
|
|
|
|
Text('点这里选题图')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor('#A8988A')
|
|
|
|
|
|
Text('16:9,上传后按这个比例裁好')
|
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor('#B8AA9C')
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.aspectRatio(16 / 9)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.clip(true)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.pickCover();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:27:32 +08:00
|
|
|
|
/** 单行输入 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
textField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(48)
|
|
|
|
|
|
|
|
|
|
|
|
TextInput({ text: value, placeholder: placeholder })
|
|
|
|
|
|
.placeholderColor(FbColors.faint)
|
|
|
|
|
|
.placeholderFont({ size: FbMetric.fontTiny })
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.height(42)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.padding({ left: 10, right: 10 })
|
|
|
|
|
|
.onChange((v: string): void => {
|
|
|
|
|
|
onChange(v);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 多行输入(简介) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
areaField(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(48)
|
|
|
|
|
|
|
|
|
|
|
|
TextArea({ text: value, placeholder: placeholder })
|
|
|
|
|
|
.placeholderColor(FbColors.faint)
|
|
|
|
|
|
.placeholderFont({ size: FbMetric.fontTiny })
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.height(84)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
|
|
|
|
|
|
.onChange((v: string): void => {
|
|
|
|
|
|
onChange(v);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|