08-Flutter 鸿蒙实战 08:故事详情页,编辑、删除、重新生成与底部弹层

08. Flutter 鸿蒙实战 08:故事详情页,编辑、删除、重新生成与底部弹层

故事详情不只展示正文,还要处理编辑、删除、语音、活化和数据同步。

详情页职责

StoryDetailPage 是整个项目里逻辑最复杂的页面之一。它要显示故事、照片、人物、地点、时间、语音讲述、照片活化状态,还要支持编辑、删除、重新生成故事。文件位于 lib/features/stories/story_detail_page.dart

页面接收预加载数据:

dart 复制代码
class StoryDetailPage extends StatefulWidget {
  const StoryDetailPage({
    super.key,
    required this.storyId,
    required this.repository,
    this.initialStory,
    this.initialPhotoInfo,
    this.initialVideoFilePath,
  });
}

这样从列表进入详情时,可以先展示已有内容,不必等详情接口全部完成。

初始化和刷新

详情页 initState 中先使用初始数据,如果缺少再请求:

dart 复制代码
_story = widget.initialStory;
_photoInfo = widget.initialPhotoInfo;
_syncAnimationPolling(_photoInfo);
if (_story == null || _photoInfo == null) {
  _load();
} else {
  unawaited(_load(forceRefresh: false));
}

这种模式适合移动端详情页:先展示缓存,再后台校正。用户看到内容更快,数据也不会长期停留在旧状态。

编辑故事

编辑采用底部弹层,不跳新页面:

dart 复制代码
final draft = await showModalBottomSheet<StoryEditDraft>(
  context: context,
  isScrollControlled: true,
  backgroundColor: Colors.white,
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(22)),
  ),
  builder: (_) => _StoryEditSheet(story: story),
);

移动端表单用底部弹层比新页面轻。故事详情仍在背景里,用户知道自己正在编辑哪条故事。

保存和同步元数据

保存故事时,不只更新故事正文,还会同步照片的年代、地点、人物等元数据:

dart 复制代码
final shouldOfferRegenerate = _shouldOfferRegenerate(story, draft);
final contentChanged = story.content.trim() != draft.content.trim();
final updated = await widget.repository.update(story.id, draft);
await widget.repository.updatePhotoMetadata(
  updated.photoId,
  era: draft.era,
  location: draft.location,
  persons: draft.persons,
);

故事和照片元数据是两套数据,但它们在业务上有关联。用户修改地点、人物后,照片本身也应该同步更新,否则地图、故事详情、照片管理页会出现不一致。

是否需要重新生成

当年代、地点、日期、人物变化时,旧故事正文可能不再准确。项目用 _shouldOfferRegenerate 判断是否提示重新生成:

dart 复制代码
bool _shouldOfferRegenerate(Story story, StoryEditDraft draft) {
  return story.era.trim() != draft.era.trim() ||
      story.location.trim() != draft.location.trim() ||
      !_sameDate(story.storyDate, draft.storyDate) ||
      !_sameStringList(story.persons, draft.persons);
}

这个逻辑比"保存后总是重生成"更合理。只改标题或正文时,不需要重新生成;改影响故事语义的字段时,再给用户选择。

删除确认

故事删除必须二次确认。这个项目里删除故事不等于删除原始照片,删除成功后返回列表并刷新缓存。这里的数据边界必须说清楚:故事、照片、活化视频、语音缓存不是同一个东西。

详情页一致性处理

编辑前先保留原始状态

详情页编辑不是简单把表单提交出去。保存前要先拿旧 story 和新 draft 做对比,判断哪些字段真的发生变化。

dart 复制代码
final shouldOfferRegenerate = _shouldOfferRegenerate(story, draft);
final contentChanged = story.content.trim() != draft.content.trim();

contentChanged 决定是否清理旧语音状态,shouldOfferRegenerate 决定是否提示重新生成故事。先比较再保存,页面就能根据变化类型做不同处理。

正文变化要清理语音状态

如果用户编辑了故事正文,旧语音已经不再对应新正文。项目里保存后会检查 contentChanged,然后重置音频状态。这个细节能避免"文字是新的,播放还是旧的"的错位体验。

