给一个社区类项目做高保真原型时,我在"多选图片上传"这条交互链路上卡了整整一下午。页面本身不难,难的是要把手机端"打开相册—多选勾选—回填预览—删图重选—上传进度"这一整套体验用Axure高保真还原出来。后来我把方案拆成了动态面板加中继器组合拳,顺便趟平了几个中继器的暗坑。这篇就把这套Axure移动端多选图片上传的做法完整拆一遍,包括数据搬运、选中态切换、9张上限约束,以及几个演示时才暴露的问题。
1. 动手前先想清楚:多选上传在高保真里要还原到什么程度
1.1 这条交互链路包含哪些环节
移动端多选图片上传看起来就是"选几张图,传上去",但落到原型里,一条完整链路至少有五段:
- 入口触发:页面上的"添加图片"按钮或上传区域,点击后要调起系统相册或自定义相册面板;
- 相册选择:图片以九宫格形式展示,支持多选,每选一张要给视觉反馈(勾选角标、边框高亮);
- 数量反馈:底部按钮实时显示"完成(N)",达到上限后禁止继续选择并给出提示;
- 结果回填:确定后关闭相册,已选图片以缩略图形式出现在页面上,支持删除;
- 后续动作:如果想更高级一点,可以模拟上传进度、上传成功状态、重新替换等。
做高保真的第一个原则不是"什么都做",而是先分清哪些环节决定产品评审的核心结论,哪些环节只是锦上添花。比如"相册弹层怎么滑入"是观感问题,"选到第九张还能不能继续选"是规则问题,后者比前者重要得多。
1.2 画一条交互红线:哪些细节必须做,哪些可以降级
我在动手前列过一张取舍表,分享出来供参考:
| 环节 | 高保真程度 | 理由 |
|---|---|---|
| 相册弹出/关闭动画 | 做 | 弹层滑动是移动端基础观感,不做显得很"网页" |
| 选中态勾选与边框反馈 | 做 | 多选的核心反馈,评审最关注的点 |
| 完成按钮实时计数 | 做 | 数量逻辑是高保真的灵魂 |
| 9张上限与Toast提示 | 做 | 上限规则是真实需求,必须可演示 |
| 确定后回填预览图 | 做 | 整条链路是否成立的关键 |
| 删除已选图片 | 做 | 增删改查里"删"最容易漏 |
| 上传进度条动画 | 可选 | 动画好看但费时,可先用静态"上传中"状态代替 |
| 长按图片预览大图 | 可选 | 属于交互细节,放在第二版再做 |
这个表不一定要全做。我最后的做法是:核心链路全部做,上传进度用轻模拟的进度条完成,长按预览大图先砍掉。原型的目的不是"以假乱真到每一帧",而是在评审时让产品、设计、开发三方对交互规则有一致的理解。
2. 页面骨架与素材准备:先铺好底子再谈动画
2.1 移动端画布与基础布局
Axure里做移动端原型,第一步先把画布尺寸设好。我习惯用 375x812 这套常见尺寸,页面背景用浅灰色(比如 #F2F3F5),这样白色卡片和图片缩略图能自然浮出来。
页面结构上,我搭了一个最贴近真实场景的"发布动态"页面:
- 顶部导航栏:标题"发布动态",右侧"发布"按钮;
- 内容区:一条说明文字、一个"添加图片"的大按钮(虚线边框,中间一个加号图标);
- 预览区:一个中继器,专门承载已选图片的缩略图网格,初始为空状态,显示"暂无图片"的提示文字。
这里有个细节:预览区不要用固定九个图片框去摆,虽然那也算一种方案,但一旦选中数量动态变化,九个框的显示隐藏会写出一堆变量判断。我这次整个流程都用中继器接管,后面数据搬运会顺很多。
2.2 相册中继器的数据集设计
相册弹层里的图片网格,我用一个中继器(Repeater)来做。先准备十来张素材图,放到项目文件夹下的 assets/img 目录里,命名为 photo_01.jpg 到 photo_12.jpg。
中继器的数据集我设计成四个字段:
字段名: id | name | src | isSelected 第1行: 1 | photo_01 | assets/img/photo_01.jpg | false 第2行: 2 | photo_02 | assets/img/photo_02.jpg | false ...其中 src 是图片路径列,isSelected 是布尔列,用来记录这张图是否处于选中状态。
为什么不把图片直接塞进"图片"类型的列里?两个原因:一是路径列方便后续批量替换素材,我只要在数据集里把"assets/img"改成"assets/img2",所有图片一起换掉;二是路径列配合"每项加载时"事件统一设置图片,维护成本低很多。缺点是如果直接把RP文件发给别人,对方本地没有那个图片目录就会显示空白,所以要么把整个项目文件夹打压缩包,要么在中继器里直接嵌入图片,二选一。
2.3 全局变量和它们的作用域
这个原型里我用了三个全局变量,先列出来:
| 变量名 | 类型 | 作用 |
|---|---|---|
| totalSelect | number | 记录当前已选图片数量 |
| maxNum | number | 上限,默认 9 |
| uploadIndex | number | 上传模拟时记录当前传到了第几张 |
| uploadProgress | number | 上传模拟时的进度百分比 |
Axure的全局变量可以跨页面、跨交互使用,非常适合这种需要多处联动计数的场景。唯一要注意的是变量在页面加载时的初始状态:我一般把 totalSelect 的初始值设为 0,在页面"加载时"再显式设置一遍,避免从别的页面跳转过来出现残留值。
可能有人会问,预览区中继器里已经能通过 visibleItemCount 拿到当前行数了,为什么还需要 totalSelect 这个变量?因为相册弹层里的勾选行为发生在"确定"之前,预览区还没有数据,此时需要用变量单独记录临场选择数量。等确定回填之后,预览区才根据自己的行数显示计数。这两套计数逻辑各管一段,不冲突。
3. 相册选择层:底部弹层的结构与勾选逻辑
3.1 用动态面板搭出一个会滑动的系统相册
移动端相册弹层,我用一个动态面板来承载,整个面板默认隐藏,命名为 PanAlbum。
动态面板内部分成两层:
- 背景遮罩:一个铺满全屏的矩形,颜色黑色,初始透明度 0;
- 底部白色面板:宽度满屏,高度约 420 像素,初始位置在画布底部之外(比如 y=812),里面放标题栏和相册中继器。
调起弹层的交互很简单:点击页面上的"添加图片"按钮,设置为"显示 PanAlbum",同时把底部面板移动到可视区域。关键在动画参数:移动时长 350ms,缓动效果选 ease-out,这样弹层滑入时有一个"先快后慢"的收尾,观感非常接近手机原生交互。背景遮罩的透明度从 0 渐变到 50,同样用 300ms 的渐变。
关掉弹层的逻辑反过来:先移动底部面板回 y=812,同时把遮罩透明度归零,然后等待 350ms 后再隐藏整个动态面板。用"等待"命令延迟隐藏是必须的,否则面板被瞬间隐藏,移动动画根本播不出来。
我在这个环节踩过一个小坑:动态面板隐藏后,再显示时内部元件的状态(比如滚动条位置、透明度)会保持上次的值。所以打开弹层时最好重新把底部面板的坐标、遮罩透明度设一遍,保证每次都是从干净状态开始。
3.2 中继器图片项的选中态切换
相册中继器的每一项,我做成一个正方形卡片,里面放几层元件:
- 背景矩形:圆角 6px,底色白色,用于呈现未选中的卡片;
- 图片元件:铺满整个卡片,用来显示素材图;
- 右上角圆形角标:默认隐藏,选中时显示一个对勾图标;
- 透明热区按钮:覆盖整个卡片,接收点击事件。
关键逻辑都写在中继器的"每项加载时"事件里。这个事件对每一行数据都会执行一次,适合做行内控件的初始化和数据绑定。我在事件里做了两件事:
- 设置图片元件:选择图片,图片来源选"字段值",对应 item.src;
- 根据 item.isSelected 的值控制选中态显示:如果为 true,显示角标、给背景矩形加一圈主题色边框;如果为 false,隐藏角标、边框恢复默认。
这样数据集里 isSelected 是 true 的行加载出来就是选中状态,false 就是未选中状态。视觉层完全由数据驱动,后面更新选中态时只需要改中继器的行数据,不用手工去切换一堆元件的可见性。
点击卡片的交互逻辑写在透明热区按钮上。大致规则是:
如果 item.isSelected == false 且 totalSelect >= maxNum 显示 Toast 提示,结束 否则 更新当前行: isSelected = !item.isSelected 如果这一行从 false 变成 true totalSelect = totalSelect + 1 同步更新''完成(N)''按钮文案 否则 totalSelect = totalSelect - 1 同步更新''完成(N)''按钮文案更新行的字段值时,Axure 支持直接在值里写!item.isSelected做布尔取反。如果你用的版本不支持逻辑非,就拆成两个条件用例:当前行 isSelected 为 true 时设 false,为 false 时设 true。
3.3 数量统计与"完成(N)"按钮
底部"完成"按钮的文案是动态的,用户每勾选一张图,按钮上的数字就要跟着变。实现方式是在勾选逻辑里同时更新按钮文本:
设置文本:"完成([[totalSelect]])"Axure 的双中括号会自动计算表达式,totalSelect 是全局变量,所以文本会显示成"完成(3)"这样的效果。注意按钮控件如果设置了圆角背景,更新文本不会破坏它的样式,放心用。
数量统计还有一个边界情况要注意:当 totalSelect 从 1 变 0 时,按钮要变回"完成(0)"而不是停留在"完成(1)"。这个在减分支里同样更新文本即可,别只更新加分支。
3.4 Toast 提示的自动消失
达到 9 张上限后点击未选中的图片,要弹出一个轻提示"最多选择9张图"。我在原型的右上角放了一个动态面板 Toast,里面一行字,初始隐藏。
处理逻辑用动态面板的"显示时"事件:Toast 显示的同时,等待 2 秒,然后隐藏 Toast。如果用户连续点击多张图片触发多次显示,我一般会在显示前先把 Toast 隐藏一次再重新显示,这样每次提示都是完整的 2 秒,不会出现叠在一起的情况。
要注意,Toast 不能放在相册弹层的动态面板内部,否则弹层关闭时 Toast 跟着被隐藏,提示可能闪一下就没了。我把它放在页面最外层,作为独立动态面板,这样无论在哪个环节触发,它都能稳定弹出。
4. 确定回填:把选中图片"搬"到预览区
4.1 为什么不能直接在两家中继器之间复制数据
选完图片点"完成",下一件事就是把相册中继器里所有 isSelected 为 true 的行,回填到页面预览区的中继器里。这是整条链路最绕的一步。
很多人一开始会直接找"复制行"或者"添加行"命令,想把相册中继器的勾选行整个追加到预览中继器里。很遗憾,Axure 的中继器并不支持这种"数据集对象级"的批量搬运。添加行只能添加你在数据集里定义好的字段值,没法把另一个中继器的行数据作为整体塞进来。
所以必须换一种思路:让每一行数据自己"走"过去。
4.2 利用"每项加载时"做数据搬运
我在相册中继器的"每项加载时"事件里,除了设置图片和选中态之外,再加一条判断:
如果 item.isSelected == true 在''预览中继器''中添加一行 name = item.name src = item.src原理很简单:相册中继器加载每一行时,如果它是一个选中行,就顺手往预览中继器里插入一行相同的数据。这样等相册中继器把 12 行都加载完,预览中继器自然就收集了所有选中的图片。
但这里有个前置条件:中继器的"每项加载时"只在它加载数据时执行。如果相册中继器在页面里已经加载完了,用户再去勾选几行,哪怕行数据变了,也不会重新触发这个事件。也就是说,光在点击卡片时更新 isSelected,预览区是不会自己长出图片的。
4.3 隐藏再显示:强制中继器重跑的真相
要触发"每项加载时"重新执行,我实测下来最稳定的办法是:先隐藏相册中继器,再重新显示它。中继器每次从隐藏变为显示时,都会重新加载数据,于是"每项加载时"里的搬运逻辑会重新跑一遍。
"完成"按钮的完整交互链是这样:
1. 清空预览中继器中的全部行 2. 隐藏相册中继器 3. 显示相册中继器 4. 播放关闭弹层动画第 1 步清空很重要,否则用户第二次选择时,预览区里会堆积之前选过的图片,出现"历史残留"。清空全部行可以用按条件删除:给预览中继器加一个 tempDelete 字段,先把所有行的 tempDelete 更新为 true,然后删除满足 tempDelete == true 的所有行,一行都不会剩。
第 2、3 步就是刚才说的强制重跑技巧。隐藏再显示相册中继器,相册中继器重新加载,每一行执行"每项加载时",选中行自动添加进预览中继器。我把这个逻辑跑通时,第一反应是"原来如此"——Axure 没有现成的 reload 命令,但显示/隐藏就是它的 reload。
4.4 空状态和上限状态的处理
回填完成后,预览区可能有两种非正常情况需要处理。
一是预览区空:用户打开相册但没选任何图片,直接点完成。这种情况下预览中继器没有任何行,页面上的缩略图区域会塌成一块空白。我放了一个"暂无图片"的提示文本,默认显示;当预览中继器的可见行数大于 0 时隐藏。文本的可见性绑定中继器行数:判断预览中继器.visibleItemCount > 0,为真隐藏提示,为假显示提示。这个判断也需要在回填之后手动触发一次,让提示状态同步。
二是达到上限:如果已经选了 9 张,页面预览区应该刚好 9 个格子,此时"添加图片"按钮要不要隐藏?真实产品里通常会把入口隐藏或变灰,原型里我一般保留按钮但点击时弹 Toast"最多选择9张",这样评审时能更直观看到规则约束。
5. 已选图片的增删改:删除按钮、计数与放大预览
5.1 预览中继器与删除按钮的层级关系
预览中继器的每一项,我设计成一张缩略图加右上角一个小删除按钮。这里有个层级问题:如果用一张透明热区按钮覆盖整个图片做点击预览,那删除按钮必须放在热区按钮的上层,否则点击删除按钮时会先被热区接住,事件根本传不到删除按钮上。
Axure 的元件层级就是画布里的前后顺序,把删除按钮放到最上面一层,热区按钮放到下一个层级,图片和背景再往下放,即可解决问题。我习惯把热区按钮做成完全不透明的矩形(透明度设为 0),它只负责接收点击,不影响视觉。
5.2 删除后计数用 visibleItemCount 更省心
预览区每张图片的右上角都有个"×"按钮,点击后要删除这一项。中继器内的事件可以直接操作当前行,所以删除逻辑是"删除当前行"。
删完之后,预览区底部或"发布"按钮旁边要显示当前图片数量,比如 3/9。这里不需要重新维护一个变量,直接用中继器的 visibleItemCount 属性读取行数即可:
设置文本:''[[预览中继器.visibleItemCount]]/9''visibleItemCount 是中继器的内置属性,会随行数增删自动刷新,省去了手动维护变量的麻烦。
还有一个容易被忽略的细节:删掉预览区某张图之后,相册里对应的图片应该同步取消选中状态。否则用户再次打开相册时,会发现删掉的图还带着勾选角标,明显是状态不同步。我用图片路径作为两边的关联键:在删除按钮的交互里加一步更新相册中继器行,条件是src == item.src,把 isSelected 改为 false。这样两边始终保持一致。
5.3 点击图片放大查看大图(可选)
放大预览属于高保真的加分项,我把它放在基础流程做完之后再加。实现方式不复杂:页面放一个全屏动态面板 PanPreview,内部是一个大图展示区,加一个关闭按钮。点击预览中继器里的图片时,设置 PanPreview 里的图片元件为当前行的 src,然后显示 PanPreview。
如果要支持左右切换,需要再加一个当前查看索引变量,配合上一张/下一张按钮更新图片。这部分我建议放到第一版原型之后再做,它不影响主流程的评审结论,但做好之后确实会让原型显得很完整。
6. 评审加分项:上传进度与队列动画的轻模拟
6.1 动态面板进度罩
高保真原型做到"确定回填"已经能过大多数评审,但如果你想在演示现场多打动几个评审,可以再补一段"上传进度"模拟。
我在页面放了一个全屏动态面板 PanUpload,默认隐藏,内部结构包括半透明遮罩、白色圆角卡片、一行"正在上传第 1/3 张"的文本、一条进度条矩形以及一个进度百分比文本。
点击"发布"按钮时做两件事:先把预览中继器的可见行数存到一个全局变量里(比如 totalUpload = 当前行数),然后显示 PanUpload,并从第一张开始跑进度。
6.2 用"文本改变时"实现递归进度条
Axure 没有真正的定时器,要靠"文本改变时"事件玩出循环效果。我在 PanUpload 内部放了一个隐藏文本框 TriggerText,它的初始文本是"start"。然后在它的"文本改变时"事件里写递归逻辑:
如果 uploadProgress < 100 等待 300ms uploadProgress = uploadProgress + 25 设置进度条宽度 = (uploadProgress / 100) * 200 设置进度文本 = [[uploadProgress]]% 设置 TriggerText 文本 = "go_" + uploadProgress 否则 如果 uploadIndex < totalUpload uploadIndex = uploadIndex + 1 uploadProgress = 0 设置进度文本 = 0% 设置上传序号文本 = 正在上传第 [[uploadIndex]]/[[totalUpload]] 张 等待 300ms 设置 TriggerText 文本 = "next_" + uploadIndex 否则 隐藏 PanUpload这里的关键是,Axure 的"文本改变时"只有在文本值发生变化时才会触发。所以每次触发后,必须给 TriggerText 设置一个和当前值不同的新字符串,才能进入下一轮。我在例子里用"go_" + uploadProgress保证了这一点。
进度条宽度的 200 是一个基准值,代表进度条满格时的宽度像素。100% 对应 200px,每涨 25%,宽度增加 50px。你也可以用中继器或者百分比来做,但直接设宽度最简单可控。
6.3 队列切换逻辑和容易踩的边界
这段模拟有两个容易踩的边界:
一是不要忘记重置变量。如果用户第一次上传了 3 张,第二次只上传 2 张,uploadIndex 没重置的话可能直接从第 4 张开始跑,进度逻辑直接就错了。所以每次点"发布"时,除了给 totalUpload 赋新值,还必须把 uploadIndex 和 uploadProgress 都重置为初始值。
二是等待时间的控制。Axure 里的等待是按事件队列执行的,300ms 是串行的,不是并发。所以整个递归过程会严格按照设定的节奏走,不会出现多条进度同时跑的情况。如果你希望整体速度加快,把等待时间从 300ms 改成 150ms 即可,不需要改别的逻辑。
实际上这段进度动画就是"看起来在传",真到开发落地时,前端自然有自己的 upload 组件。但原型里有一个能动的进度条,评审对"上传中状态"的理解会直观很多,这个时间花得值。
7. 常见坑位与收尾经验
7.1 图片路径:本地通、发布不通的老问题
如果你用路径列来存图片地址,在 Windows 环境里最容易碰到反斜杠问题。某些版本在读取路径时对\处理不稳定,图片加载不出来。我的习惯是拿到素材路径后,统一把\替换成/。另外,共享或交付RP包时,对方机器上如果没有 assets 目录,图片同样会挂。要么连文件夹一起打包,要么使用相对路径而不要写死C:/用户/...这种绝对路径。
如果你担心路径问题影响演示效果,可以直接用"图片"类型列把图片嵌入中继器数据集。这样发布RP包时会自动携带图片资源,优点是不会破裂,缺点是批量替换素材时要逐个改。两种方式按团队协作方式来选,没有绝对优劣。
7.2 中继器删除行后的自动重排风险
中继器删除任意一行后,其余行会自动向前补位,所以"第 N 个格子对应哪张图"是一个动态结果。如果你依赖当前行的索引去做一些标记,删除之后可能出现错位。
比如你想在缩略图左上角显示"第 1 张、第 2 张"这样的序号,可以用中继器内置的 item.index 做到,但要意识到删除后这些序号会实时重排。如果你要做的是"删除后保持其他图片状态不变",那用数据本身的 src 做关联键比用索引可靠得多。我在同步相册选中态时,用的就是 src 相等匹配,目的就是避开索引带来的不确定性。
7.3 演示与真机预览的视口差异
原型做完了,演示环境也要提前准备。移动端原型在电脑浏览器里默认按桌面视口展示,整个页面会拉得很宽,弹层的滑动动画、Toast 位置看起来都不对。我一般会在浏览器开发者工具里打开设备模拟,设置成 375x812,再配合触摸模拟演示。
这里顺便提一个演示细节:鼠标的 hover 效果在手机端不存在,所以别用鼠标移入/移出做任何关键交互。需要点击反馈的一律用"鼠标按下时"或"单击时",这样用触屏演示时行为是一致的。
这套方案我后来又复用到好几个移动端项目的原型上,把相册中继器、预览中继器、上传进度这三个组件沉淀成了团队里的内部模板。以后再接"多选图片上传"的需求时,我只需要替换素材图和调整上限数字,整个交互链路的搭建时间能压缩到半小时以内。希望这篇拆解也能帮你少踩几个中继器的坑,把功夫花在真正需要打磨的交互细节上。