job-uniapp 职位详情页:富文本渲染、投递状态机、分享海报三个硬骨头
详情页看着就一张卡片的活,真做起来三个坑一个不少:企业介绍是后端吐的富文本 HTML,直接塞进 rich-text 会带一堆样式乱掉;投递按钮的状态一变再变;分享海报要用 canvas 画出来保存到相册。job-uniapp 这三块都趟平了,逐个说。
富文本:先把脏 HTML 洗一遍再渲染
后端给的是content字段,长这样:福利。直接rich-text渲染问题不大,但遇到http:省略协议头、style 里带position:fixed的奇葩样式就会出事。统一走一个清洗函数:
// utils/sanitize.jsexportfunctionsanitizeRich(html){returnhtml// 协议头补全.replace(/src="\/\//g,'src="https://').replace(/href="\/\//g,'href="https://')// 干掉会导致排版错乱的样式.replace(/style="[^"]*position\s*:\s*fixed[^"]*"/gi,'').replace(//gi, '')// 图片统一加上懒加载参数.replace(/import{computed}from'vue'import{sanitizeRich}from'@/utils/sanitize.js'constprops=defineProps({content:String})constsanitizedHtml=computed(()=>sanitizeRich(props.content||''))投递状态机:一个枚举管住所有按钮态
职位详情页的按钮文案会变:未投递→"投递简历",已投递→"投递成功(灰)“,被查看→"已被查看”,进入沟通→"去沟通"。状态一多,if 嵌套就乱。job-uniapp 用一张映射表:
// constants/applyStatus.jsexportconstAPPLY_STATUS={NOT_APPLIED:{code:0,text:'投递简历',type:'primary',disabled:false},APPLIED:{code:1,text:'已投递',type:'default',disabled:true},VIEWED:{code:2,text:'已被查看',type:'default',disabled:true},CONTACTED:{code:3,text:'去沟通',type:'primary',disabled:false},}exportfunctionnextApplyAction(status){constmap={0:APPLY_STATUS.APPLIED,// 点投递 → 已投递1:APPLY_STATUS.VIEWED,// 后端轮询推进2:APPLY_STATUS.CONTACTED,}returnmap[status]||APPLY_STATUS.NOT_APPLIED}轮询用setInterval拉投递状态,推进到 2/3 就停,避免一直请求:
lettimer=nullfunctionpollApplyStatus(orderNo){timer=setInterval(async()=>{const{data}=awaitgetApplyStatus(orderNo)if([2,3].includes(data.status)){clearInterval(timer)}applyStatus.value=data.status},5000)}分享海报:canvas 画图,先落本地再 drawImage
海报要放职位名、薪资、二维码,uniapp 里最稳的是 canvas 2D 接口,但有个大坑:网络图片不能直接 drawImage,会白图。
// utils/poster.jsasyncfunctionbuildPoster({title,salary,qrUrl}){constqrPath=awaitdownloadImage(qrUrl)// 先落本地constcanvas=uni.createCanvasContext('posterCanvas')canvas.setFillStyle('#ffffff')canvas.fillRect(0,0,600,900)canvas.setFillStyle('#222222')canvas.setFontSize(32)canvas.fillText(title.slice(0,12),40,120)// 标题超长截断canvas.setFontSize(28)canvas.setFillStyle('#ff7d00')canvas.fillText(salary,40,200)canvas.drawImage(qrPath,460,720,100,100)// 本地路径才能画canvas.draw()returnawaitnewPromise((resolve)=>{canvas.toTempFilePath({success:(res)=>resolve(res.tempFilePath),})})}functiondownloadImage(url){returnnewPromise((resolve,reject)=>{uni.downloadFile({url,success:(res)=>resolve(res.tempFilePath),fail:reject,})})}保存到相册前先uni.authorize({ scope: 'scope.writePhotosAlbum' }),拒绝过就引导去设置页开权限。
富文本里的图片点击预览,别让用户干瞪眼
企业介绍里的图片,点了没反应会显得很业余。原生 rich-text 不支持图片点击事件,这也是我换 u-parse 的原因之一——它自带imgtap事件。预览用 uni 内置的图片预览能力,传当前图和全部图片列表,支持左右滑:
functionpreviewImg(e){// u-parse 的 imgtap 事件带当前图片 url 和整组 urlsconstcurrent=e.urlconsturls=e.urls&&e.urls.length?e.urls:[current]uni.previewImage({current,urls})}图片预览要传整个 urls 数组,只传当前图的话,用户想滑到下一张就会退出预览,体验差一截。这个细节是拿真机测出来的,模拟器上不明显。
投递接口的幂等:用户连点两下,不能投出两份简历
投递按钮虽然disabled了,但手快的人连点两下、或者弱网下请求重发,后端就会收到两条投递记录。前端的disabled是防君子不防小人的,真正的幂等要后端兜底。job-uniapp 后端在投递表上加了唯一索引:
CREATETABLEjob_apply(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,job_idBIGINTNOTNULL,statusTINYINTNOTNULLDEFAULT0COMMENT'0已投递 1被查看 2已沟通',create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,UNIQUEKEYuk_user_job(user_id,job_id)-- 一个人对同一职位只能投一次)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;接口里INSERT ... ON DUPLICATE KEY UPDATE id = id,重复投递返回已投递状态而不是报错,前端拿到status直接刷新按钮态。唯一索引比"先查后插"靠谱,并发下先查后插照样穿,唯一索引是硬约束。
海报尺寸和二维码,真机上挨个调过
canvas 海报尺寸我踩过一版坑:设计稿 600×900 逻辑像素,真机 iPhone 和安卓渲染出来差一截——因为 canvas 的绘制单位是 px,真机 DPR 不同,同一张画在不同屏上会糊或者裁掉。job-uniapp 的解法是按设计稿固定逻辑尺寸,导出时放大 2 倍:
functiondrawPoster(){constscale=2// 2 倍图,真机更清晰constw=600*scaleconsth=900*scaleconstcanvas=uni.createCanvasContext('posterCanvas')// 所有坐标按 scale 缩放后绘制}二维码那块还要求必须带职位 id 的跳转参数,不然海报扫出来进的是首页不是职位详情,转化全白费。生成二维码时把https://m.xxx.com/job/{jobId}编码进去,这个细节产品验收时专门盯过。
踩坑:海报 onLoad 就画,canvas 还没就绪
现象:分享海报弹层第一次打开,图片区域是空白的,关掉再开又正常。
排查:打日志发现第一次canvas.draw()时downloadFile还没回来,drawImage画的是空路径;第二次因为图片有缓存,downloadFile秒回,竞态消失了,所以"看起来"正常。
定位:onLoad里直接调buildPoster,但 canvas 组件要等this.$nextTick渲染完成后才有尺寸上下文,网络图下载更是异步,两个异步叠加就是竞态。
解决:弹层用v-if控制,打开后nextTick里再画;海报生成函数加个"图片先下载完再 drawImage"的串行保证,下载用Promise.all等所有图就绪:
asyncfunctionopenPoster(){posterVisible.value=trueawaitnextTick()awaitnewPromise((r)=>setTimeout(r,300))// 等 canvas 渲染constqrPath=awaitdownloadImage(qrUrl.value)awaitdrawPoster(qrPath)}可直接复用的清单
- 富文本必过清洗函数:补协议头、删 fixed/script、图片加 lazy,再用 u-parse 渲染。
- 投递/订单这类多态按钮,用"状态→文案→可用性"映射表,别写 if 嵌套。
- canvas 画网络图必须先
uni.downloadFile落本地,直接画 URL 必白图。 - 投递幂等靠后端唯一索引(user_id + job_id),前端 disabled 只是体验优化。
- 弹层里的 canvas 等
nextTick+ 短延时再画,防竞态。 - 海报按逻辑尺寸绘制、导出 2 倍图,二维码必须带跳转参数。
- 存相册先授权
scope.writePhotosAlbum,被拒后引导开设置。
项目源码:https://gitee.com/gzqkl/job-uniapp