用设计语言解读「一起展」App 技术理念与产品哲学的映射

本文基于完整开发过程整理,力求还原每一处产品设计与技术实现的细节,供记忆与理念深挖。

零、技术理念与产品哲学的映射

技术实现 产品/设计理念
「一分钟创建展览」的快速布展 降低策展门槛,让艺术表达平民化
展厅透视+画框材质渲染 线上展览不是简单图片排列,而是空间与审美的还原
视频/音频/图文的多媒体混合 不同内容形态适配不同表达方式
笔记两级浏览(预览→全文) 尊重用户注意力,先吸引再深入
背景音乐与视频的音频焦点管理 细节处的沉浸感打磨
邀约办展机制 平台对优质内容的筛选与背书
实名认证前置 社区治理与内容可信度的基础建设
thumbnailLevel 分级加载 性能与体验的平衡,不因省流量牺牲画质

一、产品定位与架构概览

「一起展」是一款围绕展览、笔记、生活秀构建的内容社区与创作工具型 App。用户既可以浏览他人发布的展览与图文笔记,也可以通过「一分钟创建展览」快速搭建自己的线上展厅,形成从内容消费到内容生产的完整闭环。

核心架构特征:

  • 模块化 Monorepo:采用 apps/host_app 壳工程 + packages/features/* 业务包拆分,严格遵循单向依赖(apps → features → common → core),业务包之间绝对隔离。
  • 状态管理:全面使用 flutter_riverpod(StateNotifierProvider、AsyncNotifierProvider、StateProvider),已完成从 Provider 的迁移。
  • 路由体系:基于 go_router 的统一命名路由,所有跨模块跳转通过 core_router 实现,禁止直接 import 目标页面类文件。
  • 媒体处理:视频使用 video_player,音频使用 just_audio,图片使用 CachedNetworkImage,本地媒体选取使用 image_picker。
  • 数据埋点:集成 Matomo 分析,页面进入、按钮点击、视频播放、发布漏斗等关键行为均带 matomoTitle 上报。

二、启动与认证体系

2.1 启动页(Splash)

  • 纯品牌展示页,作为应用入口;完成后根据登录态决定跳转登录页或首页。

2.2 登录页(LoginPage)

设计亮点:

  • 沉浸式登录氛围:页面顶部使用全宽品牌背景图(assets/icons/login/login_bg.png),下方承载登录表单,视觉层次分明。
  • 双通道登录:支持「手机号 + 短信验证码」与「微信授权登录」两种方式。
  • 手机号输入细节:
    • 左侧固定展示 +86 区号选择器;
    • 输入框底部边框随焦点状态动态变色(聚焦高亮蓝色,失焦灰色);
    • 支持一键清空输入(右侧 clear 图标);
    • 输入限制:仅数字、最多 11 位。
  • 验证码输入细节:
    • 与手机号输入框共享一致的设计语言;
    • 右侧「获取验证码」按钮带倒计时状态(countdown),发送中展示 CircularProgressIndicator;
    • 倒计时期间按钮置灰并显示剩余秒数。
  • 协议同意机制:登录按钮下方放置勾选框,必须勾选《用户协议》与《隐私政策》才能触发登录;文案可点击跳转对应协议页。未勾选时点击登录会弹出 SnackBar 提示。
  • 登录按钮:紫蓝渐变背景(#6A11CB → #2575FC),宽度铺满,圆角 8px;仅当手机号 11 位且验证码 6 位时才可点击。
  • 其他登录方式区:独立区块展示「其他登录方式」文案,居中放置微信图标按钮,点击唤起微信授权。
  • 登录后路由:登录成功优先 pop 返回上一页;若无上一页则 pushReplacementNamed 到首页。
  • 异常处理:捕获 DioException,对 HandshakeException 给出明确的「网络证书校验失败」提示,便于调试。

2.3 绑定手机号(BindPhonePage)

  • 微信登录后若后端返回 NeedBindPhoneException,强制引导至绑定手机号页,完成后方可进入主流程。

2.4 协议页(AgreementPage)

  • 支持《用户协议》与《隐私政策》两种类型,通过 AgreementType 枚举区分,纯文本展示。

三、首页信息流(HomePage)

首页是整个 App 的核心流量入口,采用底部导航 + 顶部 Tab 双层结构。

3.1 底部导航栏(4 大板块)

Tab 说明
首页 信息流主阵地,含 6 个二级频道
办展 展览分类浏览与快速布展入口
邀约 活动/邀请函相关(预留入口)
我的 个人中心与内容管理

设计细节:

  • 底部导航栏图标与文案配合,选中态有明确视觉反馈。
  • 状态栏图标颜色自适应:当位于「视频」频道时,状态栏使用浅色图标(Brightness.light),其余频道使用深色图标(Brightness.dark),避免从视频/看展页返回后状态栏颜色残留导致白底不可见。
  • 页面根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,确保系统 UI 风格与当前内容一致。

3.2 顶部频道 Tab(6 个频道)

推荐、视频、热展、直通车、关注、上海

3.2.1 推荐频道

  • 典型的瀑布流笔记列表,卡片高度不一,形成视觉节奏。
  • 笔记卡片包含:封面图/视频、标题、作者头像与昵称、点赞数。
  • 支持无限滚动加载,带骨架屏占位(AppSkeletonHallGrid)。

3.2.2 视频频道(HomeVideoTabPage)

核心体验设计:

  • 垂直 PageView 全屏视频流:模拟抖音/快手的沉浸式短视频体验,每个视频占满一屏。
  • 自定义滑动物理效果:_ShortDragPageScrollPhysics 允许更短的拖拽距离触发翻页,提升操作跟手性。
  • 视频预加载策略:开始播放约 5 秒后预热下一条视频,优先保证滑动切换时减少黑屏与卡顿。
  • 交互手势:
    • 单击:播放/暂停切换;
    • 双击:触发点赞动画(红心浮起);
    • 长按:触发倍速播放(2x/3x),松手恢复常速。
  • 错误降级:视频加载失败时自动回退到封面图展示,避免黑屏。
  • 埋点:自动上报视频观看时长、播放错误率等事件。

3.2.3 热展频道

  • 展示热门展览卡片,点击进入展览详情页(ExhibitViewingPageTemplate1)。
  • 展览封面解析有专门策略:优先使用 visitFileId 作为海报;若展位首项为图片则海报+首张展位图轮播;若首项为视频则仅展示海报。

3.2.4 直通车 / 关注 / 上海

  • 直通车:平台精选或商业推广内容入口。
  • 关注:展示已关注用户的动态。
  • 上海:基于地域(上海)的内容聚合,体现本地化运营思路。

3.4 搜索入口

  • 首页顶部导航栏右侧带有搜索图标,点击触发 Matomo 埋点(category: 'search', action: 'entry_click', name: 'home_headbar')。
  • 搜索功能当前为入口占位,后续可扩展为全局内容搜索(笔记/展览/用户)。

3.5 首页生命周期与数据一致性

  • didPopNext 自动刷新:首页监听 RouteObserver,当用户从其他页面(如笔记详情、展览页、编辑页)返回时,自动触发当前 Tab 的数据刷新,确保点赞、关注、删除等操作的结果即时同步到列表。
  • 首次加载优化:Tab 切换时首次 invalidate 对应 Provider 触发加载;已加载过的 Tab 不重复请求,除非用户手动下拉刷新。
  • 状态栏动态适配:首页根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 使用 SystemUiOverlayStyle.light,其余 Tab 使用 SystemUiOverlayStyle.dark,避免从视频/看展页返回后状态栏图标颜色残留导致白底不可见。

3.3 笔记创建(HomeCreateNotePage)

设计亮点:

  • 媒体选择互斥:图片与视频不能同时存在,选中一类后另一类入口自动隐藏。
  • 富文本编辑:支持输入标题与正文,正文支持话题提取与插入(#话题# 形式)。
  • 草稿机制:支持保存草稿、加载草稿继续编辑,草稿数量在个人主页展示。
  • 隐私分级:「公开可见」改造为一级隐私选择 + 二级联系人选择抽屉:
    • 公开 / 互关可见 / 私密
    • 部分可见 / 不让谁看:支持搜索联系人、多选、显示已选人数与确认回填。
  • 位置标签:支持添加地理位置。
  • 发布流程:先上传媒体文件,拿到 fileId 后再调用发布接口,失败时保留草稿。

四、办展与展览系统(Exhibit)

这是 App 最具差异化的功能模块,将「线上策展」轻量化,让普通用户也能「一分钟创建展览」。

4.1 办展分类页(ExhibitTabPage)

视觉设计:

  • 顶部有大幅 Banner 背景图,文案「一分钟创建展览!」,右侧 CTA「立即办展」。
  • Banner 与下方内容区有重叠效果(exhibitContentTopOverlap),内容区以圆角(exhibitContentTopRadius)裁切浮起,形成卡片层叠感。
  • 分类导航:横向滚动的一级分类 Tab,带底部指示器(圆角小条);下方有二级/三级分类 Chip 筛选。
  • 邀约角标:部分分类带有「邀约」角标,金橙渐变背景(#FFD09D → #DE8220),圆角不对称设计(左上/右上/右下圆角 6,左下圆角 2),精致且有辨识度。
  • 展馆瀑布流:选中分类后展示对应模板/展馆卡片,支持下拉刷新与上拉加载更多;无数据时展示「暂无展馆」空状态。
  • 列表底部:到达末尾时展示趣味文案「恭喜你发现了世界的尽头!🌈」配分割线,缓解无更多内容的失落感。
  • 实名提醒弹窗:双击 Banner CTA 区域触发「实名提醒」弹窗,引导用户完成实名认证解锁办展功能。

4.2 办展封面页(ExhibitCoverPageTemplate1)

  • 展示单个模板/展馆的封面大图(全景图比例 393:262)、标题、关键词标签(橙色背景 #FF8C00 半透明)、推荐描述。
  • 底部渐变遮罩 +「开始布展」大按钮,深蓝渐变(#11317C → #1B46AA → #224598)。
  • 邀约拦截:若当前模板所属分类标记为 requiresInvitation=true,点击「开始布展」不进入编辑,而是重置底部导航到办展 Tab 并回到首页。

4.3 快速布展(FastExhibitPage)

核心交互设计:

  • 多展厅管理:支持最多 6 个展厅,底部横向缩略图列表展示各厅;默认 1 号厅选中,右侧「新增展厅」按钮带加号叠加;仅允许删除非一号厅。
  • 展位作品管理:
    • 空展位:直接点击唤起媒体选择(图片);
    • 已填充展位:点击弹出蒙层,可选择「更换作品」或「编辑查看」;
    • 长按/批量选择模式:支持多选后批量删除。
  • 展厅切换:底部缩略图点击切换,主舞台实时加载对应展厅图片;空位不再回填默认图,保持真实创作状态。
  • 底部操作栏:居中分布「信息」「音乐」「清空」按钮,清晰易触。
  • 退出确认:返回时若存在未保存内容,弹窗询问「保存为草稿」或「放弃离开」。
  • 状态管理:使用 FastExhibitDraftController(StateNotifier)管理 hallDrafts,每个展厅独立维护 entryLocalPath 与 boothLocalPaths。

4.4 发布页(FastExhibitPublishPage)

  • 全景预览:上方展示当前选中展厅的透视效果,展位按模板 boothList.length 动态渲染(不再固定 5 个图位),支持画框材质、阴影、厚度。
  • 展厅缩略图切换:底部横向缩略图可切换预览不同展厅。
  • 表单信息:填写展览名称、推荐描述、选择定位、隐私设置。
  • 提交逻辑:
    • 先调用 saveOrUpdate 创建展览(esQuantity = boothList.length * hallCount,状态草稿=1/发布=2);
    • 拿到 id 后二次调用同接口编辑提交 id/templateName/recommendDesc/fileId/isType/scopeUserIds/boothList。
  • 发布后路由:发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab。
  • 动态展位布局:exhibit_wall_artworks_perspective.dart 支持动态展位布局与可选 frameDataList/frameMaterialUrls,可渲染边框、阴影、厚度与材质。

4.5 展览浏览页(ExhibitViewingPageTemplate1)

沉浸式看展体验:

  • 展厅信息卡:外框自上而下渐变并圆角 24,策展人姓名仅加粗(无蓝色下划线),去除花哨样式保持阅读专注。
  • 标题滚动动画:超长标题从右往左单向循环滚动,吸引注意力又不干扰整体布局。
  • 展厅分布:去掉伸缩头,仅保留更小缩略图横向列表,选中高亮且隐藏「x号厅」字样,减少信息噪音。
  • 主视觉区:图片/视频轮播,视频自动播放,图片支持手动滑动。
  • 画框与材质:每个展位作品根据 ExhibitBoothFrameData 渲染真实画框效果,含材质贴图、装饰垫颜色、厚度阴影。
  • 音乐播放:展厅支持背景音乐,右上角音乐按钮实底设计提高清晰度;进入作品详情或放大预览时自动暂停,返回后恢复,状态严格同步避免失步。
  • 弹幕:支持弹幕开关;关闭时以固定高度深色占位(_DanmakuAreaPlaceholder)避免背景发浅突兀。
  • 底部操作栏:左到右渐变背景,点赞/评论/收藏图标使用定制 SVG(icon-exhibit-like.svg、icon-exhibit-comment.svg);点赞激活态为红色,收藏激活态为金色,视觉反馈鲜明。
  • 交互防遮挡:自动预览蒙层限制 bottom 为 MediaQuery.padding.bottom + 64,避免拦截底部操作栏的点击。
  • 点赞状态持久化:通过 Riverpod noteEntityProvider 维护展位点赞状态,离开页面再进入不丢失。

4.6 作品详情页(ExhibitArtworkDetailPageTemplate1)

  • 主舞台:在可见区域中垂直居中展示作品,带画框材质渲染;图片未加载时保留最小占位约束,加载完成后释放以自适应宽高比。
  • 视频作品:fileType=2 时主舞台缩略预览使用视频播放器(thumbnailLevel=3),支持播放控制;点击「查看超清」进入全屏视频播放器(thumbnailLevel=0 原视频)。
  • 横向滑动切换:左右滑动手势切换同一展厅内其他作品,过滤掉 fileId 为空的展位,仅在有实质内容的展位间跳转。
  • 底部缩略图轮播:展厅内多作品时,底部展示缩略图条,点击或滑动切换。
  • 作品信息弹窗:点击「信息」按钮从底部弹出 AppModalSheet,毛玻璃效果(blurSigma: 10),展示作品名称、作者、推荐描述。
  • 互动操作:点赞、评论、分享;点赞状态实时同步到全局实体仓库。

4.7 展览详情页模板(ExhibitDetailPageTemplate1)

另一种展览浏览形态,更偏向「展览介绍 + 作品概览」:

  • 背景沉浸:全屏背景图 + 60% 黑色遮罩,营造展厅暗环境氛围。
  • 顶部导航栏:白色返回/分享图标,居中展览标题。
  • 主展品轮播:PageView.builder 横向滑动,含 10 张展品图,viewportFraction: 0.9 让两侧露出下一张边缘;切换时触发 AnimationController 缩放动效,增强视觉反馈。
  • 缩略图导航:底部横向缩略图列表,选中项放大(45px)并带白色边框(2px),未选中项缩小(35px),点击可跳转对应页面。
  • 底部功能栏:5 个功能按钮(详情、播放、VR、全屏、分享),白色半透明玻璃质感背景(alpha: 0.12,边框 alpha: 0.6),圆角 6px。
  • 详情弹窗:点击「详情」唤起 DraggableScrollableSheet,初始高度 50%,最小 30%,最大 90%;
    • 顶部白色圆角面板,带系统指示条(灰色短横线);
    • 展厅介绍长文本,首行缩进 18px,段落间距 12px,两端对齐;
    • 底部浮起「进入展厅」按钮,橙色渐变(#FF9022)+ 自定义 ClipPath 弧形裁切,两侧箭头图标引导点击。

4.9 多种展览详情模板(Type 1 / 2 / 3)

产品意图:为不同展览类型提供差异化的浏览体验,体现「一展一风格」的策展理念。

Type 1(智慧卡片):

  • 暖米色背景(#FFFFFAF0),横向 PageView 展示 GIF 动图卡片,viewportFraction: 0.85;
  • 卡片带缩放动效(非当前页缩小至 0.8),营造立体纵深感;
  • 底部 BackdropFilter 毛玻璃控制栏,含缩略图导航(选中项带橙色边框 #FF8F1F)与功能按钮(标签/播放/VR/详情/全屏/分享);
  • 卡片底部叠加毛玻璃文字层,展示作品名称与作者。

Type 2(雕塑展览):

  • 类似 Type 1 的横向轮播,但每张卡片展示雕塑作品大图 + 标题 + 详细描述;
  • 更适合静态艺术品的深度介绍。

Type 3(画廊模式):

  • 全屏背景图随当前选中作品实时切换,extendBodyBehindAppBar: true 让背景延伸至状态栏;
  • BackdropFilter 高斯模糊背景图作为氛围层;
  • 底部横向缩略图条,选中高亮。

3D 展厅(Exhibition3dPage):

  • 当前为占位页(”3D view will be implemented here”),预留 WebGL/3D 引擎接入能力,未来可支持 360° 虚拟展厅漫游。

4.8 评论页(ExhibitArtworkCommentPageTemplate1)

  • 评论结构:支持一级评论与二级回复,嵌套展示;子评论默认展示前 2 条,超出时「查看全部 x 条评论」可展开。
  • 交互细节:
    • 每条评论展示头像、昵称、内容、时间;
    • 支持回复、点赞、删除(仅自己的评论);
    • 评论点赞数实时更新;
    • 时间显示智能格式化(刚刚、x分钟前、x小时前、x天前)。
  • 输入框:底部悬浮输入栏,点击唤起全屏键盘输入 Sheet,带圆角与阴影;支持回复指定用户(hintText: '回复:$replyToNickName')。
  • 空状态:无评论时展示「暂无评论,快来抢沙发吧」。
  • 加载更多:列表滚动到底部 80px 内自动加载更多。

五、笔记内容生态(Note)

5.1 笔记详情页(NoteDetailPage)

两级浏览架构:

  • 一级预览:默认进入 startInPreview=true,展示笔记媒体(图片轮播/视频)+ 基本信息;支持左右滑动浏览图片,双击放大。
  • 二级文本页:点击「查看全文」进入完整文本页;拦截系统返回与顶部返回,先回一级预览,再返回列表,保证「一级→二级→全屏」与「全屏→二级→一级」的导航链完整。
  • 媒体播放:
    • 图片使用 CachedNetworkImage,笔记详情轮播图使用缩略图级别 9;
    • 视频使用 VideoPlayerController,支持点击播放/暂停、双击进入全屏、长按拖动进度条(scrub);
    • 首次播放提示「双击可放大播放」,2 秒后自动消失。
  • 背景音乐:笔记详情增加 musicFileId(取 data.videoFileId),详情页自动播放背景音乐并支持右上角按钮播放/暂停;使用 GlobalMusicService 统一音频焦点管理,视频播放时自动暂停背景音乐,视频结束/离开恢复。
  • 评论:底部评论区支持点赞、回复、删除;支持下拉刷新。
  • 生命周期管理:页面 dispose 前暂停所有视频,防止返回上一页后视频继续播放;didPushNext/didPopNext/AppLifecycleState 均做媒体暂停/恢复处理。

5.2 笔记评论系统(NoteCommentSection / NoteCommentController)

架构设计:

  • 使用 FamilyAsyncNotifier<NoteCommentState, int>,以 noteId 为 family key,每个笔记的评论数据独立管理。
  • 评论结构:支持一级评论与二级回复,嵌套展示;replyShowCount 默认 10,UI 层可对超过 3 条的子评论做折叠处理。
  • 分页加载:首次加载 page 1,滚动到底部自动 loadMore();isLoadingMore 状态控制加载指示器,避免重复请求;到达末尾展示「没有更多评论了」。
  • 评论操作:
    • 发表评论:支持一级评论(parentId = 0)与回复指定用户(parentId = commentId, replyToUserId = targetUserId);发表成功后自动 refresh() 并同步评论数到全局实体仓库(noteInteractionControllerProvider.updateCommentCount)。
    • 删除评论:递归遍历评论树找到目标评论并移除(removeFromList),同步更新总数;删除后同步全局仓库。
    • 点赞/取消点赞(乐观更新):先本地翻转 isLiked 与 likeCount 更新 UI,再调用接口;若接口失败则回滚到旧状态,保证用户操作即时反馈且数据一致性。
  • UI 组件:NoteCommentItem 展示头像、昵称、内容、时间、点赞数;支持点击回复、点赞、删除(仅自己的评论)。
  • 空状态:「暂无评论,快来抢沙发吧」。

5.3 图片全屏浏览(NoteImageGalleryPage)

  • 支持多图左右滑动查看,全屏黑色背景,沉浸体验。

六、音频展览(Audio Exhibit)

6.1 音频展览发布(AudioExhibitPublishPage)

  • 海报上传区:虚线边框(DashedBorder)设计,点击上传海报图片;上传后支持替换。
  • 音视频上传:支持选择音频或视频文件;视频上传后展示预览,含播放/暂停与进度条。
  • 富文本表单:标题、话题标签、简介、脚本;话题支持 # 提取。
  • 底部发布栏:固定在底部,「发布」按钮带渐变背景。

6.2 音频展览播放(AudioExhibitViewPage)

  • 沉浸式播放器设计:
    • 背景:基于封面图的 BackdropFilter 高斯模糊(sigmaX: 30, sigmaY: 30),营造氛围感;
    • 中央:圆角封面卡片(ClipRRect + BoxShadow),悬浮于模糊背景之上;
    • 信息卡:标题、艺术家名、关注按钮;
    • 底部:播放进度条、播放/暂停按钮、评论与歌单按钮。
  • 状态管理:使用 GlobalMusicHandle 管理音频播放状态,支持全局暂停/恢复。

七、个人主页与社交(MyPage / UserProfilePage)

7.1 个人主页结构

  • 顶部 Header:全宽背景图 + BackdropFilter 毛玻璃效果(sigmaX: 30, sigmaY: 30)+ 15% 黑色遮罩;内容区展示头像、昵称、ID、个性签名、关注/粉丝/获赞/收藏数据。
  • 头像预览:点击头像进入全屏 PhotoView,支持双指缩放。
  • Tab 导航(5 个 Tab):笔记、赞过、收藏、展览、生活秀。
  • 瀑布流展示:笔记采用 MasonryGridView 双列瀑布流;展览/生活秀采用固定比例网格(177:181)。
  • 数据懒加载:Tab 切换时首次加载对应数据,滚动到底部 200px 内自动加载更多。
  • 刷新机制:下拉刷新联动当前 Tab 与用户信息;didPopNext 与 AppLifecycleState.resumed 自动刷新用户数据。
  • 展览封面轮播:个人主页展览卡片若有多张图,自动 3 秒轮播,带动画过渡(animateToPage, 600ms, easeInOut)。
  • 社交互动:支持关注/取消关注、私信入口(聊天功能开发中占位)。

7.2 编辑资料(HomeEditProfilePage)

  • 头像上传:点击头像唤起图片选择,支持拍照/相册;右下角悬浮相机图标(白色圆形+阴影)。
  • 昵称与个性签名输入:表单分组设计,标签 + 圆角输入框(borderRadius: 14);签名区最小高度 110,多行输入。
  • 保存按钮:主色背景,带加载态(CircularProgressIndicator)。

7.3 生活秀(Life Show)

产品定位:生活秀是展览的一种轻量表达形态,用户可以将日常创作、活动记录以展览形式呈现,降低正式策展的仪式感门槛。

展示形态:

  • 列表卡片(LifeShowList):纵向列表,每项为圆角 12px 白色卡片,带轻微阴影(0x0A000000)。
    • 顶部封面图(高度 180,铺满圆角裁切),加载失败展示灰色占位图标;
    • 内容区:红色标签(special,如「热门」「精选」)+ 地理位置文案;
    • 展览名称(单行截断,16px 加粗);
    • 推荐描述(两行截断,浅灰色);
    • 底部作者行:作者头像(32px 圆形)+ 昵称 + 点赞/评论/收藏统计图标;
    • 展期信息(startTime - overTime)。
  • 网格卡片(MyLifeShowGrid):个人主页「生活秀」Tab 采用双列网格(childAspectRatio: 177/181)。
    • 封面图铺满,底部渐变遮罩(#00000000 → #99000000)承载标题与位置;
    • 右上角红色角标(special);
    • 右下角悬浮统计胶囊(半透明黑底圆角 10,点赞/评论数)。
  • 数据模型:LifeShowItem 包含完整的展览字段(boothList、keywords、author、likeCount、isLiked 等),支持点赞、收藏、评论的完整交互。

7.4 草稿箱(HomeDraftListPage)

双 Tab 结构:笔记草稿 + 展览草稿

笔记草稿:

  • 使用 _MyNotesGrid 网格展示(与「笔记」Tab 一致),支持点击继续编辑;
  • 点击后进入 HomeCreateNotePage(editNoteId: noteId),加载原有标题、正文、媒体、隐私设置;
  • 编辑返回后自动刷新草稿列表并同步草稿数量。
  • 分页加载:滚动到底部 180px 内自动加载更多。

展览草稿:

  • 懒加载策略:首次切换到「展览」Tab 时才触发数据拉取,减少首屏开销。
  • 展示形式:双列网格(177:181),与已发布展览卡片视觉一致。
  • 继续布展:点击草稿卡片后:
    1. 调用 getExhibitionDetail(id) 拉取完整展览详情;
    2. 通过 fastExhibitDraftProvider.notifier.loadFromExhibition(detail) 将草稿数据回填到快布展状态;
    3. 导航至 FastExhibitPage,用户可在原有基础上继续编辑、增删展厅/展位、更换作品;
    4. 返回草稿箱后自动刷新列表。
  • 空状态:「暂无展览草稿,保存展览草稿后会显示在这里」。

八、设置与系统

8.1 设置页(SettingsPage)

  • 卡片式设置列表:圆角 12px 白色卡片,分组展示「个人信息」「当前版本」「关于一起展」。
  • 注销账号:独立卡片,带警示文案「注销后账号无法恢复,请谨慎操作」。
  • 退出登录:底部全宽按钮,点击后清理 Token、上报 Matomo 埋点、跳转登录页并清空路由栈。

8.2 个人信息页(PersonalInfoPage)

  • 头像展示区:114px 大圆形头像,使用 CachedNetworkImage 加载;右下角悬浮相机图标(28px 圆形白底),提示可编辑;点击跳转「编辑资料」页。
  • 信息列表:白色卡片圆角 12px,展示「昵称」与「简介」两项,每项带标签(浅灰色 14px)+ 值(深灰色 14px)+ 右侧箭头;未设置时显示「未设置」占位文案。
  • 跳转逻辑:点击昵称或简介均跳转 HomeEditProfilePage,用户可在同一页面完成两项编辑。

8.3 关于页(AboutPage)

  • 品牌区:顶部居中展示 App Logo(108px 圆角方形,assets/logo.jpg)+ 应用名称「一起展」(18px 加粗)。
  • 版本信息:圆角 12px 白色卡片,展示「当前版本」与「版本更新」;版本更新项带深蓝色标签(#1C2F5D)展示最新版本号,点击提示「已是最新版本」。
  • 法律合规:独立白色卡片展示「用户协议」「隐私政策」,点击唤起对应协议页。
  • 底部版权:页脚展示「@ 2025-2026 上海艾魅之文化艺术有限公司. All Rights Reserved」+ ICP 备案号「沪ICP备2025146963号-7A」+ 版本号,字体 11px 灰色,体现平台合规性。

8.4 账号注销页(AccountDeletePage)

  • 警示性页面,引导用户了解注销后果,确认后执行账号注销流程。

九、扫码功能(QrScanPage)

  • 使用 mobile_scanner 实现二维码扫描。
  • 权限处理:首次进入检查相机权限,被拒绝时展示「需要摄像头权限」引导页,提供「重试」与「去设置」两个选项。
  • 扫描界面:黑色背景,中央挖空矩形框(PathFillType.evenOdd 实现遮罩),白色边框 + 圆角 16;顶部 AppBar 支持手电筒开关与前后摄像头切换。
  • 结果处理:扫码结果若是 URL 则尝试外部浏览器打开;否则弹窗展示文本内容;处理完成后自动关闭扫描页。

十、设计与交互细节汇总

10.1 视觉一致性

  • 主题系统:所有颜色、文字样式、间距均沉淀在 design_system 包的 AppColors、AppTextStyles、AppDimens 中,严禁使用魔法数字。
  • 渐变运用:登录按钮紫蓝渐变、发布按钮深蓝渐变、底部栏渐变、展厅信息卡渐变——渐变方向与色值经过精确计算,强化品牌质感。
  • 圆角体系:不同场景使用不同圆角半径(小至 2px 的 Chip,大至 24px 的展厅卡,999px 的圆形头像),层次分明。

10.2 动效与微交互

  • 点赞动画:双击视频/作品时红心浮起并淡出。
  • 标题滚动:展览页超长标题单向循环滚动,避免截断又不占用额外空间。
  • 状态切换过渡:Tab 指示器、Chip 选中态、按钮态均有过渡动画(AnimatedOpacity、AnimatedContainer)。
  • 图片加载占位:统一使用 AppImageLoadingPlaceholder,避免布局跳动。

10.3 性能优化

  • 图片分级加载:thumbnailLevel 机制(0=原图/视频,1=大图,2=中图,3=小图,9=笔记轮播缩略图),不同场景按需加载不同尺寸。
  • 视频预加载:视频频道提前预热下一条视频控制器。
  • Riverpod 状态共享:点赞、收藏等交互状态通过全局 noteEntityProvider 共享,避免页面间状态不同步。
  • RepaintBoundary:复杂独立图形区域使用 RepaintBoundary 减少不必要的重绘。

10.4 安全与隐私

  • 协议前置:登录、注册必须明确同意用户协议与隐私政策。
  • 隐私分级:笔记发布支持「部分可见/不让谁看」的细粒度权限控制,联系人选择支持搜索与多选。
  • 实名认证:办展功能需完成实名认证,未认证用户点击办展触发弹窗引导。

10.5 异常与降级

  • 视频加载失败:降级为封面图展示。
  • 网络证书错误:登录/验证码场景对 HandshakeException 给出明确中文提示。
  • 空状态:所有列表均配备空状态占位(AppEmptyPlaceholder),带图标与引导文案。
  • 认证过期:个人主页监听 AuthExpiredException,弹窗引导重新登录,登录成功后自动刷新全部数据。

十二、全局实体状态管理与跨页面同步

问题背景:在传统 Flutter 开发中,跨页面的点赞、收藏、关注状态很容易因各自维护独立状态而失步。用户在一个页面点赞后,回到列表页或进入另一个页面,状态可能回退到旧值。

「一起展」的解决方案:

  • 全局实体仓库:在 shared_models 中定义 noteEntityProvider(StateProvider.family<NoteEntity?, int>)与 exhibitionEntityProvider,以笔记/展览 ID 为 family key,任何页面均可通过同一 Provider 读写同一实体的状态。
  • 批量同步机制:batchUpsertNoteEntities(ref, entities) 与 batchUpsertExhibitionEntities(ref, entities) 在列表页收到接口返回后,一次性将所有条目注入全局仓库;详情页、交互控制器则从同一仓库读取最新状态。
  • 交互控制器架构:
    • noteInteractionControllerProvider:封装点赞/取消点赞逻辑,成功后更新全局 noteEntityProvider 中对应实体的 isLiked 与 likeCount;
    • exhibitionInteractionControllerProvider:同理处理展览的点赞与收藏。
  • 状态持久化效果:用户在看展页点赞某作品 → 退出看展页 → 再次进入同一展览,点赞状态保持红色高亮,不会回退到初始值。
  • 跨页面即时同步:用户在笔记详情页点赞 → 返回首页瀑布流 → 列表中该笔记的点赞数与红心状态已同步更新(因首页 didPopNext 刷新拉取最新数据,同时全局仓库保证了无刷新时的状态一致性)。

十三、音频焦点与全局媒体协调

问题背景:移动设备同时只能有一个音频焦点。当用户在看展页播放背景音乐,又切换到笔记详情页观看视频,或者进入音频展览页播放语音导览时,必须有一套协调机制防止多个音源互相抢占、混乱播放。

「一起展」的协调策略:

  • GlobalMusicService:基于 just_audio 封装的全局音乐服务,所有非视频类音频(展厅背景音乐、音频展览)统一由此服务播放。
  • MediaPlaybackCoordinator:视频播放器(video_player)在初始化/播放前向协调器注册;协调器确保同一时刻只有一个视频在播放;新视频开始播放时,自动暂停其他视频与背景音乐。
  • 音频焦点抢占与恢复:
    • 视频播放 → 自动暂停 GlobalMusicService;
    • 视频结束/离开视频页 → 若当前页有背景音乐需求,自动恢复播放;
    • 展厅背景音乐开启 → 视频预加载但不自动播放,等待用户手动点击后才抢占焦点。
  • 展厅内音乐状态精细管理:
    • 进入作品详情页或放大预览 → _pauseMusicTemporarily() 暂停音乐,同时更新 _currentMusicPlaying = false;
    • 返回展厅 → _resumeMusicTemporarily() 恢复音乐,同时更新 _currentMusicPlaying = true;
    • 避免因状态缓存过期导致音乐按钮点击无效(_scheduleSyncMusic 中每次校验 _musicHandle.isPlaying,而非依赖过期的本地布尔值)。
  • Android 音频焦点不中断:VideoPlayerController 增加 VideoPlayerOptions(mixWithOthers: true),避免 Android 系统强制抢占音频焦点导致背景乐被系统级中断后无法恢复。

十四、展览封面解析策略

封装位置:lib/features/exhibit/domain/exhibit_cover_resolver.dart

解析规则(体现对多媒体混合场景的细致处理):

  • 传入 files 列表(含 fileId + fileType)。
  • 首项为视频(fileType==2):仅使用视频封面(thumbnailLevel=2),不再额外拼接图片,避免视频封面与静态图风格不一致造成视觉跳跃。
  • 首项为图片(fileType==1):提取前 2 张图片(自动跳过视频),用于封面轮播;若不足 2 张则有几张展示几张。
  • 海报优先:优先使用 visitFileId 作为海报;若展位首项为图片,则海报 + 首张展位图轮播;若首项为视频,则仅展示海报。
  • 接入点:已统一接入 ExhibitRepository 的展览列表、我的展览列表、模板列表组装逻辑,确保全 App 封面展示规则一致。

十五、一些设计与优化

优化项 问题 解决方案
状态栏自适应 从视频/看展页返回首页后,状态栏图标颜色残留,导致在白底上不可见 首页根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 强制 light,其余强制 dark
展厅分组规则 旧逻辑要求 boothNo 必须完整覆盖 1..boothCount,导致合法但不连续的展位无法渲染 放宽为:只要存在合法 boothNo 即渲染;fileId 为空的展位占位显示;仅当所有 booth fileId 为空时整厅不渲染
交互按钮不可点 自动预览蒙层 Positioned.fill + GestureDetector(opaque) 覆盖了底部点赞/评论/收藏栏 限制蒙层 bottom: MediaQuery.padding.bottom + 64,仅覆盖内容区,避开底部操作栏
音乐按钮状态失步 缓存的 _currentMusicPlaying 布尔值与真实音频播放状态不一致,导致点击无效 每次同步时以 _musicHandle.isPlaying 为准;临时暂停/恢复时同步更新本地状态
点赞状态回退 离开展厅再进入,点赞状态回退到初始值 全部迁移到 Riverpod 全局实体仓库,以真实 ID 为键持久化交互状态
视频预加载 滑动切换视频时频繁出现黑屏/卡顿 开始播放约 5 秒后预热下一条视频的 VideoPlayerController,优先保证滑动流畅
动态展位布局 发布页固定 5 个图位,无法适配不同模板的展位数量差异 按模板 boothList.length 动态生成展位,提交时也按实际展位数提交 boothList
封面解析一致性 不同页面展览封面展示规则不一,视频封面与图片混排时视觉混乱 统一封装 exhibit_cover_resolver,按 fileType 优先级提取,全 App 复用同一策略
展厅信息卡设计 策展人姓名蓝色下划线过于花哨,干扰阅读;音乐按钮透明层模糊不清晰 策展人姓名仅加粗无下划线;音乐按钮改为实底设计,提高可识别度
发布成功后路由 发布成功或保存草稿后,用户迷失在布展流程中,不知道回到了哪里 发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab,明确告知用户内容归属

十六、路由体系(AppRoutes / AppRouter)

架构原则:壳工程 host_app 集中注册所有路由,各 feature_* 包仅依赖 core_router 的抽象,禁止互相 import 其他 feature 的 Page 类。

已注册路由清单:

路由名 页面 关键参数
splash SplashPage —
login LoginPage —
bindPhone BindPhonePage —
agreement AgreementPage type(AgreementType)
home HomePage —
homeCreateNote HomeCreateNotePage —
homeDrafts HomeDraftListPage initialTabIndex(0=笔记,1=展览)
noteDetail NoteDetailPage noteId, startInPreview, enableBackToPreviewWhenTextMode
audioExhibitPublish AudioExhibitPublishPage —
audioExhibitView AudioExhibitViewPage coverUrl, title, artist
fastExhibit FastExhibitPage —
exhibitTab ExhibitTabPage —
exhibitCoverTemplate1 ExhibitCoverPageTemplate1 hall(ExhibitHall)
exhibitDetailTemplate1 ExhibitDetailPageTemplate1 —
exhibitDetailType1/2/3 多种展览详情模板 —
exhibitViewingTemplate1 ExhibitViewingPageTemplate1 title, exhibitionId, initialDetail
exhibitArtworkDetailTemplate1 ExhibitArtworkDetailPageTemplate1 作品相关全量参数(imageUrl, visitFileId, artworkAuthor 等)
exhibitArtworkCommentTemplate1 ExhibitArtworkCommentPageTemplate1 title, commentCount, exhibitionId, boothId
exhibition3d Exhibition3dPage —
userProfile UserProfilePage userId
mySettings SettingsPage —
myPersonalInfo PersonalInfoPage —
myAbout AboutPage —
myAccountDelete AccountDeletePage —

参数传递规范:

  • 所有参数统一通过 Map<String, Object?> 传递;
  • _args() 辅助函数自动转换参数类型,兼容 Map 与 Map<String, Object?>;
  • 每个路由构造器负责从 args 中提取字段并赋予默认值,避免空值崩溃。

十七、设计系统组件(Design System)

packages/common/design_system 沉淀了全 App 复用的视觉原子与分子组件,确保跨 feature 体验一致性。

17.1 主题 Token

  • AppColors:全部颜色常量(textPrimary, textSecondary, textTertiary, buttonPrimary, link, inputBorder, surface, backgroundSecondary 等),无魔法数字。
  • AppTextStyles:按字号/字重/行高分级(loginTitleText28, body12, body14, body16, fastExhibitTitle, button16 等),保证全 App 字体层级统一。
  • AppDimens:间距、圆角、高度等维度常量(pageHorizontalPadding, exhibitBannerHeight, exhibitContentTopOverlap, inputRadius 等)。

17.2 通用组件

  • AppEmptyPlaceholder:空状态占位,支持自定义标题、副标题、图标;全 App 列表空状态统一使用。
  • AppSkeletonHallGrid:骨架屏网格,用于展览/笔记列表首次加载时的占位,减少白屏焦虑。
  • AppImageLoadingPlaceholder:图片加载中占位,统一灰色背景 + 加载图标,避免布局跳动。
  • AppModalSheet:底部弹窗封装,支持 blurSigma 毛玻璃效果、圆角、自定义内容区;用于作品信息、筛选、分享等场景。
  • AppTopToast:顶部轻提示,用于操作成功/失败的非阻塞反馈(如「已是最新版本」「加载更多失败」)。

17.3 表单与输入

  • DashedBorder:虚线边框容器,用于海报/封面上传区的「点击上传」视觉引导。
  • AppMediaPicker:统一封装图片/视频/音频选择器,支持拍照、相册、文件选择,供所有 feature 复用。

十八、埋点与分析(Matomo)

集成方式:core_analytics 包封装 Matomo 上报,提供 AppAnalyticsService.instance.trackEvent() 与 trackPageView()。

埋点覆盖场景:

  • 页面浏览:所有路由跳转均携带 matomoTitle(如「首页-推荐」「办展分类」「笔记详情」),用于漏斗分析。
  • 用户行为:登录/登出、点赞/取消点赞、收藏、评论、分享、发布笔记/展览、保存草稿。
  • 视频播放:视频频道自动上报播放时长、播放错误率、播放完成率。
  • 布展漏斗:进入办展分类 → 点击模板 → 开始布展 → 填写信息 → 发布成功,全流程追踪转化率。
  • 认证过期:AuthExpiredException 触发时单独上报,用于监控登录态稳定性。

隐私合规:

  • 用户登出时调用 clearVisitorUser(),清除 Matomo 访客标识;
  • 所有事件 category/action/name 均使用英文枚举,便于国际化团队理解。

十九、分享系统(AppShareSheet / AppShareService)

产品定位:分享是内容社区的核心传播链路,「一起展」为笔记、展览、作品、个人主页均提供了统一的分享能力。

UI 组件(AppShareSheet):

  • 底部弹窗:showModalBottomSheet 实现,顶部圆角 16px,毛玻璃背景(BackdropFilter, sigmaX: 16, sigmaY: 16),高度固定 254px;
  • 标题栏:左侧「分享」标题(20px 加粗),右侧圆形关闭按钮(28px,浅灰背景 + 深色关闭图标);
  • 分享渠道:横向滑动排列的渠道图标:
    • 微信:54px SVG 图标 + 13px 标签;
    • 朋友圈:同上;
    • 复制链接:54px 圆形灰底图标 + 链接符号;
    • 未启用的渠道(私信/微博/QQ 空间)置灰处理(opacity: 0.4),点击提示「该分享渠道暂未开放」。
  • 渠道状态管理:通过 enabledChannels 参数灵活控制哪些渠道可用,不同场景可差异化配置。

分享服务(AppShareService):

  • 注册机制:registerGlobalHandler() 在 App 启动时注入全局分享处理器,实现 feature 包与具体分享 SDK 的解耦;
  • 微信分享:基于 wechat_kit 实现,支持分享到微信会话(kSession)和朋友圈(kTimeline);
  • 链接复制:调用 Clipboard.setData 将分享链接复制到剪贴板,成功后提示「链接已复制」;

分享链接构建(ShareUrlBuilder):

  • 统一域名:https://app.amaz-cn.com/app/share/,便于 Deeplink 解析与追踪;
  • 支持类型:笔记详情、展览观看、作品详情、用户主页;
  • 参数结构:?type=xxx&noteId=xxx&exhibitionId=xxx&boothId=xxx&userId=xxx;
  • 解析能力:ShareUrlBuilder.parse(Uri) 可反向解析分享链接,用于 Deeplink 跳转(如从微信 H5 唤醒 App 并直达内容页)。

使用场景:

  • 笔记详情页底部栏 → 分享笔记;
  • 展览浏览页 → 分享展览;
  • 作品详情页 → 分享单幅作品;
  • 个人主页 → 分享用户主页。

注:本文档基于代码遍历整理,后续可根据实际产品迭代持续补充完善。

分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理