收资页面上传的照片基本上都需要水印,当前所有水印均是用户使用第三方水印相机拍摄之后再通过小程序上传,过程麻烦冗长,收资本来拍摄的照片就非常多,加上每个图片水印相机信息设置-水印拍照-照片上传,花费的时间非常长,并且容易出错。
分析
希望实现小程序拍照直接生成水印并直接上传到服务端,一趟水自动完成拍照->水印->上传
设计思路
交互设计
图片上传自动判断上传文件类型为图片/视频上传,提供三种方式供用户选择(拍照/从手机相册选择/水印拍照),选择水印拍照自动调用水印拍照组件拍摄并自动完成上传.
技术实现
原生camera组件+Painter插件
- camera组件获取原始拍摄照片
- 获取水印(定位、地名、用户信息)相关信息
- 照片+水印信息 设置Painter插件模板
- Painter绘制生成带水印图片
- wx.getFileInfo获取图片相关信息
- 后台图片上传oss
- 图片上传完成
问题及处理
步骤2中获取用户定位,受微信wx.getLocation返回时间影响,导致步骤2花费时间比较长,综合考虑之后,可以将获取定位及地名放到页面级别获取,提前获取水印信息,缩短水印拍照时间,改造后步骤二如下:
- 进入收资页面即异步获取用户定位,并存入缓存。
- 水印拍照时判断缓存中是否有用户定位及地名,有则直接使用
- 否则本次拍照还是即时获取,并存入缓存,第二次之后还是使用缓存加速
埋点梳理
业务埋点
burialPointFn({ eventName: '水印相机', desc: '水印相机拍照成功', eventAttr: { currentPage: currentPage } })
burialPointFn({ eventName: '水印相机', desc: '水印相机拍照未获取到Storage信息', eventAttr: { currentPage: currentPage } })
异常埋点
burialPointFn({ eventName: '水印相机', appId: 99, desc: '水印拍照canvas生成失败!', eventAttr: { err: e } })
burialPointFn({ eventName: '水印相机', desc: '水印相机Storage信息中获取addressName失败', appId: 99,eventAttr: { currentPage: currentPage, watermarkPhotoInfo: watermarkPhotoInfo } })
burialPointFn({ eventName: '水印相机', desc: '水印相机wx.getLocation接口获取地址信息失败', appId: 99, eventAttr: { currentPage: currentPage, adderssInfo: adderssInfo } })
burialPointFn({ eventName: '水印相机', appId: 99, desc: '水印拍照获取水印信息失败!', eventAttr: { err: error } })
具体代码
<view class="watermarkPhoto">
<camera wx:if="{{photoHeight > 0}}" device-position="back" style="height: {{photoHeight+'rpx'}}" flash="auto" binderror="error" class="card-size"></camera>
<view class="box">
<painter
:dirty="true"
customStyle="position: absolute; left: -9999rpx;"
scaleRatio="{{scaleRatio}}"
palette="{{template}}"
bind:imgOK="canvasSuc"
bind:imgErr="canvasErr"
/>
</view>
<view class="photo-action">
<view bind:tap="fnCancel">
<text>取消</text>
</view>
<view class="take-photo-btn" bind:tap="takePhoto">
<view class="take-photo-btn-inside"></view>
</view>
<view></view>
</view>
</view>
const app = getApp();
import {
format, getFileInfo, getAddressInfo
} from "../../utils/util"
import { burialPointFn } from '../../utils/burialPoint'
import * as api from "../../api/api"
const config = app.require('api/config.js')
Component({
data: {
template: {},
scaleRatio: 3,
image: "",
photoHeight: 0,
loading: false
},
properties: {},
methods: {
// 图片绘制成功回调
async canvasSuc (e) {
try {
wx.hideLoading()
this.setData({
image: e.detail.path,
})
let imgInfo = await getFileInfo(e.detail.path)
let imgRealInfo = await this.uploadFile({
files: [e.detail.path], // 文件本地地址
size: imgInfo.size, // 文件大小
fileType: 'image', // 文件类型 image/video
})
const pages = getCurrentPages();
const currentPage = pages && pages.length > 0 ? pages[pages.length - 1] : ''; // 当前页面
burialPointFn({ eventName: '水印相机', desc: '水印相机拍照成功', eventAttr: { currentPage: currentPage } })
this.triggerEvent('watermarkShootSuccess', imgRealInfo)
} catch (error) {
that.setData({
loading: false
})
}
},
canvasErr(e) {
burialPointFn({ eventName: '水印相机', appId: 99, desc: '水印拍照canvas生成失败!', eventAttr: { err: e } })
},
// 获取水印信息
async getDraw (url) {
let that = this
wx.showLoading({
title: '正在努力生成中',
})
try {
// 企业列表
let officeList = wx.getStorageSync('offices')
// 用户信息
let userInfo = wx.getStorageSync('getUserInfor')
const watermarkPhotoInfo = wx.getStorageSync('watermarkPhotoInfo')
const pages = getCurrentPages();
const currentPage = pages && pages.length > 0 ? pages[pages.length - 1] : ''; // 当前页面
let latitude = '', longitude = '', addressName = ''
if(watermarkPhotoInfo) {
latitude = watermarkPhotoInfo.latitude
longitude = watermarkPhotoInfo.longitude
addressName = watermarkPhotoInfo.addressName
if(!addressName) {
burialPointFn({ eventName: '水印相机', desc: '水印相机Storage信息中获取addressName失败', appId: 99,eventAttr: { currentPage: currentPage, watermarkPhotoInfo: watermarkPhotoInfo } })
}
} else {
burialPointFn({ eventName: '水印相机', desc: '水印相机拍照未获取到Storage信息', eventAttr: { currentPage: currentPage } })
// 此接口很慢,尽量走上面缓存来(微信wx.getLocation接口返回慢,尽量在页面将定位信息放到缓存中)
let adderssInfo = await getAddressInfo()
latitude = adderssInfo.latitude
longitude = adderssInfo.longitude
addressName = adderssInfo.addressName
if(!addressName) {
burialPointFn({ eventName: '水印相机', desc: '水印相机wx.getLocation接口获取地址信息失败', appId: 99, eventAttr: { currentPage: currentPage, adderssInfo: adderssInfo } })
}
wx.setStorageSync('watermarkPhotoInfo', { latitude, longitude, addressName })
}
let time = format(new Date(), 'yyyy-MM-dd hh:mm') // 拍摄时间
let customer = wx.getStorageSync('watermarkUsername') // 客户名称
let principal = userInfo?.nickName ?? '' // 负责人
let officeName = '' // 公司名称
if (userInfo && userInfo.officeCode && officeList && officeList.length > 0) {
officeList.forEach((item) => {
if (item.officeCode == userInfo.officeCode) {
officeName = item.officeName
}
})
}
this.setTemplate({ url, time, addressName, latitude, longitude, customer, principal, officeName })
} catch (error) {
that.setData({
loading: false
})
wx.hideLoading()
burialPointFn({ eventName: '水印相机', appId: 99, desc: '水印拍照获取水印信息失败!', eventAttr: { err: error } })
console.log(error)
}
},
// 文件上传服务端
uploadFile (data) {
wx.showLoading({
title: '上传中...',
})
return new Promise((reslove, reject) => {
api.uploadFile({
data: data,
success (res) {
let url = res.fileName //地址
let name = res.fileName
let size = res.size; //大小
let fileType = res.fileType // 文件类型 image/video
wx.hideLoading();
reslove({ url, name, size, fileType });
},
fail (res) {
wx.hideLoading();
wx.showToast({
title: '上传失败',
icon: 'none'
})
reject(res)
}
})
})
},
// 设置水印模板
setTemplate ({ url, time, addressName, latitude, longitude, customer, principal, officeName }) {
// url=${url}&
let qrcodeUrl = `${config.getXjH5Url()}/watermarkPhotoInfo?time=${time}&addressName=${addressName}&latitude=${latitude}&longitude=${longitude}&customer=${customer}&principal=${principal}&officeName=${officeName}`
this.setData(
{
template:
{
"background": "#fff",
"width": "750rpx",
"height": this.data.photoHeight + "rpx",
"views": [
{
"type": "image",
"url": url,
"css": {
"width": "750rpx",
"height": this.data.photoHeight + "rpx"
}
},
{
"type": "rect",
"css": {
"width": "400rpx",
"height": "80rpx",
"bottom": "210rpx",
"left": "30rpx",
"backgroundColor": "#000",
"color":
"linear-gradient(-90deg, rgba(3,61,149,0.5) 0%, transparent 100%)"
}
},
{
"type": "image",
"url": "https://cdn-images.cdfinance.com.cn/pv-static/file/wx/static/img/logo2.png",
"css": {
"width": "80rpx",
"height": "80rpx",
"bottom": "210rpx",
"left": "35rpx"
}
},
{
"type": "text",
"text": officeName,
"css": {
"bottom": officeName.length < 13 ? "240rpx" : "230rpx",
"left": "110rpx",
"width": "300rpx",
"fontSize": "22rpx",
"color": "#ffffff",
"fontWeight": "bold",
"textAlign": "left"
}
},
{
"type": "text",
"text": "拍摄时间:" + time,
"css": {
"bottom": "180rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#fff"
// "color": "rgb(241,209,48)"
}
},
{
"type": "text",
"text": "拍摄地点:" + addressName,
"css": {
"bottom": "150rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#ffffff"
}
},
{
"type": "text",
"text": "经 度:" + latitude,
"css": {
"bottom": "120rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#ffffff"
}
},
{
"type": "text",
"text": "纬 度:" + longitude,
"css": {
"bottom": "90rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#ffffff"
}
},
{
"type": "text",
"text": "客户名称:" + customer,
"css": {
"bottom": "60rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#ffffff"
}
},
{
"type": "text",
"text": "负 责 人:" + principal,
"css": {
"bottom": "30rpx",
"left": "30rpx",
"fontSize": "20rpx",
"color": "#ffffff"
}
},
{
"type": "rect",
"css": {
"width": "100rpx",
"height": "130rpx",
"top": "20rpx",
"right": "20rpx",
"backgroundColor": "#fff",
"color": "#fff"
}
},
{
"type": "text",
"text": "扫码导航",
"css": {
"top": "120rpx",
"right": "30rpx",
"fontSize": "20rpx",
"color": "#000"
}
},
{
type: "qrcode",
content: qrcodeUrl,
css: {
top: "30rpx",
right: "30rpx",
width: "80rpx",
height: "80rpx",
},
},
]
}
}
)
},
fnCancel () {
this.triggerEvent('close')
},
takePhoto () {
let that = this
// this.getDraw('https://cdn-images.cdfinance.com.cn/pv-static/file/common/ceshi.png')
if(that.data.loading) {
return
}
const ctx = wx.createCameraContext()
that.setData({
loading: true
}, ()=> {
ctx.takePhoto({
quality: 'high',
success: (res) => {
if (res.errMsg === 'takePhoto:ok' || res.errMsg === 'operateCamera:ok') {
that.getDraw(res.tempImagePath)
}
},
fail: () => {
that.setData({
loading: false
})
}
})
})
},
},
ready: function () {
let that = this
wx.getSystemInfo({
success: (res) => {
let clientHeight = res.windowHeight;
let clientWidth = res.windowWidth;
let changeHeight = 750 / clientWidth;
let height = clientHeight * changeHeight;
that.setData({
photoHeight: height - 320
})
}
})
},
})
效果演示
此处为语雀视频卡片,点击链接查看:73789d2569e23a777ff5ef383e68e822.mp4