详情页状态拆分

详情页要同时处理展示和编辑

故事详情页不是单纯展示正文。它还要处理编辑、删除、重新生成、语音播放、视频播放、照片元数据同步等动作。为了避免状态混乱,页面把关键状态拆成多个布尔值:

dart 复制代码
bool _saving = false;
bool _deleting = false;
bool _regenerating = false;
bool _audioLoading = false;
bool _audioLoaded = false;
bool _audioPlaying = false;

这些状态分别对应不同按钮和区域。保存中不应该禁用音频播放,音频加载中也不应该影响删除确认。状态拆得细,页面交互才不会一忙就全灰。

编辑故事后要同步照片元数据

故事里的年代、地点、人物并不只属于故事正文,也会影响地图、照片管理和重新生成逻辑。项目在保存故事后,会继续调用 updatePhotoMetadata 同步照片元数据。

dart 复制代码
final updated = await widget.repository.update(story.id, draft);
await widget.repository.updatePhotoMetadata(
  updated.photoId,
  era: draft.era,
  location: draft.location,
  persons: draft.persons,
);

如果只更新故事,不更新照片,地图页可能仍然读到旧地点,照片列表也可能显示旧人物信息。详情页看起来保存成功了,其他模块却不同步,这就是跨模块数据边界没处理好。

什么时候提示重新生成

不是每次编辑都要重新生成故事。正文改了,就直接保存正文;年代、地点、日期、人物变了,旧正文可能不再匹配新的元数据,这时才需要询问是否重新生成。

dart 复制代码
bool _shouldOfferRegenerate(Story story, StoryEditDraft draft) {
  return story.era.trim() != draft.era.trim() ||
      story.location.trim() != draft.location.trim() ||
      !_sameDate(story.storyDate, draft.storyDate) ||
      !_sameStringList(story.persons, draft.persons);
}

这个判断把"文本编辑"和"业务元数据变化"区分开。用户只是润色一句话时,不会被打断;用户改了地点和人物时,系统才提示重新生成更匹配的故事。

删除必须说清数据边界

删除故事不等于删除照片。项目里删除故事后会从故事列表移除,并清掉故事详情缓存,但原始照片仍然可以继续存在。这个边界要清楚,否则用户会担心删除故事把照片也删掉。

dart 复制代码
Future<void> delete(String id) async {
  await _api.deleteJson('/api/stories/$id');
  final cached = _storyCache;
  if (cached != null) {
    _storyCache = cached.where((item) => item.id != id).toList();
  }
  _storyDetailCache.remove(id);
}

删除动作还需要二次确认。移动端误触概率高,尤其详情页按钮区域靠近底部时,确认弹层能减少不可逆操作。

详情页验证路径

详情页至少覆盖这些操作:

text 复制代码
1. 从故事列表进入详情,先展示缓存内容,再刷新接口数据。
2. 修改正文并保存,返回列表后内容同步更新。
3. 修改地点或人物后出现重新生成提示。
4. 删除故事后返回列表,被删除项不再显示。
5. 正文变化后音频状态被重置,避免播放旧内容。

详情页涉及的数据最多,验证时要跨页面看结果。只在详情页看到保存成功不够,还要返回列表、地图、照片信息确认数据是否一致。

详情操作排查

底部弹层适合承载编辑表单

故事详情页没有跳转到单独编辑页,而是用 showModalBottomSheet 打开编辑表单。这样用户仍然停留在故事上下文里,修改完成后能立即看到结果。

dart 复制代码
final draft = await showModalBottomSheet<StoryEditDraft>(
  context: context,
  isScrollControlled: true,
  backgroundColor: Colors.white,
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(22)),
  ),
  builder: (_) => _StoryEditSheet(story: story),
);

isScrollControlled: true 对移动端表单很重要。键盘弹起后,如果弹层高度不可控,输入框容易被遮挡。编辑正文、地点、人物这类字段时,弹层要能随键盘调整。

保存动作要防止重复提交

_saving 状态不只是显示 loading,还要防止用户连续点击保存。重复提交可能导致两次接口请求交叉返回,页面最后显示旧结果。

dart 复制代码
setState(() => _saving = true);
try {
  final updated = await widget.repository.update(story.id, draft);
  ...
} finally {
  if (mounted) {
    setState(() => _saving = false);
  }
}

项目代码里在成功和失败分支分别收回 _saving,效果一样。重点是请求发出后,按钮状态要进入不可重复提交状态。

重新生成会影响多个区域

重新生成故事后,正文、标题、摘要、语音状态都可能变化。页面不能只替换正文,还要重置与旧正文绑定的资源。

text 复制代码
故事内容变化 -> 更新 Story 缓存
正文变化 -> 清理旧音频状态
照片元数据变化 -> 重新判断是否需要生成

如果正文变了但语音状态没重置,用户可能播放到旧音频。这个问题在内容类应用里很常见,文本和衍生资源要一起维护。

删除后的返回值

详情页删除成功后,需要把"数据已变化"传回上一页。上一页收到结果后刷新列表或使用缓存删除结果。

text 复制代码
详情页删除故事
  -> Repository 删除缓存
  -> Navigator.pop(context, true)
  -> 列表页刷新或移除对应项

这种返回值比让列表页盲目刷新更明确。页面之间不用共享太多状态,只传递"发生了变化"这个事实。

详情页问题定位表

现象 优先检查 对应文件 处理方向
保存后列表仍是旧标题 缓存是否同步更新 stories_repository.dart 更新 _storyCache_storyDetailCache
修改地点后地图不变 照片元数据是否同步 story_detail_page.dart 保存故事后调用 updatePhotoMetadata
重新生成后还播放旧语音 音频状态是否清理 story_detail_page.dart 正文变化后执行 _resetAudioState()
删除后返回列表仍显示 删除结果是否传回 详情页和列表页 删除缓存并通知上一页刷新

详情页牵动的数据很多。只看当前页面保存成功不够,还要确认列表、地图、音频这些相关模块是否跟着更新。

编辑草稿的字段边界

编辑弹层提交的不是完整 Story,而是 StoryEditDraft。这样能避免把只读字段、服务端生成字段一起提交回去。

text 复制代码
可编辑:标题、正文、年代、地点、日期、人物
不可编辑:故事 id、照片 id、创建时间、视频状态、缓存路径

草稿对象越干净,接口越不容易收到无关字段。详情页只提交用户真正能修改的内容,照片和视频这些关联数据通过专门接口维护,边界更清楚。

|

详情页牵动的数据很多。只看当前页面保存成功不够,还要确认列表、地图、音频这些相关模块是否跟着更新。

相关推荐
shaodong11233 小时前
HarmonyOS新特性-沉浸光感在叠叠消小游戏中的落地实践
harmonyos·鸿蒙
xd1855785553 小时前
歌词创作工坊-基于鸿蒙的AI歌词创作应用开发实践
人工智能·华为·harmonyos·鸿蒙
子非鱼94274 小时前
07-Flutter 鸿蒙实战 07:故事集列表与缓存设计
学习·flutter·缓存·华为·harmonyos
程序员黑豆4 小时前
从零开始:创建第一个鸿蒙应用程序
前端·harmonyos
千逐686 小时前
HarmonyOS 实战 | 弹窗/提示框怎么做——Alert、Dialog、Toast 详解
华为·harmonyos·鸿蒙
子非鱼94276 小时前
06-Flutter 鸿蒙实战 06:上传后处理流程,照片识别、故事生成和状态刷新
学习·flutter·华为·harmonyos
Helen_cai7 小时前
HarmonyOS ArkTS 实战:实现一个校园课程表与考勤管理应用
华为·harmonyos
xd1855785557 小时前
藏头诗工坊-基于鸿蒙的AI藏头诗创作应用开发实践
人工智能·学习·华为·harmonyos·鸿蒙
风华圆舞7 小时前
鸿蒙HarmonyOS 多模态输入实战 —— 图片与文档输入智谱/DeepSeek
开发语言·人工智能·华为·php·harmonyos·arkts·arkui