
关于把google登录集成到uniapp安卓APP的最终帖子
坦白讲,我真的被uniapp官方以及一些登录插件搞死了,总算是搞成了,记录一下有哪些坑。
先写有哪些坑,因为这些坑太痛苦,太隐藏了,然后官方也好,很多说明也好都没有提到!简直误国误民!
1,SHA1 有两个!一个是你用keystore通过命令行生成的,一个是google play console 里面的(在play console设置=》签名),这两个不是一样的,测试环境用前面的,正式环境用后面的。
我想问一下官方,为什么不说!为什么不说!我想问一下那些搞google 登录插件的,为什么不说!为什么不说!

2,不是创建web端的凭证,是创建安卓端的凭证,有人说用web端的,有一些插件也说是,反正我用的安卓端的,而且我的是要审核的,就是我这个最后要提交给谷歌审核,在审核通过前,好像只能验证100个用户。
3,firebase我也整了半天,反正最后是通过https://console.cloud.google.com/这里建立auth2.0凭证,选择的安卓客户端,然后弄成的。
反正最终我这个是可以的,也没有谁说过要去谷歌审核,但是话说回来,谷歌登录给你装到你app,你说你不要审核,你觉得可能吗!
如果是黄赌毒,用谷歌登录,是什么感觉!问题是我从来没有在官方以及其他任何一个帖子上看到。
坦白讲,我真的被uniapp官方以及一些登录插件搞死了,总算是搞成了,记录一下有哪些坑。
先写有哪些坑,因为这些坑太痛苦,太隐藏了,然后官方也好,很多说明也好都没有提到!简直误国误民!
1,SHA1 有两个!一个是你用keystore通过命令行生成的,一个是google play console 里面的(在play console设置=》签名),这两个不是一样的,测试环境用前面的,正式环境用后面的。
我想问一下官方,为什么不说!为什么不说!我想问一下那些搞google 登录插件的,为什么不说!为什么不说!
2,不是创建web端的凭证,是创建安卓端的凭证,有人说用web端的,有一些插件也说是,反正我用的安卓端的,而且我的是要审核的,就是我这个最后要提交给谷歌审核,在审核通过前,好像只能验证100个用户。
3,firebase我也整了半天,反正最后是通过https://console.cloud.google.com/这里建立auth2.0凭证,选择的安卓客户端,然后弄成的。
反正最终我这个是可以的,也没有谁说过要去谷歌审核,但是话说回来,谷歌登录给你装到你app,你说你不要审核,你觉得可能吗!
如果是黄赌毒,用谷歌登录,是什么感觉!问题是我从来没有在官方以及其他任何一个帖子上看到。

HBuilderX 发布目录从 H5 调整为 web 的兼容方案
本文受众
本文用来解释和指导发布目录从 h5
调整 web
过程,收到影响的用户。
背景
HBuilderX 从 4.06 开始 uni-app 的 web 项目的编译目录从 h5 调整为 web 目录了。
- 调整 uni-app运行 unpackage编译目录
h5
目录名改为web
这一条改动很多人没有留意到,沿用历史的持续集成流程,会产生错误,因为目录发生了变更。
改动的原因,在相关 ask 社区中也有回答,主要还是为了统一概念。
早期 uni-app 在 web 端主要使用移动端展示内容,随着功能迭代和使用场景丰富,很多用户在宽屏 PC 端也会选择 uni-app 进行渲染和展示。
相关 api 的返回值,条件编译等已经陆陆续续统一到 web
上来,所以调整编译目录也是在计划中。
考虑到很多用户的使用场景,并不能轻易调整持续构建的流程,这里提供一些临时的兼容方案进行过度,最好的方案还是修改持续集成的流程。
临时兼容方案
如果是类似 jenkins/业务定制的 CICD 流程,可以尝试添加 post scripts,linux 服务器
cp -r unpackage/dist/build/web unpackage/dist/build/h5
# 或者习惯用 mv,二选一即可
mv unpackage/dist/build/web unpackage/dist/build/h5
如果你使用 windows
服务器
copy unpackage\\dist\\build\\web unpackage\\dist\\build\\h5
如果也不允许修改 jenkins ,可以尝试添加 npm scripts
添加上方命令。在 build 结束之后继续执行命令。
如果你仍有问题,欢迎留言讨论。
本文受众
本文用来解释和指导发布目录从 h5
调整 web
过程,收到影响的用户。
背景
HBuilderX 从 4.06 开始 uni-app 的 web 项目的编译目录从 h5 调整为 web 目录了。
- 调整 uni-app运行 unpackage编译目录
h5
目录名改为web
这一条改动很多人没有留意到,沿用历史的持续集成流程,会产生错误,因为目录发生了变更。
改动的原因,在相关 ask 社区中也有回答,主要还是为了统一概念。
早期 uni-app 在 web 端主要使用移动端展示内容,随着功能迭代和使用场景丰富,很多用户在宽屏 PC 端也会选择 uni-app 进行渲染和展示。
相关 api 的返回值,条件编译等已经陆陆续续统一到 web
上来,所以调整编译目录也是在计划中。
考虑到很多用户的使用场景,并不能轻易调整持续构建的流程,这里提供一些临时的兼容方案进行过度,最好的方案还是修改持续集成的流程。
临时兼容方案
如果是类似 jenkins/业务定制的 CICD 流程,可以尝试添加 post scripts,linux 服务器
cp -r unpackage/dist/build/web unpackage/dist/build/h5
# 或者习惯用 mv,二选一即可
mv unpackage/dist/build/web unpackage/dist/build/h5
如果你使用 windows
服务器
copy unpackage\\dist\\build\\web unpackage\\dist\\build\\h5
如果也不允许修改 jenkins ,可以尝试添加 npm scripts
添加上方命令。在 build 结束之后继续执行命令。
如果你仍有问题,欢迎留言讨论。
收起阅读 »
社交购物源码,小红书1:1还原,小红书APP社区源码模板软件开发社区电商支持即时通讯社交
小红书APP源码,1:1还原小红书功能。可接定制二次开发需求。有疑问请随时联系在线客服,谢谢!
主要功能:推荐算法、视频、圈子、购物、直播、小视频、话题、投票、城市、天气、实名认证、发布、任务、邀请、深色模式、广告、采集、暗黑模式、多语言
客户端版本:本程序有H5、小程序、安卓、苹果、电脑端、5端同步共用同一套后台,数据互通。
本程序是我们自主开发,不依赖第三方系统。
接口开发语言:PHP,mysql,thinkphp5
官网地址:www.suxiangw.com
服务承诺:提供技术、维护、更新,提供源代码,合同等服务
小红书APP源码,1:1还原小红书功能。可接定制二次开发需求。有疑问请随时联系在线客服,谢谢!
主要功能:推荐算法、视频、圈子、购物、直播、小视频、话题、投票、城市、天气、实名认证、发布、任务、邀请、深色模式、广告、采集、暗黑模式、多语言
客户端版本:本程序有H5、小程序、安卓、苹果、电脑端、5端同步共用同一套后台,数据互通。
本程序是我们自主开发,不依赖第三方系统。
接口开发语言:PHP,mysql,thinkphp5
官网地址:www.suxiangw.com
服务承诺:提供技术、维护、更新,提供源代码,合同等服务

uniapp Composition API 写法,onLaunch和onLoad的异步问题最新解决方案,简单易懂
在使用 uni-app 开发项目时,会遇到需要在 onLaunch 中请求接口返回结果,并且此结果在项目各个页面的 onLoad 中都有可能使用到的需求,比如微信小程序在 onLaunch 中进行登录后取得 openid 并获得 token,项目各页面需要带上该 token 请求其他接口。
在onLaunch 中的请求是异步的,也就是说在执行 onLaunch 后页面 onLoad 就开始执行了,而不会等待 onLaunch 异步返回数据后再执行,这就导致了页面无法拿到 onLaunch 中异步获取的数据。
使用custom-hooks-plus库可以完美的帮我解决这个问题。
可以定义一个全局变量
// global.ts 文件
import { proxyData } from 'custom-hooks-plus'
interface GlobalData {
token: string
userInfo: number
}
export const globalData = proxyData({
token: '',
})
export function set<K extends keyof GlobalData>(key: K, val: GlobalData[K]) {
globalData[key] = val
}
export function get<K extends keyof GlobalData>(key: K): GlobalData[K] {
return globalData[key]
}
// App.vue
import { init } from 'custom-hooks-plus';
init(
{
Token: {
key: 'token', // 监听global文件中globalData的token的变化
}
}
);
onLaunch(() => {
uni.login((res) => {
// 进行登录操作,修改globalData中的token
})
})
// 页面中使用
import { onCustomLoad, onCustomShow } from 'custom-hooks-plus';
onCustomLoad((options) => {
console.log('globalData的token都被修改了才会触发');
}, ['Token']);
通过上面的代码实现了,页面中的onCustomLoad的回调会等token有值后再执行,完美的解决了onLaunch和onLoad的异步问题。
具体可看:https://github.com/DBAAZzz/custom-hooks-plus
在使用 uni-app 开发项目时,会遇到需要在 onLaunch 中请求接口返回结果,并且此结果在项目各个页面的 onLoad 中都有可能使用到的需求,比如微信小程序在 onLaunch 中进行登录后取得 openid 并获得 token,项目各页面需要带上该 token 请求其他接口。
在onLaunch 中的请求是异步的,也就是说在执行 onLaunch 后页面 onLoad 就开始执行了,而不会等待 onLaunch 异步返回数据后再执行,这就导致了页面无法拿到 onLaunch 中异步获取的数据。
使用custom-hooks-plus库可以完美的帮我解决这个问题。
可以定义一个全局变量
// global.ts 文件
import { proxyData } from 'custom-hooks-plus'
interface GlobalData {
token: string
userInfo: number
}
export const globalData = proxyData({
token: '',
})
export function set<K extends keyof GlobalData>(key: K, val: GlobalData[K]) {
globalData[key] = val
}
export function get<K extends keyof GlobalData>(key: K): GlobalData[K] {
return globalData[key]
}
// App.vue
import { init } from 'custom-hooks-plus';
init(
{
Token: {
key: 'token', // 监听global文件中globalData的token的变化
}
}
);
onLaunch(() => {
uni.login((res) => {
// 进行登录操作,修改globalData中的token
})
})
// 页面中使用
import { onCustomLoad, onCustomShow } from 'custom-hooks-plus';
onCustomLoad((options) => {
console.log('globalData的token都被修改了才会触发');
}, ['Token']);
通过上面的代码实现了,页面中的onCustomLoad的回调会等token有值后再执行,完美的解决了onLaunch和onLoad的异步问题。
具体可看:https://github.com/DBAAZzz/custom-hooks-plus
收起阅读 »
原创uniapp+vue3+uni-ui聊天室|uni-app+vue3仿微信app模板
原创研发uniapp+vue3+pinia2+uv-ui+vite4.x跨端仿微信app聊天Uniapp-Wechat。支持编译到H5+小程序端+App端。实现编辑框多行自适应高度消息+emoj混合、长按仿微信语音面板、图片/视频预览、红包/朋友圈等功能。
https://ask.dcloud.net.cn/article/40928
编译h5+小程序+APP端效果
uni-vue3-wechat项目使用HbuilderX4.0.8开发工具,采用vue3 setup
语法规范编码。
使用技术
- 开发工具:HbuilderX 4.0.8
- 技术框架:Uniapp+Vue3+Pinia2+Vite4.x
- UI组件库:uni-ui+uv-ui
- 弹框组件:uv3-popup(uniapp+vue3多端自定义弹框组件)
- 自定义组件:uv3-navbar+uv3-tabbar组件
- 缓存服务:pinia-plugin-unistorage
- 编译支持:H5+小程序+APP端
项目结构图
main.js配置
/**
* 入口文件 main.js
*/
import { createSSRApp } from 'vue'
import App from './App'
// 引入pinia状态管理
import pinia from '@/pinia'
export function createApp() {
const app = createSSRApp(App)
app.use(pinia)
return {
app,
pinia
}
}
在web端则是以750px像素布局。
App.vue配置
<script setup>
import { provide } from 'vue'
import { onLaunch, onShow, onHide, onPageNotFound } from '@dcloudio/uni-app'
onLaunch(() => {
console.log('App Launch')
uni.hideTabBar()
loadSystemInfo()
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
onPageNotFound((e) => {
console.warn('Route Error:', `${e.path}`)
})
// 获取系统设备信息
const loadSystemInfo = () => {
uni.getSystemInfo({
success: (e) => {
// 获取手机状态栏高度
let statusBar = e.statusBarHeight
let customBar
// #ifndef MP
customBar = statusBar + (e.platform == 'android' ? 50 : 45)
// #endif
// #ifdef MP-WEIXIN
// 获取按钮的布局位置信息
let menu = wx.getMenuButtonBoundingClientRect()
// 导航栏高度 = 下距离 + 上距离 - 状态栏高度
customBar = menu.bottom + menu.top - statusBar
// #endif
// #ifdef MP-ALIPAY
customBar = statusBar + e.titleBarHeight
// #endif
// 由于globalData在vue3 setup存在兼容性问题,改为provide/inject替代方案
provide('globalData', {
statusBarH: statusBar,
customBarH: customBar,
screenWidth: e.screenWidth,
screenHeight: e.screenHeight,
platform: e.platform
})
}
})
}
</script>
<style>
/* #ifndef APP-NVUE */
@import 'static/fonts/iconfont.css';
/* #endif */
</style>
<style lang="scss">
@import 'styles/reset.scss';
@import 'styles/layout.scss';
</style>
公共布局模板
项目整体分为顶部区域+内容区+底部区域三个大的模块。
<!-- 公共布局模板 -->
<!-- #ifdef MP-WEIXIN -->
<script>
export default {
/**
* 解决小程序class、id问题
* manifest.json中配置mergeVirtualHostAttributes: true, 在微信小程序平台不生效,组件外部传入的class没有挂到组件根节点上,在组件中增加options: { virtualHost: true }
* https://github.com/dcloudio/uni-ui/issues/753
*/
options: { virtualHost: true }
}
</script>
<!-- #endif -->
<script setup>
const props = defineProps({
// 是否显示自定义tabbar
showTabBar: { type: [Boolean, String], default: false },
})
</script>
<template>
<view class="uv3__container flexbox flex-col flex1">
<!-- 顶部插槽 -->
<slot name="header" />
<!-- 内容区 -->
<view class="uv3__scrollview flex1">
<slot />
</view>
<!-- 底部插槽 -->
<slot name="footer" />
<!-- tabbar栏 -->
<uv3-tabbar v-if="showTabBar" hideTabBar fixed />
</view>
</template>
uniapp+vue3实现微信九宫格图像
<script setup>
import { onMounted, ref, computed, watch, getCurrentInstance } from 'vue'
const props = defineProps({
// 图像组
avatar: { type: Array, default: null },
})
const instance = getCurrentInstance()
const uuid = computed(() => Math.floor(Math.random() * 10000))
const avatarPainterId = ref('canvasid' + uuid.value)
const createAvatar = () => {
const ctx = uni.createCanvasContext(avatarPainterId.value, instance)
// 计算图像在画布上的坐标
const avatarSize = 12
const gap = 2
for(let i = 0, len = props.avatar.length; i < len; i++) {
const row = Math.floor(i / 3)
const col = i % 3
const x = col * (avatarSize + gap)
const y = row * (avatarSize + gap)
ctx.drawImage(props.avatar[i], x, y, avatarSize, avatarSize)
}
ctx.draw(false, () => {
// 输出临时图片
/* uni.canvasToTempFilePath({
canvasId: avatarPainterId.value,
success: (res) => {
console.log(res.tempFilePath)
}
}) */
})
}
onMounted(() => {
createAvatar()
})
watch(() => props.avatar, () => {
createAvatar()
})
</script>
<template>
<template v-if="avatar.length > 1">
<view class="uv3__avatarPainter">
<canvas :canvas-id="avatarPainterId" class="uv3__avatarPainter-canvas"></canvas>
</view>
</template>
<template v-else>
<image class="uv3__avatarOne" :src="avatar[0]" />
</template>
</template>
<style lang="scss" scoped>
.uv3__avatarPainter {background-color: #eee; border-radius: 5px; overflow: hidden; padding: 2px; height: 44px; width: 44px;}
.uv3__avatarPainter-canvas {height: 100%; width: 100%;}
.uv3__avatarOne {border-radius: 5px; height: 44px; width: 44px;}
</style>
项目中顶部导航条、底部菜单栏、各种弹窗功能均是使用自定义组件实现功能。
uniapp+vue3聊天功能
<view v-if="voicePanelEnable" class="uv3__voicepanel-popup">
<view class="uv3__voicepanel-body flexbox flex-col">
<!-- 取消发送+语音转文字 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-transfer">
<!-- 提示动效 -->
<view class="animtips flexbox" :class="voiceType == 2 ? 'left' : voiceType == 3 ? 'right' : null"><Waves :lines="[2, 3].includes(voiceType) ? 10 : 20" /></view>
<!-- 操作项 -->
<view class="icobtns flexbox">
<view class="vbtn cancel flexbox flex-col" :class="{'hover': voiceType == 2}" @click="handleVoiceCancel"><text class="vicon uv3-icon uv3-icon-close"></text></view>
<view class="vbtn word flexbox flex-col" :class="{'hover': voiceType == 3}"><text class="vicon uv3-icon uv3-icon-word"></text></view>
</view>
</view>
<!-- 识别结果状态 -->
<view v-if="voiceToTransfer" class="uv3__voicepanel-transfer result fail">
<!-- 提示动效 -->
<view class="animtips flexbox"><uni-icons type="info-filled" color="#fff" size="20"></uni-icons><text class="c-fff">未识别到文字</text></view>
<view class="icobtns flexbox">
<view class="vbtn cancel flexbox flex-col" @click="handleVoiceCancel"><text class="vicon uv3-icon uv3-icon-chexiao"></text>取消</view>
<view class="vbtn word flexbox flex-col"><text class="vicon uv3-icon uv3-icon-audio"></text>发送原语音</view>
<view class="vbtn check flexbox flex-col"><text class="vicon uv3-icon uv3-icon-duigou"></text></view>
</view>
</view>
<!-- 背景语音图 -->
<view class="uv3__voicepanel-cover">
<image v-if="!voiceToTransfer" src="/static/voice_bg.webp" :webp="true" mode="widthFix" style="width: 100%;" />
</view>
<!-- 提示文字 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-tooltip">{{voiceTypeMap[voiceType]}}</view>
<!-- 背景图标 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-fixico"><text class="uv3-icon uv3-icon-audio fs-50"></text></view>
</view>
</view>
Okay,综上就是uni-app+vue3实战开发微信app聊天项目的一些知识分享。
作者:xiaoyan2015
链接: https://juejin.cn/post/7363121890791899170
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原创研发uniapp+vue3+pinia2+uv-ui+vite4.x跨端仿微信app聊天Uniapp-Wechat。支持编译到H5+小程序端+App端。实现编辑框多行自适应高度消息+emoj混合、长按仿微信语音面板、图片/视频预览、红包/朋友圈等功能。
https://ask.dcloud.net.cn/article/40928
编译h5+小程序+APP端效果
uni-vue3-wechat项目使用HbuilderX4.0.8开发工具,采用vue3 setup
语法规范编码。
使用技术
- 开发工具:HbuilderX 4.0.8
- 技术框架:Uniapp+Vue3+Pinia2+Vite4.x
- UI组件库:uni-ui+uv-ui
- 弹框组件:uv3-popup(uniapp+vue3多端自定义弹框组件)
- 自定义组件:uv3-navbar+uv3-tabbar组件
- 缓存服务:pinia-plugin-unistorage
- 编译支持:H5+小程序+APP端
项目结构图
main.js配置
/**
* 入口文件 main.js
*/
import { createSSRApp } from 'vue'
import App from './App'
// 引入pinia状态管理
import pinia from '@/pinia'
export function createApp() {
const app = createSSRApp(App)
app.use(pinia)
return {
app,
pinia
}
}
在web端则是以750px像素布局。
App.vue配置
<script setup>
import { provide } from 'vue'
import { onLaunch, onShow, onHide, onPageNotFound } from '@dcloudio/uni-app'
onLaunch(() => {
console.log('App Launch')
uni.hideTabBar()
loadSystemInfo()
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
onPageNotFound((e) => {
console.warn('Route Error:', `${e.path}`)
})
// 获取系统设备信息
const loadSystemInfo = () => {
uni.getSystemInfo({
success: (e) => {
// 获取手机状态栏高度
let statusBar = e.statusBarHeight
let customBar
// #ifndef MP
customBar = statusBar + (e.platform == 'android' ? 50 : 45)
// #endif
// #ifdef MP-WEIXIN
// 获取按钮的布局位置信息
let menu = wx.getMenuButtonBoundingClientRect()
// 导航栏高度 = 下距离 + 上距离 - 状态栏高度
customBar = menu.bottom + menu.top - statusBar
// #endif
// #ifdef MP-ALIPAY
customBar = statusBar + e.titleBarHeight
// #endif
// 由于globalData在vue3 setup存在兼容性问题,改为provide/inject替代方案
provide('globalData', {
statusBarH: statusBar,
customBarH: customBar,
screenWidth: e.screenWidth,
screenHeight: e.screenHeight,
platform: e.platform
})
}
})
}
</script>
<style>
/* #ifndef APP-NVUE */
@import 'static/fonts/iconfont.css';
/* #endif */
</style>
<style lang="scss">
@import 'styles/reset.scss';
@import 'styles/layout.scss';
</style>
公共布局模板
项目整体分为顶部区域+内容区+底部区域三个大的模块。
<!-- 公共布局模板 -->
<!-- #ifdef MP-WEIXIN -->
<script>
export default {
/**
* 解决小程序class、id问题
* manifest.json中配置mergeVirtualHostAttributes: true, 在微信小程序平台不生效,组件外部传入的class没有挂到组件根节点上,在组件中增加options: { virtualHost: true }
* https://github.com/dcloudio/uni-ui/issues/753
*/
options: { virtualHost: true }
}
</script>
<!-- #endif -->
<script setup>
const props = defineProps({
// 是否显示自定义tabbar
showTabBar: { type: [Boolean, String], default: false },
})
</script>
<template>
<view class="uv3__container flexbox flex-col flex1">
<!-- 顶部插槽 -->
<slot name="header" />
<!-- 内容区 -->
<view class="uv3__scrollview flex1">
<slot />
</view>
<!-- 底部插槽 -->
<slot name="footer" />
<!-- tabbar栏 -->
<uv3-tabbar v-if="showTabBar" hideTabBar fixed />
</view>
</template>
uniapp+vue3实现微信九宫格图像
<script setup>
import { onMounted, ref, computed, watch, getCurrentInstance } from 'vue'
const props = defineProps({
// 图像组
avatar: { type: Array, default: null },
})
const instance = getCurrentInstance()
const uuid = computed(() => Math.floor(Math.random() * 10000))
const avatarPainterId = ref('canvasid' + uuid.value)
const createAvatar = () => {
const ctx = uni.createCanvasContext(avatarPainterId.value, instance)
// 计算图像在画布上的坐标
const avatarSize = 12
const gap = 2
for(let i = 0, len = props.avatar.length; i < len; i++) {
const row = Math.floor(i / 3)
const col = i % 3
const x = col * (avatarSize + gap)
const y = row * (avatarSize + gap)
ctx.drawImage(props.avatar[i], x, y, avatarSize, avatarSize)
}
ctx.draw(false, () => {
// 输出临时图片
/* uni.canvasToTempFilePath({
canvasId: avatarPainterId.value,
success: (res) => {
console.log(res.tempFilePath)
}
}) */
})
}
onMounted(() => {
createAvatar()
})
watch(() => props.avatar, () => {
createAvatar()
})
</script>
<template>
<template v-if="avatar.length > 1">
<view class="uv3__avatarPainter">
<canvas :canvas-id="avatarPainterId" class="uv3__avatarPainter-canvas"></canvas>
</view>
</template>
<template v-else>
<image class="uv3__avatarOne" :src="avatar[0]" />
</template>
</template>
<style lang="scss" scoped>
.uv3__avatarPainter {background-color: #eee; border-radius: 5px; overflow: hidden; padding: 2px; height: 44px; width: 44px;}
.uv3__avatarPainter-canvas {height: 100%; width: 100%;}
.uv3__avatarOne {border-radius: 5px; height: 44px; width: 44px;}
</style>
项目中顶部导航条、底部菜单栏、各种弹窗功能均是使用自定义组件实现功能。
uniapp+vue3聊天功能
<view v-if="voicePanelEnable" class="uv3__voicepanel-popup">
<view class="uv3__voicepanel-body flexbox flex-col">
<!-- 取消发送+语音转文字 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-transfer">
<!-- 提示动效 -->
<view class="animtips flexbox" :class="voiceType == 2 ? 'left' : voiceType == 3 ? 'right' : null"><Waves :lines="[2, 3].includes(voiceType) ? 10 : 20" /></view>
<!-- 操作项 -->
<view class="icobtns flexbox">
<view class="vbtn cancel flexbox flex-col" :class="{'hover': voiceType == 2}" @click="handleVoiceCancel"><text class="vicon uv3-icon uv3-icon-close"></text></view>
<view class="vbtn word flexbox flex-col" :class="{'hover': voiceType == 3}"><text class="vicon uv3-icon uv3-icon-word"></text></view>
</view>
</view>
<!-- 识别结果状态 -->
<view v-if="voiceToTransfer" class="uv3__voicepanel-transfer result fail">
<!-- 提示动效 -->
<view class="animtips flexbox"><uni-icons type="info-filled" color="#fff" size="20"></uni-icons><text class="c-fff">未识别到文字</text></view>
<view class="icobtns flexbox">
<view class="vbtn cancel flexbox flex-col" @click="handleVoiceCancel"><text class="vicon uv3-icon uv3-icon-chexiao"></text>取消</view>
<view class="vbtn word flexbox flex-col"><text class="vicon uv3-icon uv3-icon-audio"></text>发送原语音</view>
<view class="vbtn check flexbox flex-col"><text class="vicon uv3-icon uv3-icon-duigou"></text></view>
</view>
</view>
<!-- 背景语音图 -->
<view class="uv3__voicepanel-cover">
<image v-if="!voiceToTransfer" src="/static/voice_bg.webp" :webp="true" mode="widthFix" style="width: 100%;" />
</view>
<!-- 提示文字 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-tooltip">{{voiceTypeMap[voiceType]}}</view>
<!-- 背景图标 -->
<view v-if="!voiceToTransfer" class="uv3__voicepanel-fixico"><text class="uv3-icon uv3-icon-audio fs-50"></text></view>
</view>
</view>
Okay,综上就是uni-app+vue3实战开发微信app聊天项目的一些知识分享。
作者:xiaoyan2015
链接: https://juejin.cn/post/7363121890791899170
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

skyline worklet 纯 setup 写法分享 ts支持 全场景支持
仅在微信小程序测试,多端请自行添加条件编译
首先是一个 worklet.ts 的工具
declare const wx: {
worklet: {
timing: (value: number, options?: { duration?: number, easing?: any }, callback?: Function) => AnimationObject
runOnJS: any
shared: <T>(val: T) => SharedValue<T>
derived: <T>(val: () => T) => SharedValue<T>
Easing: {
in: any
out: any
inOut: any
ease: any
}
}
}
export interface SharedValue<T = any> { value: T }
export type AnimationObject = any
export const worklet = wx.worklet
export function getMpInstance() {
return (getCurrentInstance()?.proxy as any).$scope
}
export interface MpInstance {
applyAnimatedStyle: (selector: string, callback: () => Record<string, any>) => void
}
export function workletValue<T>(val: T) {
return worklet.shared(val)
}
export function workletDerived<T>(val: () => T) {
return worklet.derived(val)
}
export function workletMethods<T extends { [key: string]: Function }>(methods: T) {
const mpInstance = getMpInstance()
for (const key in methods)
mpInstance[key] = methods[key]
return methods
}
export function useApplyAnimatedStyle() {
const mpInstance = getMpInstance()
return (selector: string, callback: () => Record<string, any>) => {
mpInstance.applyAnimatedStyle(selector, callback)
}
}
export const GestureState = {
POSSIBLE: 0, // 0 此时手势未识别,如 panDown等
BEGIN: 1, // 1 手势已识别
ACTIVE: 2, // 2 连续手势活跃状态
END: 3, // 3 手势终止
CANCELLED: 4, // 4 手势取消,
}
export const ScrollState = {
scrollStart: 0,
scrollUpdate: 1,
scrollEnd: 2,
}
使用案例
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
const props = defineProps<{
open: boolean
}>()
const _height = workletValue(0)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
applyAnimatedStyle('.collapse-item', () => {
'worklet'
return {
height: _height.value < 0 ? 'auto' : `${_height.value}px`,
}
})
})
watch(() => props.open, () => {
getBoundingClientRect('.collapse-content')
.then(rect => rect.height ?? 0)
.then((height) => {
if (!props.open) {
if (_height.value === -1)
_height.value = height
_height.value = worklet.timing(0, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) })
}
else {
_height.value = worklet.timing(height, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) }, () => {
'worklet'
if (_height.value === height)
_height.value = -1
})
}
})
})
</script>
<template>
<view class="collapse-item block overflow-hidden">
<view class="collapse-content block">
<slot />
</view>
</view>
</template>
worklet js 互调 inject popup 示例
popup.ts
const [useProvidePopupStore, usePopupStoreInner] = createInjectionState((close: () => void) => {
const _transY = workletValue(1000)
const _scrollTop = workletValue(0)
const _startPan = workletValue(true)
const _popupHeight = workletValue(1000)
return { _transY, _scrollTop, _startPan, _popupHeight, close }
})
export { useProvidePopupStore }
export function usePopupStore() {
const popupStore = usePopupStoreInner()
if (popupStore == null)
throw new Error('Please call `useProvidePopupStore` on the appropriate parent component')
return popupStore
}
export function usePopupWorkletMethods() {
const { _popupHeight, _scrollTop, _startPan, _transY, close } = usePopupStore()
return workletMethods({
openPopup() {
'worklet'
_transY.value = worklet.timing(0, { duration: 200 })
},
closePopup() {
'worklet'
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 })
worklet.runOnJS(close)()
},
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
'worklet'
return _startPan.value
},
shouldScrollViewResponse(pointerEvent: any) {
'worklet'
// transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效
if (_transY.value > 0)
return false
const scrollTop = _scrollTop.value
const { deltaY } = pointerEvent
// deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效
const result = scrollTop <= 0 && deltaY > 0
_startPan.value = result
return !result
},
handlePan(gestureEvent: any) {
'worklet'
if (gestureEvent.state === GestureState.ACTIVE) {
const curPosition = _transY.value
const destination = Math.max(0, curPosition + gestureEvent.deltaY)
if (curPosition === destination)
return
_transY.value = destination
}
if (
gestureEvent.state === GestureState.END
|| gestureEvent.state === GestureState.CANCELLED
) {
if (gestureEvent.velocityY > 500 && _transY.value > 50)
this.closePopup()
else if (_transY.value > _popupHeight.value / 2)
this.closePopup()
else
this.openPopup()
}
},
adjustDecelerationVelocity(velocity: number) {
'worklet'
const scrollTop = _scrollTop.value
return scrollTop <= 0 ? 0 : velocity
},
handleScroll(evt: any) {
'worklet'
_scrollTop.value = evt.detail.scrollTop
},
})
}
popup.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
import { useProvidePopupStore } from './popup'
const props = withDefaults(defineProps<{
open: boolean
fullScreen?: boolean
rounded?: boolean
}>(), {
fullScreen: false,
rounded: true,
})
const emit = defineEmits<{
(e: 'update:open', val: boolean): void
(e: 'scrolltolower'): void
(e: 'leave'): void
(e: 'afterLeave'): void
(e: 'enter'): void
(e: 'afterEnter'): void
}>()
function onClose() {
emit('update:open', false)
}
const { _transY, _popupHeight } = useProvidePopupStore(onClose)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
getBoundingClientRect('.popup-container').then((res) => {
_transY.value = _popupHeight.value = res.height ?? 0
})
applyAnimatedStyle('.popup-container', () => {
'worklet'
return {
'transform': `translateY(${_transY.value}px)`,
'box-shadow': `0px 0px ${_transY.value !== _popupHeight.value ? 10 : 0}px 0px rgba(0, 0, 0, 0.2)`,
}
})
applyAnimatedStyle(`.popup-mask`, () => {
'worklet'
return {
opacity: `${1 - _transY.value / _popupHeight.value}`,
display: `${_transY.value !== _popupHeight.value ? 'flex' : 'none'}`,
}
})
})
function onAfterEnter() {
emit('afterEnter')
}
function onAfterLeave() {
emit('afterLeave')
}
watch(() => props.open, () => {
if (props.open) {
emit('enter')
_transY.value = worklet.timing(0, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterEnter)()
})
}
else {
emit('leave')
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterLeave)()
})
}
})
</script>
<template>
<root-portal>
<view class="popup-mask absolute left-0 top-0 h-screen w-screen" @tap="onClose" />
<view class="popup-container absolute bottom-0 w-screen overflow-hidden bg-white" :class="[rounded && 'rounded-t-5', fullScreen ? 'h-screen' : 'h-70vh']">
<slot />
</view>
</root-portal>
</template>
<style>
.popup-container {
z-index: 999;
transform: translateY(100%);
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
}
.popup-mask {
z-index: 998;
background-color: rgba(0, 0, 0, 0.5);
display: none;
}
</style>
popup-drag-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
const { handlePan } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler :worklet:ongesture="handlePan.name">
<view :class="mergedClass">
<slot />
</view>
</pan-gesture-handler>
</template>
popup-scroll-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
defineProps<{
type: string
}>()
defineEmits(['scrolltolower'])
const { handlePan, shouldPanResponse, shouldScrollViewResponse, adjustDecelerationVelocity, handleScroll } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler
tag="pan"
:worklet:should-response-on-move="shouldPanResponse.name"
:simultaneous-handlers="['scroll']"
:worklet:ongesture="handlePan.name"
>
<vertical-drag-gesture-handler
tag="scroll"
native-view="scroll-view"
:worklet:should-response-on-move="shouldScrollViewResponse.name"
:simultaneous-handlers="['pan']"
>
<scroll-view
:class="mergedClass"
scroll-y
:worklet:adjust-deceleration-velocity="adjustDecelerationVelocity.name"
:worklet:onscrollupdate="handleScroll.name"
type="list"
:show-scrollbar="false"
@scrolltolower="$emit('scrolltolower')"
>
<slot />
</scroll-view>
</vertical-drag-gesture-handler>
</pan-gesture-handler>
</template>
使用方式
<u-popup v-model:open="open">
<u-popup-drag-view v-if="!roomDetail" class="flex-1">
<u-loading />
</u-popup-drag-view>
<u-popup-scroll-view v-else class="flex-1" type="list">
<view class="py-4">
<view
v-for="player in roomDetail?.players"
:key="player.steam"
class="mb-2 flex-row items-center px-4"
@click="onRoomPlayer(player.steam)"
>
<view>
<u-avatar class="h-8 w-8" :src="player.avatar" />
</view>
<view class="ml-2 flex-1 truncate text-sm">
{{ player.name }}
</view>
<view class="ml-2 text-xs text-neutral-500 font-mono">
{{ player.elo }}
</view>
<view class="ml-2" />
</view>
<view class="h-[var(--safe-bottom)]" />
</view>
</u-popup-scroll-view>
</u-popup>
至此,便可以细粒度的调整popup的手势协商
在需要用到scroll-into-view的时候 slot中的元素无法被定位
便可以再次复用usePopupWorkletStore来再次实现定制化的popup-scroll-view
workletMethods的实现中,微信小程序会为具名worklet func 添加 _worklet_factory到包含他的object中 (非具名不会处理) 所以需要全部assgin到mpinstance上
仅在微信小程序测试,多端请自行添加条件编译
首先是一个 worklet.ts 的工具
declare const wx: {
worklet: {
timing: (value: number, options?: { duration?: number, easing?: any }, callback?: Function) => AnimationObject
runOnJS: any
shared: <T>(val: T) => SharedValue<T>
derived: <T>(val: () => T) => SharedValue<T>
Easing: {
in: any
out: any
inOut: any
ease: any
}
}
}
export interface SharedValue<T = any> { value: T }
export type AnimationObject = any
export const worklet = wx.worklet
export function getMpInstance() {
return (getCurrentInstance()?.proxy as any).$scope
}
export interface MpInstance {
applyAnimatedStyle: (selector: string, callback: () => Record<string, any>) => void
}
export function workletValue<T>(val: T) {
return worklet.shared(val)
}
export function workletDerived<T>(val: () => T) {
return worklet.derived(val)
}
export function workletMethods<T extends { [key: string]: Function }>(methods: T) {
const mpInstance = getMpInstance()
for (const key in methods)
mpInstance[key] = methods[key]
return methods
}
export function useApplyAnimatedStyle() {
const mpInstance = getMpInstance()
return (selector: string, callback: () => Record<string, any>) => {
mpInstance.applyAnimatedStyle(selector, callback)
}
}
export const GestureState = {
POSSIBLE: 0, // 0 此时手势未识别,如 panDown等
BEGIN: 1, // 1 手势已识别
ACTIVE: 2, // 2 连续手势活跃状态
END: 3, // 3 手势终止
CANCELLED: 4, // 4 手势取消,
}
export const ScrollState = {
scrollStart: 0,
scrollUpdate: 1,
scrollEnd: 2,
}
使用案例
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
const props = defineProps<{
open: boolean
}>()
const _height = workletValue(0)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
applyAnimatedStyle('.collapse-item', () => {
'worklet'
return {
height: _height.value < 0 ? 'auto' : `${_height.value}px`,
}
})
})
watch(() => props.open, () => {
getBoundingClientRect('.collapse-content')
.then(rect => rect.height ?? 0)
.then((height) => {
if (!props.open) {
if (_height.value === -1)
_height.value = height
_height.value = worklet.timing(0, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) })
}
else {
_height.value = worklet.timing(height, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) }, () => {
'worklet'
if (_height.value === height)
_height.value = -1
})
}
})
})
</script>
<template>
<view class="collapse-item block overflow-hidden">
<view class="collapse-content block">
<slot />
</view>
</view>
</template>
worklet js 互调 inject popup 示例
popup.ts
const [useProvidePopupStore, usePopupStoreInner] = createInjectionState((close: () => void) => {
const _transY = workletValue(1000)
const _scrollTop = workletValue(0)
const _startPan = workletValue(true)
const _popupHeight = workletValue(1000)
return { _transY, _scrollTop, _startPan, _popupHeight, close }
})
export { useProvidePopupStore }
export function usePopupStore() {
const popupStore = usePopupStoreInner()
if (popupStore == null)
throw new Error('Please call `useProvidePopupStore` on the appropriate parent component')
return popupStore
}
export function usePopupWorkletMethods() {
const { _popupHeight, _scrollTop, _startPan, _transY, close } = usePopupStore()
return workletMethods({
openPopup() {
'worklet'
_transY.value = worklet.timing(0, { duration: 200 })
},
closePopup() {
'worklet'
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 })
worklet.runOnJS(close)()
},
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
'worklet'
return _startPan.value
},
shouldScrollViewResponse(pointerEvent: any) {
'worklet'
// transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效
if (_transY.value > 0)
return false
const scrollTop = _scrollTop.value
const { deltaY } = pointerEvent
// deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效
const result = scrollTop <= 0 && deltaY > 0
_startPan.value = result
return !result
},
handlePan(gestureEvent: any) {
'worklet'
if (gestureEvent.state === GestureState.ACTIVE) {
const curPosition = _transY.value
const destination = Math.max(0, curPosition + gestureEvent.deltaY)
if (curPosition === destination)
return
_transY.value = destination
}
if (
gestureEvent.state === GestureState.END
|| gestureEvent.state === GestureState.CANCELLED
) {
if (gestureEvent.velocityY > 500 && _transY.value > 50)
this.closePopup()
else if (_transY.value > _popupHeight.value / 2)
this.closePopup()
else
this.openPopup()
}
},
adjustDecelerationVelocity(velocity: number) {
'worklet'
const scrollTop = _scrollTop.value
return scrollTop <= 0 ? 0 : velocity
},
handleScroll(evt: any) {
'worklet'
_scrollTop.value = evt.detail.scrollTop
},
})
}
popup.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
import { useProvidePopupStore } from './popup'
const props = withDefaults(defineProps<{
open: boolean
fullScreen?: boolean
rounded?: boolean
}>(), {
fullScreen: false,
rounded: true,
})
const emit = defineEmits<{
(e: 'update:open', val: boolean): void
(e: 'scrolltolower'): void
(e: 'leave'): void
(e: 'afterLeave'): void
(e: 'enter'): void
(e: 'afterEnter'): void
}>()
function onClose() {
emit('update:open', false)
}
const { _transY, _popupHeight } = useProvidePopupStore(onClose)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
getBoundingClientRect('.popup-container').then((res) => {
_transY.value = _popupHeight.value = res.height ?? 0
})
applyAnimatedStyle('.popup-container', () => {
'worklet'
return {
'transform': `translateY(${_transY.value}px)`,
'box-shadow': `0px 0px ${_transY.value !== _popupHeight.value ? 10 : 0}px 0px rgba(0, 0, 0, 0.2)`,
}
})
applyAnimatedStyle(`.popup-mask`, () => {
'worklet'
return {
opacity: `${1 - _transY.value / _popupHeight.value}`,
display: `${_transY.value !== _popupHeight.value ? 'flex' : 'none'}`,
}
})
})
function onAfterEnter() {
emit('afterEnter')
}
function onAfterLeave() {
emit('afterLeave')
}
watch(() => props.open, () => {
if (props.open) {
emit('enter')
_transY.value = worklet.timing(0, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterEnter)()
})
}
else {
emit('leave')
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterLeave)()
})
}
})
</script>
<template>
<root-portal>
<view class="popup-mask absolute left-0 top-0 h-screen w-screen" @tap="onClose" />
<view class="popup-container absolute bottom-0 w-screen overflow-hidden bg-white" :class="[rounded && 'rounded-t-5', fullScreen ? 'h-screen' : 'h-70vh']">
<slot />
</view>
</root-portal>
</template>
<style>
.popup-container {
z-index: 999;
transform: translateY(100%);
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
}
.popup-mask {
z-index: 998;
background-color: rgba(0, 0, 0, 0.5);
display: none;
}
</style>
popup-drag-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
const { handlePan } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler :worklet:ongesture="handlePan.name">
<view :class="mergedClass">
<slot />
</view>
</pan-gesture-handler>
</template>
popup-scroll-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
defineProps<{
type: string
}>()
defineEmits(['scrolltolower'])
const { handlePan, shouldPanResponse, shouldScrollViewResponse, adjustDecelerationVelocity, handleScroll } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler
tag="pan"
:worklet:should-response-on-move="shouldPanResponse.name"
:simultaneous-handlers="['scroll']"
:worklet:ongesture="handlePan.name"
>
<vertical-drag-gesture-handler
tag="scroll"
native-view="scroll-view"
:worklet:should-response-on-move="shouldScrollViewResponse.name"
:simultaneous-handlers="['pan']"
>
<scroll-view
:class="mergedClass"
scroll-y
:worklet:adjust-deceleration-velocity="adjustDecelerationVelocity.name"
:worklet:onscrollupdate="handleScroll.name"
type="list"
:show-scrollbar="false"
@scrolltolower="$emit('scrolltolower')"
>
<slot />
</scroll-view>
</vertical-drag-gesture-handler>
</pan-gesture-handler>
</template>
使用方式
<u-popup v-model:open="open">
<u-popup-drag-view v-if="!roomDetail" class="flex-1">
<u-loading />
</u-popup-drag-view>
<u-popup-scroll-view v-else class="flex-1" type="list">
<view class="py-4">
<view
v-for="player in roomDetail?.players"
:key="player.steam"
class="mb-2 flex-row items-center px-4"
@click="onRoomPlayer(player.steam)"
>
<view>
<u-avatar class="h-8 w-8" :src="player.avatar" />
</view>
<view class="ml-2 flex-1 truncate text-sm">
{{ player.name }}
</view>
<view class="ml-2 text-xs text-neutral-500 font-mono">
{{ player.elo }}
</view>
<view class="ml-2" />
</view>
<view class="h-[var(--safe-bottom)]" />
</view>
</u-popup-scroll-view>
</u-popup>
至此,便可以细粒度的调整popup的手势协商
在需要用到scroll-into-view的时候 slot中的元素无法被定位
便可以再次复用usePopupWorkletStore来再次实现定制化的popup-scroll-view
workletMethods的实现中,微信小程序会为具名worklet func 添加 _worklet_factory到包含他的object中 (非具名不会处理) 所以需要全部assgin到mpinstance上
收起阅读 »
关于ios运行白屏问题
我这边遇到的情况是图片资源命名错误, 图片从蓝湖导出的是由名字是xxx@2x.png使用时没有改名把@去掉就行了
我这边遇到的情况是图片资源命名错误, 图片从蓝湖导出的是由名字是xxx@2x.png使用时没有改名把@去掉就行了

语雀迁移支付宝云开发:安全与弹性双重升级,开启业务发展新篇章
4月12日凌晨2点,随着最后一条数据流的平稳迁移,语雀正式完成了至支付宝云开发的切换,翻开了业务发展的新篇章。这次迁移不仅是技术架构的升级,更是语雀在安全性和弹性伸缩能力方面的一次飞跃,为未来迎接更大的挑战做好了充足的准备。
一、迁移前景及背景分析
语雀当前用户达千万级,云函数的日调用量在 1kw 左右。过去,由于传统架构的局限,语雀面临着日益增长的安全及弹性挑战。高昂的维护成本、难以适应流量波动的扩容需求,以及不断升级的安全威胁,成为亟需解决的痛点。迁移至支付宝云开发,代表了对这些挑战的积极回应和预见性规划。
二、调研与规划阶段
在详尽的预期效益分析后,语雀选定了支付宝云开发平台,以期提高安全能力和云资源的弹性伸缩能力,同时降低成本。迁移前的准备工作包括对团队的培训和新环境的搭建,确保迁移过程的顺畅。
三、迁移实施和成果
利用语雀强大的文档管理能力,团队管理了整个迁移计划的文档和进度,确保每一步都有据可循。在搭建支付宝云开发环境后,逐一迁移了云函数,并实施了闭环的数据迁移与同步策略。经过严格的测试和验证,迁移成功切流。迁移完成后的语雀小程序,展现出了显著的改进与提升:
安全升级,护航文档创作
安全是任何线上平台的生命线,对于承载大量知识内容的语雀来说更是如此。
通过迁移至支付宝开发,语雀获得了更强大的安全防护能力。支付宝云开发提供了全面的安全解决方案,包括DDoS防护、WAF、漏洞扫描等,有效保障平台抵御各类安全威胁。同时,安全应急响应机制也得到了显著提升,保障用户数据安全和平台稳定运行。
弹性伸缩,资源高效利用
随着用户量和业务规模的增长,平台需要具备灵活的资源伸缩能力。
支付宝云开发的弹性伸缩功能,完美地解决了这一需求。语雀可以根据业务流量、资源使用率等指标,自动调整计算资源,实现资源的按需分配和高效利用。这种弹性伸缩能力不仅降低了机器成本,也大大缩短了扩缩容时间,保障平台稳定应对流量高峰。
易用性强,开发效率提升
支付宝云开发平台为开发者提供了简洁易用的开发环境和丰富的功能组件。
语雀团队通过使用云函数、云数据库等云开发产品,快速实现了业务逻辑的迁移和功能的迭代。云开发平台的低代码特性,降低了开发门槛,让开发者更专注于业务逻辑的实现,显著提升了开发效率。
优化成本,助力业务发展
语雀迁移至支付宝云开发平台后,不仅获得了更强的安全防护能力和弹性伸缩能力,也实现了成本最优控制的目标。资源的高效利用和开发效率的提升,为语雀节省了大量的运营成本,也为业务的快速发展提供了强有力的支持。
四、支付宝云开发:开发者的首选
语雀迁移支付宝云开发平台的成功案例,充分展现了云开发产品的易用性、稳定性和降本提效的优势。对于开发者而言,云开发是构建现代化应用的理想选择,它可以帮助开发者:
▪️ 专注于业务逻辑: 无需关注底层基础设施,专注于业务逻辑的实现。
▪️ 快速构建应用: 丰富的云端能力和完善的工具链,帮助开发者快速构建应用。
▪️ 降低开发成本: 免去服务器运维,降低开发和运维成本。
如果您正在寻找一个高效、稳定、安全的开发平台,支付宝云开发将是您的不二选择。
4月12日凌晨2点,随着最后一条数据流的平稳迁移,语雀正式完成了至支付宝云开发的切换,翻开了业务发展的新篇章。这次迁移不仅是技术架构的升级,更是语雀在安全性和弹性伸缩能力方面的一次飞跃,为未来迎接更大的挑战做好了充足的准备。
一、迁移前景及背景分析
语雀当前用户达千万级,云函数的日调用量在 1kw 左右。过去,由于传统架构的局限,语雀面临着日益增长的安全及弹性挑战。高昂的维护成本、难以适应流量波动的扩容需求,以及不断升级的安全威胁,成为亟需解决的痛点。迁移至支付宝云开发,代表了对这些挑战的积极回应和预见性规划。
二、调研与规划阶段
在详尽的预期效益分析后,语雀选定了支付宝云开发平台,以期提高安全能力和云资源的弹性伸缩能力,同时降低成本。迁移前的准备工作包括对团队的培训和新环境的搭建,确保迁移过程的顺畅。
三、迁移实施和成果
利用语雀强大的文档管理能力,团队管理了整个迁移计划的文档和进度,确保每一步都有据可循。在搭建支付宝云开发环境后,逐一迁移了云函数,并实施了闭环的数据迁移与同步策略。经过严格的测试和验证,迁移成功切流。迁移完成后的语雀小程序,展现出了显著的改进与提升:
安全升级,护航文档创作
安全是任何线上平台的生命线,对于承载大量知识内容的语雀来说更是如此。
通过迁移至支付宝开发,语雀获得了更强大的安全防护能力。支付宝云开发提供了全面的安全解决方案,包括DDoS防护、WAF、漏洞扫描等,有效保障平台抵御各类安全威胁。同时,安全应急响应机制也得到了显著提升,保障用户数据安全和平台稳定运行。
弹性伸缩,资源高效利用
随着用户量和业务规模的增长,平台需要具备灵活的资源伸缩能力。
支付宝云开发的弹性伸缩功能,完美地解决了这一需求。语雀可以根据业务流量、资源使用率等指标,自动调整计算资源,实现资源的按需分配和高效利用。这种弹性伸缩能力不仅降低了机器成本,也大大缩短了扩缩容时间,保障平台稳定应对流量高峰。
易用性强,开发效率提升
支付宝云开发平台为开发者提供了简洁易用的开发环境和丰富的功能组件。
语雀团队通过使用云函数、云数据库等云开发产品,快速实现了业务逻辑的迁移和功能的迭代。云开发平台的低代码特性,降低了开发门槛,让开发者更专注于业务逻辑的实现,显著提升了开发效率。
优化成本,助力业务发展
语雀迁移至支付宝云开发平台后,不仅获得了更强的安全防护能力和弹性伸缩能力,也实现了成本最优控制的目标。资源的高效利用和开发效率的提升,为语雀节省了大量的运营成本,也为业务的快速发展提供了强有力的支持。
四、支付宝云开发:开发者的首选
语雀迁移支付宝云开发平台的成功案例,充分展现了云开发产品的易用性、稳定性和降本提效的优势。对于开发者而言,云开发是构建现代化应用的理想选择,它可以帮助开发者:
▪️ 专注于业务逻辑: 无需关注底层基础设施,专注于业务逻辑的实现。
▪️ 快速构建应用: 丰富的云端能力和完善的工具链,帮助开发者快速构建应用。
▪️ 降低开发成本: 免去服务器运维,降低开发和运维成本。
如果您正在寻找一个高效、稳定、安全的开发平台,支付宝云开发将是您的不二选择。
收起阅读 »
微信小程序、H5、APP百度地图组件的实现(定位、获取定位附近地址列表、移动地图获取地图中心附近地址列表)
最新在开发公司用的百度地图实现,发现社区这方向资料参差不齐,很多都有残缺。下面看我的例子来一起做个百度地图的组件吧(仅限在H5、APP、微信小程序中使用)
在微信小程序中用了取巧的方式,使用uni自带map组件(在微信小程序中,map组件为腾讯地图),同时在百度后台不仅仅需要创建一个Web、iOS、Android的AK,同时还需要再创建一个服务器的AK,主要是用在微信小程序中,用来将地图获取的经纬度进行转换、逆地理编码、POI查询等功能。
在微信管理后台需要开启定位权限,具体看uni.getLocation的要求。
下方代码中会指出哪里使用百度地图Web AK,哪里使用百度地图服务器 AK。请详细查看下方代码注释。
1、创建一个定位公共类
起名:location.js
代码如下:
// 获取位置信息
const getLocation = () => {
return new Promise((resolve, reject) => {
uni.showLoading({title: '获取位置中'});
// #ifdef APP-PLUS || H5
uni.getLocation({
/*
* 目前国内主要有以下三种坐标系:
* WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。
* GCJ02:又称火星坐标系,是由中国国家测绘局制订的地理信息系统的坐标系统。由WGS84坐标系经加密后的坐标系。
* BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。
*
* 如果使用的定位为手机自带定位,那么type 使用 GCJ02。
* 如果是使用百度地图定位,那么type 使用 BD09。(需要在项目中的manifest.json文件配置 百度地图定位的AK)
* 如果是使用高德地图、腾讯地图定位,那么type 使用 GCJ02。(需要在项目中的manifest.json文件配置 高德地图定位的AK)
* 如果是使用谷歌地图定位,那么type 使用 WGS84。(需要在项目中的manifest.json文件配置 谷歌地图定位的AK)
*/
type: 'BD09',
geocode: true,
success: function(data) {
resolve(data)
},
fail: function(error) {
reject(error)
},
complete: function() {
uni.hideLoading();
}
})
// #endif
// #ifdef MP-WEIXIN
uni.getLocation({
/*
* 目前国内主要有以下三种坐标系:
* WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。
* GCJ02:又称火星坐标系,是由中国国家测绘局制订的地理信息系统的坐标系统。由WGS84坐标系经加密后的坐标系。
* BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。
*
* 如果使用的定位为手机自带定位,那么type 使用 GCJ02。
* 如果是使用百度地图定位,那么type 使用 BD09。(需要在项目中的manifest.json文件配置 百度地图定位的AK)
* 如果是使用高德地图、腾讯地图定位,那么type 使用 GCJ02。(需要在项目中的manifest.json文件配置 高德地图定位的AK)
* 如果是使用谷歌地图定位,那么type 使用 WGS84。(需要在项目中的manifest.json文件配置 谷歌地图定位的AK)
*/
type: 'GCJ02',
geocode: true,
success: function(data) {
resolve(data)
},
fail: function(error) {
reject(error)
},
complete: function() {
uni.hideLoading();
}
})
// #endif
})
};
/**
* 检测是否开启定位
*/
const checkOpenGPSServiceByAndroidIOS = () => {
let system = uni.getSystemInfoSync(); // 获取系统信息
if (system.platform === 'android') { // 判断平台
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
uni.showModal({
title: '温馨提示',
content: '您未开启定位服务,请打开定位服务功能,以便获取您的位置!',
showCancel: true,
success() {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
main.startActivity(intent); // 打开系统设置GPS服务页面
}
});
return {s: 'android', b: false};
} else {
return {s: 'android', b: true};
}
} else if (system.platform === 'ios') {
var cllocationManger = plus.ios.import("CLLocationManager");
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
plus.ios.deleteObject(cllocationManger);
if (enable && status != 2) {
// 手机系统的定位已经打开
return {s: 'ios', b: true};
} else {
// 手机系统的定位没有打开
uni.showModal({
title: '提示',
content: '请前往设置-隐私-定位服务打开定位服务功能',
showCancel: true,
success() {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("app-settings:"); // UIApplicationOpenSettingsURLString
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
}
});
return {s: 'ios', b: false};
}
} else {
return {s: 'h5', b: false};
}
}
const getLocationChange = () => {
return new Promise((resolve, reject) => {
uni.startLocationUpdate({
success: function() {
uni.onLocationChange(function (res) {
// 纬度:res.latitude 经度:res.longitude
resolve(res)
});
uni.onLocationChangeError(function (error) {
reject(error);
});
},
fail: function(error) {
reject(error);
},
complete: function() {
// 调用开启小程序接收位置消息 API 完成
}
});
})
}
const stopLocationUpdate = () => {
uni.stopLocationUpdate(function () {
// 关闭监听实时位置变化,前后台都停止消息接收。
})
}
export default {
getLocation: getLocation,
getLocationChange: getLocationChange,
stopLocationUpdate: stopLocationUpdate,
checkOpenGPSServiceByAndroidIOS: checkOpenGPSServiceByAndroidIOS,
}
2、创建一个名字叫 xn-map.vue 的组件 (不知道uniapp 如何创建组件,自行百度一下)
创建完后,记得进行引入:
import xnMap from '@/components/xn-map/xn-map.vue';
Vue.component('xn-map', xnMap);
以下是 xn-map.vue 代码:
代码中的ak记得换成自己的百度地图web ak 和 百度地图服务器ak
<template>
<view class="baidu_map">
<view :style="!showSearch ? 'width: 100%; height: 100%; position: relative;' : 'width: 100%; height: 50%; position: relative;'">
<!-- #ifdef APP-PLUS || H5 -->
<view
id="myMap"
class="myMap"
:prop="dicValue"
:change:prop="bmap.changeValue"
>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<map
class="myMap"
id="myMap"
scale="17"
enable-rotate="false"
show-location="true"
:latitude="maplatitude"
:longitude="mpalongitude"
@regionchange="regionchange"
></map>
<!-- #endif -->
<view class="location" @click="clickLocation">
<u--image :src="sLocationIcon" width="28px" height="28px"></u--image>
</view>
<view v-if="showSearch" class="marking"></view>
</view>
<view v-if="showSearch" class="address_list">
<view class="search">
<u-search :clearabled="true" v-model="sQueryValue" @search="searchQueryValue" @custom="searchQueryValue"></u-search>
</view>
<view class="list">
<view v-for="(item, index) in arrayAddress" :key="index" @click="clickAddressCell(item, index)">
<view class="location_cell">
<view class="centent">
<view class="title">
{{ isEmptyString(item.title) ? '--' : item.title }}
</view>
<view class="sub_title">
{{ item.address }}
</view>
</view>
<view class="right" v-if="item.checked">
<u-icon name="checkbox-mark" color="#2979ff" size="18"></u-icon>
</view>
</view>
<u-line></u-line>
</view>
</view>
<view class="footer" :style="sSearchStyle">
<u-button type="primary" text="保存" @click="clickFootButton"></u-button>
</view>
</view>
</view>
</template>
<script>
import location from '@/tool/location.js';
export default {
name:"crm-map",
props: {
showSearch: {
type: Boolean,
default() {
return false;
}
},
},
data() {
return {
sSearchStyle: "",
dicValue: null,
sLocationIcon: "/static/map_location.png",
sQueryValue: "",
arrayAddress: [],
iLastIndex: 0,
// #ifdef MP-WEIXIN
mapObjs: null,
needSearch: true,
city: "深圳市",
maplatitude: 22.685393,
maplongitude: 113.798274,
// #endif
};
},
mounted() {
let res = this.getSystemInfo();
this.sSearchStyle = 'margin-bottom: ' + res.safeAreaInsets.bottom + 'px;';
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'needOtherAddress', needSerach: this.showSearch, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.clickLocation();
// #endif
},
methods: {
/*
* 在改变 dicValue 时,或许你会有疑惑,为啥字典中都要带一个自动生成 32 位的guid
* 这是因为,如果没有带这个自动生成的guid,在renderjs中,当改变的 dicValue 是同一个值,监听函数是不会被调用的(H5会被调用,APP不会)
* 所以在字典中添加一个 自动生成的 guid,有助于renderjs中监听函数changeValue的生效。
*/
// #ifdef APP-PLUS || H5
// 显示自身定位位置 回调
showMarking() {
if (this.showSearch) {
this.dicValue = { type: 'addEventListener', guid: uni.$u.guid(), }
}
setTimeout(() => {
this.clickLocation();
}, 200)
},
// #endif
// #ifdef MP-WEIXIN
regionchange(val) {
// 在安卓中是 end 事件
if (val.type === 'end' && this.needSearch) {
this.getCenterLatLong();
return;
}
// 在ios中是 regionchange
if (val.type ==='regionchange' && this.needSearch) {
this.getCenterLatLong();
return;
}
},
// 获取中心点位置
getCenterLatLong() {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
const than = this;
this.mapObjs.getCenterLocation({
success: res => {
if (res.errMsg === 'getMapCenterLocation:ok') {
than.circularRegionRetrieval(res.latitude, res.longitude);
} else {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
}
},
fail: res => {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
},
complete: res => {}
})
},
// 设置地图中心点
setMapCenter(latitude, longitude) {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.needSearch = false;
const than = this;
this.mapObjs.moveToLocation({
longitude: longitude,
latitude: latitude,
success: res => {},
fail: res => {},
complete: res => {
setTimeout(() => {
than.needSearch = true;
}, 500)
}
})
},
// 坐标转换
locationConversion(latitude, longitude, sendLocation, sendAddress, dicAddress) {
let sUrl = 'https://api.map.baidu.com/geoconv/v2/';
let params = {
ak: '百度地图服务器ak',
output: 'json',
model: '1',
coords: `${longitude},${latitude}`
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
uni.hideLoading();
if (res.status === 0) {
if (sendLocation) {
this.sendLocation({longitude: res.result[0].x, latitude: res.result[0].y})
}
if (sendAddress) {
let dicData = {
title: dicAddress.title,
address: dicAddress.address,
point: {lat: res.result[0].y, lng: res.result[0].x},
checked: true
}
this.$emit("clickSave", dicData);
}
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.hideLoading();
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 获取当前自身坐标点 地理信息
getMyAddress(latitude, longitude, type) {
let sUrl = 'https://api.map.baidu.com/reverse_geocoding/v3/';
let params = {
ak: '百度地图服务器ak',
output: 'json',
coordtype: type,
location: `${latitude},${longitude}`
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
this.city = res.result.addressComponent.city;
this.updateMyAddress({
address: res.result.formatted_address,
txPoint: res.result.location,
})
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 圆形区域检索
circularRegionRetrieval(latitude, longitude) {
let sUrl = 'https://api.map.baidu.com/place/v2/search';
let params = {
ak: '百度地图服务器ak',
output: 'json',
query: '公司企业$房地产$美食$酒店$购物$生活服务$休闲娱乐$医疗$交通设施$政府机构',
coord_type: 2,
ret_coordtype: 'gcj02ll',
location: `${latitude},${longitude}`,
radius: 1000,
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
let arrayAddressList = [];
for (let item of res.results) {
arrayAddressList.push({
title: item.name,
address: item.address,
point: item.location,
checked: false,
});
}
this.updateAddressList(arrayAddressList);
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 地点输入检索
locationInputRetrieval() {
let sUrl = 'https://api.map.baidu.com/place/v2/suggestion';
let params = {
ak: '百度地图服务器ak',
output: 'json',
ret_coordtype: 'gcj02ll',
query: this.sQueryValue,
region: this.city,
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
let arrayAddressList = [];
let i = 0;
for (let item of res.result) {
arrayAddressList.push({
title: item.name,
address: item.address,
point: item.location,
checked: i === 0 ? true: false
});
i = i + 1;
}
this.updateAddressList(arrayAddressList);
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// #endif
// 更新地址列表
updateAddressList(val) {
this.iLastIndex = 0;
this.arrayAddress = val;
},
// 向父页面发送当前坐标经纬度
sendLocation(val) {
this.$emit("sendLocation", val);
},
// 向父页面发送当前坐标地址信息
updateMyAddress(val) {
this.$emit("sendMyAddress", val);
},
// 获取自身当前经纬度
clickLocation() {
// #ifdef APP-PLUS
let dicInfo = location.checkOpenGPSServiceByAndroidIOS();
if (dicInfo.s !== 'h5') {
if (dicInfo.b) {
location.getLocation().then((resp) => {
this.dicValue = {
type: 'addMyLocationIcon',
longitude: resp.longitude,
latitude: resp.latitude,
guid: uni.$u.guid(),
}
this.sendLocation({longitude: resp.longitude, latitude: resp.latitude});
}).catch((error) => {
uni.showToast({ icon: 'none', title: error.errMsg || '获取位置出错', duration: 3000 });
});
}
} else {
this.dicValue = { type: 'getH5LocationPosition', guid: uni.$u.guid(), }
}
// #endif
// #ifdef H5
this.dicValue = { type: 'getH5LocationPosition', guid: uni.$u.guid(), }
// #endif
// #ifdef MP-WEIXIN
location.getLocation().then((resp) => {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.maplatitude = resp.latitude;
this.maplongitude = resp.longitude;
this.needSearch = false;
const than = this;
this.mapObjs.moveToLocation({
longitude: resp.longitude,
latitude: resp.latitude,
success: res => {
than.getMyAddress(resp.latitude, resp.longitude, 'gcj02ll');
than.circularRegionRetrieval(resp.latitude, resp.longitude);
},
fail: res => {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
},
complete: res => {
setTimeout(() => {
than.needSearch = true;
}, 500)
}
})
this.locationConversion(resp.latitude, resp.longitude, true, false, null);
}).catch((error) => {
uni.showToast({ icon: 'none', title: error.errMsg || '获取位置出错111', duration: 3000 });
});
// #endif
},
// 搜索按钮点击事件
searchQueryValue() {
uni.hideKeyboard();
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'searchQueryValue', sSearchValue: this.sQueryValue, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
this.locationInputRetrieval();
// #endif
},
// 地址信息Cell 点击事件
clickAddressCell(item, index) {
if (this.arrayAddress[index].checked) {
this.iLastIndex = index;
return;
}
if (this.iLastIndex === index) {
this.arrayAddress[this.iLastIndex].checked = !this.arrayAddress[this.iLastIndex].checked;
} else {
this.arrayAddress[this.iLastIndex].checked = !this.arrayAddress[this.iLastIndex].checked;
this.arrayAddress[index].checked = !this.arrayAddress[index].checked;
}
this.iLastIndex = index;
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'moveMapCentre', point: {longitude: item.point.lng, latitude: item.point.lat}, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
this.setMapCenter(item.point.lat, item.point.lng);
// #endif
},
// 保存按钮 点击事件
clickFootButton() {
if (this.arrayAddress.length === 0) {
uni.showToast({ icon: 'none', title: '请选择您要保存的地址', duration: 3000 });
return;
}
// #ifdef APP-PLUS || H5
this.$emit("clickSave", this.arrayAddress[this.iLastIndex]);
// #endif
// #ifdef MP-WEIXIN
uni.showLoading({title:"加载中...", mask: true})
this.locationConversion(this.arrayAddress[this.iLastIndex].point.lat, this.arrayAddress[this.iLastIndex].point.lng, false, true, this.arrayAddress[this.iLastIndex]);
// #endif
},
}
}
</script>
<!-- #ifdef APP-PLUS || H5 -->
<script module="bmap" lang="renderjs">
export default {
data() {
return {
map: null,
locationImg: '/static/location.png', // require("/static/location.png"),
innerValue: null,
needOtherAddress: false,
};
},
mounted(){
// 初始化百度地图
this.initBaiDuMap();
},
methods: {
// 动态创建Script标签
createScript(url) {
return new Promise((resolve,reject) => {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = () => { resolve() };
script.onerror = () => { reject() };
document.head.appendChild(script);
})
},
initBaiDuMap() {
const ak = '百度地图web ak';
if (typeof window.BMap === 'function') {
this.initMap();
} else {
window.init = () => this.initMap();
this.createScript(`https://api.map.baidu.com/api?v=3.0ak=${ak}&callback=init`)
}
},
async initMap() {
// myMap 要渲染地图的view的id
this.map = new BMap.Map("myMap");
this.map.centerAndZoom(this.getPoint(113.804346, 22.691386), 17); // 设置中心点
var scaleCtrl = new BMap.ScaleControl(); // 添加比例尺控件
this.map.addControl(scaleCtrl);
this.$ownerInstance.callMethod("showMarking");
},
// 转换地图坐标点 经度longitude 纬度latitude
getPoint(longitude, latitude) {
return new BMap.Point(longitude, latitude);
},
// 接收Value改变时的数据,用来操作百度地图的API
changeValue(val) {
if (val === null || val === undefined) {
return
}
this.innerValue = val;
if (this.innerValue.type === 'addMyLocationIcon') {
this.addMyLocationIcon(this.innerValue);
} else if (this.innerValue.type === 'getH5LocationPosition') {
this.getH5LocationPosition();
} else if (this.innerValue.type === 'addEventListener') {
this.addBaiduEventListener();
} else if (this.innerValue.type === 'searchQueryValue') {
this.getSearchKeyAddreeList(this.innerValue.sSearchValue);
} else if (this.innerValue.type === 'moveMapCentre') {
this.moveMapCentre(this.innerValue.point);
} else if (this.innerValue.type === 'needOtherAddress') {
this.needOtherAddress = this.innerValue.needSerach;
} else {
}
this.innerValue = null;
},
// 添加自身定位坐标点 并且将地图中心点移动至定位坐标点 经度longitude 纬度latitude
addMyLocationIcon(val) {
// 清除地图上所有覆盖物
this.map.clearOverlays();
let point = this.getPoint(val.longitude, val.latitude);
// 构造函数: 以给定的图像地址和大小创建图标对象实例
var startIcon = new BMap.Icon(
this.locationImg,
new BMap.Size(30, 30)
);
// 设置图标的大小
startIcon.setImageSize(new BMap.Size(30, 30));
var overlay = new BMap.Marker(point, {icon: startIcon});
// 将覆盖物添加到地图中,一个覆盖物实例只能向地图中添加一次
this.map.addOverlay(overlay);
// 将地图中心 移动至定位点
this.map.panTo(point);
this.getLocationMyAddress(point);
},
// 将地图中心点移动某个经纬度上 经度longitude 纬度latitude
moveMapCentre(val) {
let point = this.getPoint(val.longitude, val.latitude);
// 将地图中心 移动至定位点
this.map.panTo(point);
},
// 调用浏览器H5定位接口进行定位 (优先调用浏览器H5定位接口,如果失败会调用IP定位, IP定位:根据用户IP 返回城市级别的定位结果)
getH5LocationPosition() {
/* 关于状态码
* BMAP_STATUS_SUCCESS 检索成功。对应数值“0”
* BMAP_STATUS_CITY_LIST 城市列表。对应数值“1”
* BMAP_STATUS_UNKNOWN_LOCATION 位置结果未知。对应数值“2”
* BMAP_STATUS_UNKNOWN_ROUTE 导航结果未知。对应数值“3”
* BMAP_STATUS_INVALID_KEY 非法密钥。对应数值“4”
* BMAP_STATUS_INVALID_REQUEST 非法请求。对应数值“5”
* BMAP_STATUS_PERMISSION_DENIED 没有权限。对应数值“6”。(自 1.1 新增)
* BMAP_STATUS_SERVICE_UNAVAILABLE 服务不可用。对应数值“7”。(自 1.1 新增)
* BMAP_STATUS_TIMEOUT 超时。对应数值“8”。(自 1.1 新增)
*/
const than = this;
var geolocation = new BMap.Geolocation();
geolocation.getCurrentPosition((r) => {
if(geolocation.getStatus() === BMAP_STATUS_SUCCESS){
than.addMyLocationIcon({longitude: r.point.lng, latitude: r.point.lat});
than.$ownerInstance.callMethod("sendLocation", {longitude: r.point.lng, latitude: r.point.lat});
} else {
switch(geolocation.getStatus()) {
case 2:
alert('位置结果未知,获取位置失败。');
break;
case 3:
alert('导航结果未知,获取位置失败。');
break;
case 4:
alert('非法密钥获取位置失败。');
break;
case 5:
alert('对不起,非法请求位置,获取位置失败。');
break;
case 6:
alert('对不起,当前没有权限,获取位置失败。');
break;
case 7:
alert('对不起,服务不可用,获取位置失败。');
break;
case 8:
alert('对不起,请求超时,获取位置失败。');
break;
default:
alert('定位发生未知错误,请重试!');
break;
}
}
},{ enableHighAccuracy: true }); // 指示浏览器获取高精度的位置,默认false
},
// 添加百度地图拖拽完成事件 回调
addBaiduEventListener() {
const than = this;
this.map.addEventListener('dragend', function (e) {
const center = than.map.getCenter();
than.getLocationAddress(center);
});
},
// 获取经纬度坐标 对应的地址信息
getLocationAddress(center) {
const than = this;
const geoc = new BMap.Geocoder();
geoc.getLocation(center, function(rs) {
if (rs !== undefined && rs !== null) {
var arrayAddressList = [];
arrayAddressList.push({
title: rs.business,
address: rs.address,
point: rs.point,
checked: true,
});
for (let item of rs.surroundingPois) {
arrayAddressList.push({
title: item.title,
address: item.address,
point: item.point,
checked: false
});
}
than.$ownerInstance.callMethod("updateAddressList", arrayAddressList);
}
});
},
// 获取自身经纬度坐标 对应的地址信息
getLocationMyAddress(center) {
const than = this;
const geoc = new BMap.Geocoder();
geoc.getLocation(center, function(rs) {
if (rs !== undefined && rs !== null) {
var dicAddress = {
address: rs.address,
point: rs.point,
};
than.$ownerInstance.callMethod("updateMyAddress", dicAddress);
}
});
},
// 根据搜索关键字获取地址列表信息
getSearchKeyAddreeList(sSearchKey) {
const than = this;
const locationController = new BMap.LocalSearch(this.map, {
onSearchComplete: function(results) {
var arrayAddressList = [];
var data = [];
for (var i = 0; i < results.getCurrentNumPois(); i ++) {
data.push(results.getPoi(i));
}
var i = 0;
for (let item of data) {
arrayAddressList.push({
title: item.title,
address: item.address,
point: item.point,
checked: i === 0 ? true : false,
});
i = i + 1;
}
than.$ownerInstance.callMethod("updateAddressList", arrayAddressList);
if (arrayAddressList.length > 0) {
than.moveMapCentre({longitude: arrayAddressList[0].point.lng, latitude: arrayAddressList[0].point.lat})
}
},
});
locationController.setPageCapacity(10);
locationController.search(sSearchKey);
}
},
}
</script>
<!-- #endif -->
<style scoped>
.baidu_map {
width: 100%;
height: 100%;
}
.baidu_map .myMap {
width: 100%;
height: 100%;
}
.baidu_map .location {
position: absolute;
width: 40px;
height: 40px;
top: 10px;
right: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.baidu_map .marking {
background-image: url('/static/location_dw.png');
position: absolute;
width: 40px;
height: 40px;
left: 50%;
right: 50%;
transform: translate(-50%, -40px);
top: 50%;
/* display: flex;
justify-content: center;
align-items: center; */
}
.baidu_map .address_list {
width: 100%;
height: 50%;
overflow: hidden;
}
.baidu_map .address_list .search {
padding: 10px;
background-color: #ffffff;
}
.baidu_map .address_list .list {
width: 100%;
height: calc(100% - 104px);
overflow: scroll;
}
.baidu_map .address_list .footer {
height: 40px;
background-color: #f7f7fa;
padding: 5px;
}
.location_cell {
display: flex;
padding: 5px 10px 0px 10px;
overflow: hidden;
}
.location_cell .centent {
flex: 1;
overflow: hidden;
}
.location_cell .right {
width: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.location_cell .centent .title {
height: 24px;
font-size: 16px;
font-weight: 500;
line-height: 24px;
margin-bottom: 3px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
}
.location_cell .centent .sub_title {
font-size: 14px;
color: #222222;
text-overflow: ellipsis;
overflow: hidden;
margin-bottom: 5px;
}
</style>
3、在业务页面使用它
代码如下
<template>
<view>
<u-navbar
title="坐标获取"
:border="true"
:placeholder="true"
:pagingEnabled="false"
:autoBack="true">
</u-navbar>
<!-- showSearch 控制显示模式,true为带搜索地址功能,false为纯地图 -->
<view :style="sStyle">
<xn-map
showSearch
@clickSave="clickSave"
@sendLocation="getLocation"
/>
</view>
</view>
</template>
<script >
export default {
data() {
return {
sStyle: "",
}
},
onLoad: function(option) {},
onReady() {
let res = uni.getSystemInfoSync();
let iHeight = res.windowHeight - (44.5 + res.statusBarHeight + res.safeAreaInsets.bottom);
this.sStyle = 'width: 100vw; height: ' + iHeight + 'px;';
},
methods: {
clickSave(val) {
console.log('选定了地址');
console.log(val);
},
getLocation(val) {
console.log('获取到定位信息');
console.log(val);
}
},
}
</script>
<style scoped>
</style>
上面的百度地图有2种模式,一种就是纯地图展示(含定位功能,定位功能需要自己去将项目中的manifest.json,进行配置百度地图的ak),不展示启用搜索选地址功能。
一种就是地图加地址检索和选地址功能。
代码里面都有注释,详细的去看,保证你会了解。
最新在开发公司用的百度地图实现,发现社区这方向资料参差不齐,很多都有残缺。下面看我的例子来一起做个百度地图的组件吧(仅限在H5、APP、微信小程序中使用)
在微信小程序中用了取巧的方式,使用uni自带map组件(在微信小程序中,map组件为腾讯地图),同时在百度后台不仅仅需要创建一个Web、iOS、Android的AK,同时还需要再创建一个服务器的AK,主要是用在微信小程序中,用来将地图获取的经纬度进行转换、逆地理编码、POI查询等功能。
在微信管理后台需要开启定位权限,具体看uni.getLocation的要求。
下方代码中会指出哪里使用百度地图Web AK,哪里使用百度地图服务器 AK。请详细查看下方代码注释。
1、创建一个定位公共类
起名:location.js
代码如下:
// 获取位置信息
const getLocation = () => {
return new Promise((resolve, reject) => {
uni.showLoading({title: '获取位置中'});
// #ifdef APP-PLUS || H5
uni.getLocation({
/*
* 目前国内主要有以下三种坐标系:
* WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。
* GCJ02:又称火星坐标系,是由中国国家测绘局制订的地理信息系统的坐标系统。由WGS84坐标系经加密后的坐标系。
* BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。
*
* 如果使用的定位为手机自带定位,那么type 使用 GCJ02。
* 如果是使用百度地图定位,那么type 使用 BD09。(需要在项目中的manifest.json文件配置 百度地图定位的AK)
* 如果是使用高德地图、腾讯地图定位,那么type 使用 GCJ02。(需要在项目中的manifest.json文件配置 高德地图定位的AK)
* 如果是使用谷歌地图定位,那么type 使用 WGS84。(需要在项目中的manifest.json文件配置 谷歌地图定位的AK)
*/
type: 'BD09',
geocode: true,
success: function(data) {
resolve(data)
},
fail: function(error) {
reject(error)
},
complete: function() {
uni.hideLoading();
}
})
// #endif
// #ifdef MP-WEIXIN
uni.getLocation({
/*
* 目前国内主要有以下三种坐标系:
* WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。
* GCJ02:又称火星坐标系,是由中国国家测绘局制订的地理信息系统的坐标系统。由WGS84坐标系经加密后的坐标系。
* BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。
*
* 如果使用的定位为手机自带定位,那么type 使用 GCJ02。
* 如果是使用百度地图定位,那么type 使用 BD09。(需要在项目中的manifest.json文件配置 百度地图定位的AK)
* 如果是使用高德地图、腾讯地图定位,那么type 使用 GCJ02。(需要在项目中的manifest.json文件配置 高德地图定位的AK)
* 如果是使用谷歌地图定位,那么type 使用 WGS84。(需要在项目中的manifest.json文件配置 谷歌地图定位的AK)
*/
type: 'GCJ02',
geocode: true,
success: function(data) {
resolve(data)
},
fail: function(error) {
reject(error)
},
complete: function() {
uni.hideLoading();
}
})
// #endif
})
};
/**
* 检测是否开启定位
*/
const checkOpenGPSServiceByAndroidIOS = () => {
let system = uni.getSystemInfoSync(); // 获取系统信息
if (system.platform === 'android') { // 判断平台
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
uni.showModal({
title: '温馨提示',
content: '您未开启定位服务,请打开定位服务功能,以便获取您的位置!',
showCancel: true,
success() {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
main.startActivity(intent); // 打开系统设置GPS服务页面
}
});
return {s: 'android', b: false};
} else {
return {s: 'android', b: true};
}
} else if (system.platform === 'ios') {
var cllocationManger = plus.ios.import("CLLocationManager");
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
plus.ios.deleteObject(cllocationManger);
if (enable && status != 2) {
// 手机系统的定位已经打开
return {s: 'ios', b: true};
} else {
// 手机系统的定位没有打开
uni.showModal({
title: '提示',
content: '请前往设置-隐私-定位服务打开定位服务功能',
showCancel: true,
success() {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("app-settings:"); // UIApplicationOpenSettingsURLString
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
}
});
return {s: 'ios', b: false};
}
} else {
return {s: 'h5', b: false};
}
}
const getLocationChange = () => {
return new Promise((resolve, reject) => {
uni.startLocationUpdate({
success: function() {
uni.onLocationChange(function (res) {
// 纬度:res.latitude 经度:res.longitude
resolve(res)
});
uni.onLocationChangeError(function (error) {
reject(error);
});
},
fail: function(error) {
reject(error);
},
complete: function() {
// 调用开启小程序接收位置消息 API 完成
}
});
})
}
const stopLocationUpdate = () => {
uni.stopLocationUpdate(function () {
// 关闭监听实时位置变化,前后台都停止消息接收。
})
}
export default {
getLocation: getLocation,
getLocationChange: getLocationChange,
stopLocationUpdate: stopLocationUpdate,
checkOpenGPSServiceByAndroidIOS: checkOpenGPSServiceByAndroidIOS,
}
2、创建一个名字叫 xn-map.vue 的组件 (不知道uniapp 如何创建组件,自行百度一下)
创建完后,记得进行引入:
import xnMap from '@/components/xn-map/xn-map.vue';
Vue.component('xn-map', xnMap);
以下是 xn-map.vue 代码:
代码中的ak记得换成自己的百度地图web ak 和 百度地图服务器ak
<template>
<view class="baidu_map">
<view :style="!showSearch ? 'width: 100%; height: 100%; position: relative;' : 'width: 100%; height: 50%; position: relative;'">
<!-- #ifdef APP-PLUS || H5 -->
<view
id="myMap"
class="myMap"
:prop="dicValue"
:change:prop="bmap.changeValue"
>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<map
class="myMap"
id="myMap"
scale="17"
enable-rotate="false"
show-location="true"
:latitude="maplatitude"
:longitude="mpalongitude"
@regionchange="regionchange"
></map>
<!-- #endif -->
<view class="location" @click="clickLocation">
<u--image :src="sLocationIcon" width="28px" height="28px"></u--image>
</view>
<view v-if="showSearch" class="marking"></view>
</view>
<view v-if="showSearch" class="address_list">
<view class="search">
<u-search :clearabled="true" v-model="sQueryValue" @search="searchQueryValue" @custom="searchQueryValue"></u-search>
</view>
<view class="list">
<view v-for="(item, index) in arrayAddress" :key="index" @click="clickAddressCell(item, index)">
<view class="location_cell">
<view class="centent">
<view class="title">
{{ isEmptyString(item.title) ? '--' : item.title }}
</view>
<view class="sub_title">
{{ item.address }}
</view>
</view>
<view class="right" v-if="item.checked">
<u-icon name="checkbox-mark" color="#2979ff" size="18"></u-icon>
</view>
</view>
<u-line></u-line>
</view>
</view>
<view class="footer" :style="sSearchStyle">
<u-button type="primary" text="保存" @click="clickFootButton"></u-button>
</view>
</view>
</view>
</template>
<script>
import location from '@/tool/location.js';
export default {
name:"crm-map",
props: {
showSearch: {
type: Boolean,
default() {
return false;
}
},
},
data() {
return {
sSearchStyle: "",
dicValue: null,
sLocationIcon: "/static/map_location.png",
sQueryValue: "",
arrayAddress: [],
iLastIndex: 0,
// #ifdef MP-WEIXIN
mapObjs: null,
needSearch: true,
city: "深圳市",
maplatitude: 22.685393,
maplongitude: 113.798274,
// #endif
};
},
mounted() {
let res = this.getSystemInfo();
this.sSearchStyle = 'margin-bottom: ' + res.safeAreaInsets.bottom + 'px;';
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'needOtherAddress', needSerach: this.showSearch, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.clickLocation();
// #endif
},
methods: {
/*
* 在改变 dicValue 时,或许你会有疑惑,为啥字典中都要带一个自动生成 32 位的guid
* 这是因为,如果没有带这个自动生成的guid,在renderjs中,当改变的 dicValue 是同一个值,监听函数是不会被调用的(H5会被调用,APP不会)
* 所以在字典中添加一个 自动生成的 guid,有助于renderjs中监听函数changeValue的生效。
*/
// #ifdef APP-PLUS || H5
// 显示自身定位位置 回调
showMarking() {
if (this.showSearch) {
this.dicValue = { type: 'addEventListener', guid: uni.$u.guid(), }
}
setTimeout(() => {
this.clickLocation();
}, 200)
},
// #endif
// #ifdef MP-WEIXIN
regionchange(val) {
// 在安卓中是 end 事件
if (val.type === 'end' && this.needSearch) {
this.getCenterLatLong();
return;
}
// 在ios中是 regionchange
if (val.type ==='regionchange' && this.needSearch) {
this.getCenterLatLong();
return;
}
},
// 获取中心点位置
getCenterLatLong() {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
const than = this;
this.mapObjs.getCenterLocation({
success: res => {
if (res.errMsg === 'getMapCenterLocation:ok') {
than.circularRegionRetrieval(res.latitude, res.longitude);
} else {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
}
},
fail: res => {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
},
complete: res => {}
})
},
// 设置地图中心点
setMapCenter(latitude, longitude) {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.needSearch = false;
const than = this;
this.mapObjs.moveToLocation({
longitude: longitude,
latitude: latitude,
success: res => {},
fail: res => {},
complete: res => {
setTimeout(() => {
than.needSearch = true;
}, 500)
}
})
},
// 坐标转换
locationConversion(latitude, longitude, sendLocation, sendAddress, dicAddress) {
let sUrl = 'https://api.map.baidu.com/geoconv/v2/';
let params = {
ak: '百度地图服务器ak',
output: 'json',
model: '1',
coords: `${longitude},${latitude}`
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
uni.hideLoading();
if (res.status === 0) {
if (sendLocation) {
this.sendLocation({longitude: res.result[0].x, latitude: res.result[0].y})
}
if (sendAddress) {
let dicData = {
title: dicAddress.title,
address: dicAddress.address,
point: {lat: res.result[0].y, lng: res.result[0].x},
checked: true
}
this.$emit("clickSave", dicData);
}
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.hideLoading();
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 获取当前自身坐标点 地理信息
getMyAddress(latitude, longitude, type) {
let sUrl = 'https://api.map.baidu.com/reverse_geocoding/v3/';
let params = {
ak: '百度地图服务器ak',
output: 'json',
coordtype: type,
location: `${latitude},${longitude}`
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
this.city = res.result.addressComponent.city;
this.updateMyAddress({
address: res.result.formatted_address,
txPoint: res.result.location,
})
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 圆形区域检索
circularRegionRetrieval(latitude, longitude) {
let sUrl = 'https://api.map.baidu.com/place/v2/search';
let params = {
ak: '百度地图服务器ak',
output: 'json',
query: '公司企业$房地产$美食$酒店$购物$生活服务$休闲娱乐$医疗$交通设施$政府机构',
coord_type: 2,
ret_coordtype: 'gcj02ll',
location: `${latitude},${longitude}`,
radius: 1000,
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
let arrayAddressList = [];
for (let item of res.results) {
arrayAddressList.push({
title: item.name,
address: item.address,
point: item.location,
checked: false,
});
}
this.updateAddressList(arrayAddressList);
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// 地点输入检索
locationInputRetrieval() {
let sUrl = 'https://api.map.baidu.com/place/v2/suggestion';
let params = {
ak: '百度地图服务器ak',
output: 'json',
ret_coordtype: 'gcj02ll',
query: this.sQueryValue,
region: this.city,
}
uni.$u.http.get(sUrl, {params: params}).then(res => {
if (res.status === 0) {
let arrayAddressList = [];
let i = 0;
for (let item of res.result) {
arrayAddressList.push({
title: item.name,
address: item.address,
point: item.location,
checked: i === 0 ? true: false
});
i = i + 1;
}
this.updateAddressList(arrayAddressList);
} else {
uni.showToast({ icon: 'none', title: res.message || '获取位置信息出错', duration: 3000 });
}
}).catch(err => {
uni.showToast({ icon: 'none', title: err.errMsg || '获取位置信息出错', duration: 3000 });
})
},
// #endif
// 更新地址列表
updateAddressList(val) {
this.iLastIndex = 0;
this.arrayAddress = val;
},
// 向父页面发送当前坐标经纬度
sendLocation(val) {
this.$emit("sendLocation", val);
},
// 向父页面发送当前坐标地址信息
updateMyAddress(val) {
this.$emit("sendMyAddress", val);
},
// 获取自身当前经纬度
clickLocation() {
// #ifdef APP-PLUS
let dicInfo = location.checkOpenGPSServiceByAndroidIOS();
if (dicInfo.s !== 'h5') {
if (dicInfo.b) {
location.getLocation().then((resp) => {
this.dicValue = {
type: 'addMyLocationIcon',
longitude: resp.longitude,
latitude: resp.latitude,
guid: uni.$u.guid(),
}
this.sendLocation({longitude: resp.longitude, latitude: resp.latitude});
}).catch((error) => {
uni.showToast({ icon: 'none', title: error.errMsg || '获取位置出错', duration: 3000 });
});
}
} else {
this.dicValue = { type: 'getH5LocationPosition', guid: uni.$u.guid(), }
}
// #endif
// #ifdef H5
this.dicValue = { type: 'getH5LocationPosition', guid: uni.$u.guid(), }
// #endif
// #ifdef MP-WEIXIN
location.getLocation().then((resp) => {
if (this.mapObjs === undefined || this.mapObjs === null) {
this.mapObjs = uni.createMapContext('myMap', this); // 得到map实例对象
}
this.maplatitude = resp.latitude;
this.maplongitude = resp.longitude;
this.needSearch = false;
const than = this;
this.mapObjs.moveToLocation({
longitude: resp.longitude,
latitude: resp.latitude,
success: res => {
than.getMyAddress(resp.latitude, resp.longitude, 'gcj02ll');
than.circularRegionRetrieval(resp.latitude, resp.longitude);
},
fail: res => {
uni.showToast({ icon: 'none', title: res.errMsg || '获取位置出错', duration: 3000 });
},
complete: res => {
setTimeout(() => {
than.needSearch = true;
}, 500)
}
})
this.locationConversion(resp.latitude, resp.longitude, true, false, null);
}).catch((error) => {
uni.showToast({ icon: 'none', title: error.errMsg || '获取位置出错111', duration: 3000 });
});
// #endif
},
// 搜索按钮点击事件
searchQueryValue() {
uni.hideKeyboard();
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'searchQueryValue', sSearchValue: this.sQueryValue, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
this.locationInputRetrieval();
// #endif
},
// 地址信息Cell 点击事件
clickAddressCell(item, index) {
if (this.arrayAddress[index].checked) {
this.iLastIndex = index;
return;
}
if (this.iLastIndex === index) {
this.arrayAddress[this.iLastIndex].checked = !this.arrayAddress[this.iLastIndex].checked;
} else {
this.arrayAddress[this.iLastIndex].checked = !this.arrayAddress[this.iLastIndex].checked;
this.arrayAddress[index].checked = !this.arrayAddress[index].checked;
}
this.iLastIndex = index;
// #ifdef APP-PLUS || H5
this.dicValue = { type: 'moveMapCentre', point: {longitude: item.point.lng, latitude: item.point.lat}, guid: uni.$u.guid(), };
// #endif
// #ifdef MP-WEIXIN
this.setMapCenter(item.point.lat, item.point.lng);
// #endif
},
// 保存按钮 点击事件
clickFootButton() {
if (this.arrayAddress.length === 0) {
uni.showToast({ icon: 'none', title: '请选择您要保存的地址', duration: 3000 });
return;
}
// #ifdef APP-PLUS || H5
this.$emit("clickSave", this.arrayAddress[this.iLastIndex]);
// #endif
// #ifdef MP-WEIXIN
uni.showLoading({title:"加载中...", mask: true})
this.locationConversion(this.arrayAddress[this.iLastIndex].point.lat, this.arrayAddress[this.iLastIndex].point.lng, false, true, this.arrayAddress[this.iLastIndex]);
// #endif
},
}
}
</script>
<!-- #ifdef APP-PLUS || H5 -->
<script module="bmap" lang="renderjs">
export default {
data() {
return {
map: null,
locationImg: '/static/location.png', // require("/static/location.png"),
innerValue: null,
needOtherAddress: false,
};
},
mounted(){
// 初始化百度地图
this.initBaiDuMap();
},
methods: {
// 动态创建Script标签
createScript(url) {
return new Promise((resolve,reject) => {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = () => { resolve() };
script.onerror = () => { reject() };
document.head.appendChild(script);
})
},
initBaiDuMap() {
const ak = '百度地图web ak';
if (typeof window.BMap === 'function') {
this.initMap();
} else {
window.init = () => this.initMap();
this.createScript(`https://api.map.baidu.com/api?v=3.0ak=${ak}&callback=init`)
}
},
async initMap() {
// myMap 要渲染地图的view的id
this.map = new BMap.Map("myMap");
this.map.centerAndZoom(this.getPoint(113.804346, 22.691386), 17); // 设置中心点
var scaleCtrl = new BMap.ScaleControl(); // 添加比例尺控件
this.map.addControl(scaleCtrl);
this.$ownerInstance.callMethod("showMarking");
},
// 转换地图坐标点 经度longitude 纬度latitude
getPoint(longitude, latitude) {
return new BMap.Point(longitude, latitude);
},
// 接收Value改变时的数据,用来操作百度地图的API
changeValue(val) {
if (val === null || val === undefined) {
return
}
this.innerValue = val;
if (this.innerValue.type === 'addMyLocationIcon') {
this.addMyLocationIcon(this.innerValue);
} else if (this.innerValue.type === 'getH5LocationPosition') {
this.getH5LocationPosition();
} else if (this.innerValue.type === 'addEventListener') {
this.addBaiduEventListener();
} else if (this.innerValue.type === 'searchQueryValue') {
this.getSearchKeyAddreeList(this.innerValue.sSearchValue);
} else if (this.innerValue.type === 'moveMapCentre') {
this.moveMapCentre(this.innerValue.point);
} else if (this.innerValue.type === 'needOtherAddress') {
this.needOtherAddress = this.innerValue.needSerach;
} else {
}
this.innerValue = null;
},
// 添加自身定位坐标点 并且将地图中心点移动至定位坐标点 经度longitude 纬度latitude
addMyLocationIcon(val) {
// 清除地图上所有覆盖物
this.map.clearOverlays();
let point = this.getPoint(val.longitude, val.latitude);
// 构造函数: 以给定的图像地址和大小创建图标对象实例
var startIcon = new BMap.Icon(
this.locationImg,
new BMap.Size(30, 30)
);
// 设置图标的大小
startIcon.setImageSize(new BMap.Size(30, 30));
var overlay = new BMap.Marker(point, {icon: startIcon});
// 将覆盖物添加到地图中,一个覆盖物实例只能向地图中添加一次
this.map.addOverlay(overlay);
// 将地图中心 移动至定位点
this.map.panTo(point);
this.getLocationMyAddress(point);
},
// 将地图中心点移动某个经纬度上 经度longitude 纬度latitude
moveMapCentre(val) {
let point = this.getPoint(val.longitude, val.latitude);
// 将地图中心 移动至定位点
this.map.panTo(point);
},
// 调用浏览器H5定位接口进行定位 (优先调用浏览器H5定位接口,如果失败会调用IP定位, IP定位:根据用户IP 返回城市级别的定位结果)
getH5LocationPosition() {
/* 关于状态码
* BMAP_STATUS_SUCCESS 检索成功。对应数值“0”
* BMAP_STATUS_CITY_LIST 城市列表。对应数值“1”
* BMAP_STATUS_UNKNOWN_LOCATION 位置结果未知。对应数值“2”
* BMAP_STATUS_UNKNOWN_ROUTE 导航结果未知。对应数值“3”
* BMAP_STATUS_INVALID_KEY 非法密钥。对应数值“4”
* BMAP_STATUS_INVALID_REQUEST 非法请求。对应数值“5”
* BMAP_STATUS_PERMISSION_DENIED 没有权限。对应数值“6”。(自 1.1 新增)
* BMAP_STATUS_SERVICE_UNAVAILABLE 服务不可用。对应数值“7”。(自 1.1 新增)
* BMAP_STATUS_TIMEOUT 超时。对应数值“8”。(自 1.1 新增)
*/
const than = this;
var geolocation = new BMap.Geolocation();
geolocation.getCurrentPosition((r) => {
if(geolocation.getStatus() === BMAP_STATUS_SUCCESS){
than.addMyLocationIcon({longitude: r.point.lng, latitude: r.point.lat});
than.$ownerInstance.callMethod("sendLocation", {longitude: r.point.lng, latitude: r.point.lat});
} else {
switch(geolocation.getStatus()) {
case 2:
alert('位置结果未知,获取位置失败。');
break;
case 3:
alert('导航结果未知,获取位置失败。');
break;
case 4:
alert('非法密钥获取位置失败。');
break;
case 5:
alert('对不起,非法请求位置,获取位置失败。');
break;
case 6:
alert('对不起,当前没有权限,获取位置失败。');
break;
case 7:
alert('对不起,服务不可用,获取位置失败。');
break;
case 8:
alert('对不起,请求超时,获取位置失败。');
break;
default:
alert('定位发生未知错误,请重试!');
break;
}
}
},{ enableHighAccuracy: true }); // 指示浏览器获取高精度的位置,默认false
},
// 添加百度地图拖拽完成事件 回调
addBaiduEventListener() {
const than = this;
this.map.addEventListener('dragend', function (e) {
const center = than.map.getCenter();
than.getLocationAddress(center);
});
},
// 获取经纬度坐标 对应的地址信息
getLocationAddress(center) {
const than = this;
const geoc = new BMap.Geocoder();
geoc.getLocation(center, function(rs) {
if (rs !== undefined && rs !== null) {
var arrayAddressList = [];
arrayAddressList.push({
title: rs.business,
address: rs.address,
point: rs.point,
checked: true,
});
for (let item of rs.surroundingPois) {
arrayAddressList.push({
title: item.title,
address: item.address,
point: item.point,
checked: false
});
}
than.$ownerInstance.callMethod("updateAddressList", arrayAddressList);
}
});
},
// 获取自身经纬度坐标 对应的地址信息
getLocationMyAddress(center) {
const than = this;
const geoc = new BMap.Geocoder();
geoc.getLocation(center, function(rs) {
if (rs !== undefined && rs !== null) {
var dicAddress = {
address: rs.address,
point: rs.point,
};
than.$ownerInstance.callMethod("updateMyAddress", dicAddress);
}
});
},
// 根据搜索关键字获取地址列表信息
getSearchKeyAddreeList(sSearchKey) {
const than = this;
const locationController = new BMap.LocalSearch(this.map, {
onSearchComplete: function(results) {
var arrayAddressList = [];
var data = [];
for (var i = 0; i < results.getCurrentNumPois(); i ++) {
data.push(results.getPoi(i));
}
var i = 0;
for (let item of data) {
arrayAddressList.push({
title: item.title,
address: item.address,
point: item.point,
checked: i === 0 ? true : false,
});
i = i + 1;
}
than.$ownerInstance.callMethod("updateAddressList", arrayAddressList);
if (arrayAddressList.length > 0) {
than.moveMapCentre({longitude: arrayAddressList[0].point.lng, latitude: arrayAddressList[0].point.lat})
}
},
});
locationController.setPageCapacity(10);
locationController.search(sSearchKey);
}
},
}
</script>
<!-- #endif -->
<style scoped>
.baidu_map {
width: 100%;
height: 100%;
}
.baidu_map .myMap {
width: 100%;
height: 100%;
}
.baidu_map .location {
position: absolute;
width: 40px;
height: 40px;
top: 10px;
right: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.baidu_map .marking {
background-image: url('/static/location_dw.png');
position: absolute;
width: 40px;
height: 40px;
left: 50%;
right: 50%;
transform: translate(-50%, -40px);
top: 50%;
/* display: flex;
justify-content: center;
align-items: center; */
}
.baidu_map .address_list {
width: 100%;
height: 50%;
overflow: hidden;
}
.baidu_map .address_list .search {
padding: 10px;
background-color: #ffffff;
}
.baidu_map .address_list .list {
width: 100%;
height: calc(100% - 104px);
overflow: scroll;
}
.baidu_map .address_list .footer {
height: 40px;
background-color: #f7f7fa;
padding: 5px;
}
.location_cell {
display: flex;
padding: 5px 10px 0px 10px;
overflow: hidden;
}
.location_cell .centent {
flex: 1;
overflow: hidden;
}
.location_cell .right {
width: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.location_cell .centent .title {
height: 24px;
font-size: 16px;
font-weight: 500;
line-height: 24px;
margin-bottom: 3px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
}
.location_cell .centent .sub_title {
font-size: 14px;
color: #222222;
text-overflow: ellipsis;
overflow: hidden;
margin-bottom: 5px;
}
</style>
3、在业务页面使用它
代码如下
<template>
<view>
<u-navbar
title="坐标获取"
:border="true"
:placeholder="true"
:pagingEnabled="false"
:autoBack="true">
</u-navbar>
<!-- showSearch 控制显示模式,true为带搜索地址功能,false为纯地图 -->
<view :style="sStyle">
<xn-map
showSearch
@clickSave="clickSave"
@sendLocation="getLocation"
/>
</view>
</view>
</template>
<script >
export default {
data() {
return {
sStyle: "",
}
},
onLoad: function(option) {},
onReady() {
let res = uni.getSystemInfoSync();
let iHeight = res.windowHeight - (44.5 + res.statusBarHeight + res.safeAreaInsets.bottom);
this.sStyle = 'width: 100vw; height: ' + iHeight + 'px;';
},
methods: {
clickSave(val) {
console.log('选定了地址');
console.log(val);
},
getLocation(val) {
console.log('获取到定位信息');
console.log(val);
}
},
}
</script>
<style scoped>
</style>
上面的百度地图有2种模式,一种就是纯地图展示(含定位功能,定位功能需要自己去将项目中的manifest.json,进行配置百度地图的ak),不展示启用搜索选地址功能。
一种就是地图加地址检索和选地址功能。
代码里面都有注释,详细的去看,保证你会了解。

uni-app h5、app、微信小程序 全局水印组件
参考了:https://ask.dcloud.net.cn/article/35955 内容,对代码进行了修改,添加了优化
1、使用者需要在自己想要开始使用全局水印的地方开始写入,一般来说都是在首页index.vue执行的,在App.vue无法实现。本示例举例在 首页的 index.vue进行。
<!-- 水印组件.vue -->
<template>
<!-- #ifdef APP-PLUS -->
<view class="watermark_back">
<canvas class="watermark" canvas-id="watermarkCanvas" id="watermarkCanvas"></canvas>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="wx_watermark">
<view v-for="(item, index) in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]" :key="index">
<view class="wx_watermark-row">
<span class="wx_watermark-col" v-for="(item1, index1) in ['a1','a2','a3','a4','a5','a6']" :key="index1">{{title}}</span>
</view>
</view>
</view>
<!-- #endif -->
</template>
<script>
export default {
name:"crm-watermark",
props: {
title: {
type: String,
default() {
return '';
}
},
},
data() {
return {
};
},
mounted() {
uni.$on('initCrmWatermark', this.initCrmWatermark);
uni.$on('removeCrmWatermark', this.removeCrmWatermark);
},
destroyed() {
uni.$off('initCrmWatermark', this.initCrmWatermark)
uni.$off('removeCrmWatermark', this.removeCrmWatermark)
},
methods: {
initCrmWatermark(msg) {
// #ifdef APP-PLUS
let id = 'crm_watermark_20240423';
if (plus.nativeObj.View.getViewById(id) !== null) {
plus.nativeObj.View.getViewById(id).close();
}
let context = uni.createCanvasContext('watermarkCanvas');
context.rotate(-45 * Math.PI / 180);
context.setFontSize(15);
context.setFillStyle('rgba(200, 200, 200, 0.30)');
context.setTextAlign('left');
context.setTextBaseline('middle');
context.fillText(msg, -83, 86);
context.draw(false, function() {
uni.canvasToTempFilePath({
canvasId: "watermarkCanvas",
width: 126, // 与下方 tarArr position width 保持一致
height: 126, // 与下方 tarArr position height 保持一致
success: function(res) {
let path = res.tempFilePath;
uni.getSystemInfo({
success: function (res) {
// 水印排列行数
let row = Math.floor(res.windowHeight / 126);
let tarArr = [];
for (let i = 0; i < row + 1; i++) {
for (let j = 0; j < row + 1; j++){
tarArr.push({
tag: 'img',
src: path,
position: {
top: (126 * i) + 'px',
left: (126 * j) + 'px',
width: '126px',
height: '126px',
}
});
}
}
var watermarkView = new plus.nativeObj.View(
id,
{top:'0px', left:'0px', right: '0px', bottom: '0px'},
tarArr
);
// 拦截View控件的触屏事件,将事件穿透给下一层view
watermarkView.interceptTouchEvent(false);
watermarkView.show();
context.clearRect(0, 0, 126, 126);
context.draw();
}
});
}
});
});
// #endif
// #ifdef H5
let id = 'crm_watermark_20240423';
if (document.getElementById(id) !== null) {
document.body.removeChild(document.getElementById(id));
}
let canvas = document.createElement('canvas');
canvas.width = 126;
canvas.height = 126;
let canvas2 = canvas.getContext('2d');
canvas2.rotate(-45 * Math.PI / 180);
canvas2.font = '15px Vedana';
canvas2.fillStyle = 'rgba(200, 200, 200, 0.3)';
canvas2.textAlign = 'left';
canvas2.textBaseline = 'middle';
canvas2.fillText(msg, -83, 86);
let div = document.createElement('div');
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '0px';
div.style.left = '0px';
div.style.bottom = '0px';
div.style.right = '0px';
div.style.position = 'fixed';
div.style.zIndex = '100000';
div.style.background = 'url(' + canvas.toDataURL('image/png') + ') left top repeat';
document.body.appendChild(div);
return id;
// #endif
},
// 删除水印(仅APP与H5有效)
removeCrmWatermark() {
// #ifdef APP-PLUS
if (plus.nativeObj.View.getViewById('crm_watermark_20240423') !== null) {
plus.nativeObj.View.getViewById('crm_watermark_20240423').close();
}
// #endif
// #ifdef H5
if (document.getElementById('crm_watermark_20240423') !== null) {
document.body.removeChild(document.getElementById('crm_watermark_20240423'));
}
// #endif
},
},
}
</script>
<style scoped>
/* #ifdef APP-PLUS */
.watermark_back {
position: relative;
width: 126px;
height: 126px;
z-index: -1;
}
.watermark {
position: absolute;
left: -126px;
top: -126px;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.wx_watermark {
position: fixed;
width: 200vw;
height: 150vh;
top: -20vw;
left: -50vw;
color: rgba(200, 200, 200, 0.3);
font-size: 15px;
opacity: 1;
z-index: 1000000; // 放在顶层
pointer-events: none; // 点击穿透,不影响页面交互
transform: rotate(-45deg); // 水印倾斜角度
}
.wx_watermark-col {
display: inline-block;
padding: 50rpx 40rpx;
}
.wx_watermark-row {
white-space: nowrap;
}
.wx_watermark-row:nth-child(2n+1) {
transform: translateX(10%); // 奇偶数行水印错开
}
/* #endif */
</style>
业务代码
<template>
<view>
<!-- 其他业务代码 -->
<!-- 添加水印 app与H5 只需要调用一次,即可配置全局水印 微信小程序需要每个页面都引入 -->
<!-- #ifdef MP-WEIXIN || APP-PLUS -->
<crm-watermark :title="sWatermark"></crm-watermark>
<!-- #endif -->
</view>
</template>
<script>
data() {
return {
// 微信小程序需要每个页面都引入,并且设置水印
// #ifdef MP-WEIXIN || APP-PLUS
sWatermark: "",
// #endif
}
},
methods: {
showWatermark() {
// app与H5 只需要调用一次,即可配置全局水印
// #ifdef APP-PLUS || H5
uni.$emit('initCrmWatermark', sTitle);
// #endif
// 微信小程序需要每个页面都引入,并且设置水印
// #ifdef MP-WEIXIN || APP-PLUS
this.sWatermark = sTitle;
// #endif
},
}
如果渲染出来 在APP水印页面发现多了个 格格不入的水印,请进行 APP-PLUS 中代码的微调,这是正常的。下面让我来解释一下 (H5不会出现这个问题)
在 APP-PLUS 模式下,先是使用 uni.createCanvasContext('watermarkCanvas'),找到页面中的 canvas。并且对 canvas进行设置(如字体、旋转角度、字体颜色、文字对齐方式、文字水平对齐方式、及填充文字,并且进行 X、Y轴设置)
接着 使用 context.draw,进行渲染,这个时候,其实页面渲染成功就会出现一个 水印了,但后续调用 uni.canvasToTempFilePath(把当前画布指定区域的内容导出生成指定大小的图片) 在其中做了 for循环,将画布内容又生产了N个,并且通过plus.nativeObj.View方式加入到了页面中。这个时候页面就会多出一个看起来格格不入的水印,这个水印就是前面提到的 context.draw 的时候页面已经出来一个了。
那么如何解决这个格格不入的水印问题呢,我的方式是覆盖后将其删除。将一个通过context.draw渲染出来的水印,通过大小调整为与 uni.canvasToTempFilePath for 循环出来的 水印大小一致,将其盖在上面,这样虽然第一个水印看起来颜色深了一个点,但其他并无异常。在uni.canvasToTempFilePath for 循环成功后加入 context.clearRect(0, 0, 126, 126); context.draw(); 将其原来绘制的水印进行删除。
具体看我代码注释:下面是关键代码
width: 126, // 与下方 tarArr position width 保持一致
height: 126, // 与下方 tarArr position height 保持一致
context.clearRect(0, 0, 126, 126);
context.draw();
微信小程序由于无法实现全局水印,只能在每个页面都加入该组件,并且使用条件编译
<!-- #ifdef MP-WEIXIN-->
<crm-watermark :title="sWatermark"></crm-watermark>
<!-- #endif -->
// #ifdef MP-WEIXIN
sWatermark: "",
// #endif
// #ifdef MP-WEIXIN
this.sWatermark = sTitle;
// #endif
参考了:https://ask.dcloud.net.cn/article/35955 内容,对代码进行了修改,添加了优化
1、使用者需要在自己想要开始使用全局水印的地方开始写入,一般来说都是在首页index.vue执行的,在App.vue无法实现。本示例举例在 首页的 index.vue进行。
<!-- 水印组件.vue -->
<template>
<!-- #ifdef APP-PLUS -->
<view class="watermark_back">
<canvas class="watermark" canvas-id="watermarkCanvas" id="watermarkCanvas"></canvas>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="wx_watermark">
<view v-for="(item, index) in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]" :key="index">
<view class="wx_watermark-row">
<span class="wx_watermark-col" v-for="(item1, index1) in ['a1','a2','a3','a4','a5','a6']" :key="index1">{{title}}</span>
</view>
</view>
</view>
<!-- #endif -->
</template>
<script>
export default {
name:"crm-watermark",
props: {
title: {
type: String,
default() {
return '';
}
},
},
data() {
return {
};
},
mounted() {
uni.$on('initCrmWatermark', this.initCrmWatermark);
uni.$on('removeCrmWatermark', this.removeCrmWatermark);
},
destroyed() {
uni.$off('initCrmWatermark', this.initCrmWatermark)
uni.$off('removeCrmWatermark', this.removeCrmWatermark)
},
methods: {
initCrmWatermark(msg) {
// #ifdef APP-PLUS
let id = 'crm_watermark_20240423';
if (plus.nativeObj.View.getViewById(id) !== null) {
plus.nativeObj.View.getViewById(id).close();
}
let context = uni.createCanvasContext('watermarkCanvas');
context.rotate(-45 * Math.PI / 180);
context.setFontSize(15);
context.setFillStyle('rgba(200, 200, 200, 0.30)');
context.setTextAlign('left');
context.setTextBaseline('middle');
context.fillText(msg, -83, 86);
context.draw(false, function() {
uni.canvasToTempFilePath({
canvasId: "watermarkCanvas",
width: 126, // 与下方 tarArr position width 保持一致
height: 126, // 与下方 tarArr position height 保持一致
success: function(res) {
let path = res.tempFilePath;
uni.getSystemInfo({
success: function (res) {
// 水印排列行数
let row = Math.floor(res.windowHeight / 126);
let tarArr = [];
for (let i = 0; i < row + 1; i++) {
for (let j = 0; j < row + 1; j++){
tarArr.push({
tag: 'img',
src: path,
position: {
top: (126 * i) + 'px',
left: (126 * j) + 'px',
width: '126px',
height: '126px',
}
});
}
}
var watermarkView = new plus.nativeObj.View(
id,
{top:'0px', left:'0px', right: '0px', bottom: '0px'},
tarArr
);
// 拦截View控件的触屏事件,将事件穿透给下一层view
watermarkView.interceptTouchEvent(false);
watermarkView.show();
context.clearRect(0, 0, 126, 126);
context.draw();
}
});
}
});
});
// #endif
// #ifdef H5
let id = 'crm_watermark_20240423';
if (document.getElementById(id) !== null) {
document.body.removeChild(document.getElementById(id));
}
let canvas = document.createElement('canvas');
canvas.width = 126;
canvas.height = 126;
let canvas2 = canvas.getContext('2d');
canvas2.rotate(-45 * Math.PI / 180);
canvas2.font = '15px Vedana';
canvas2.fillStyle = 'rgba(200, 200, 200, 0.3)';
canvas2.textAlign = 'left';
canvas2.textBaseline = 'middle';
canvas2.fillText(msg, -83, 86);
let div = document.createElement('div');
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '0px';
div.style.left = '0px';
div.style.bottom = '0px';
div.style.right = '0px';
div.style.position = 'fixed';
div.style.zIndex = '100000';
div.style.background = 'url(' + canvas.toDataURL('image/png') + ') left top repeat';
document.body.appendChild(div);
return id;
// #endif
},
// 删除水印(仅APP与H5有效)
removeCrmWatermark() {
// #ifdef APP-PLUS
if (plus.nativeObj.View.getViewById('crm_watermark_20240423') !== null) {
plus.nativeObj.View.getViewById('crm_watermark_20240423').close();
}
// #endif
// #ifdef H5
if (document.getElementById('crm_watermark_20240423') !== null) {
document.body.removeChild(document.getElementById('crm_watermark_20240423'));
}
// #endif
},
},
}
</script>
<style scoped>
/* #ifdef APP-PLUS */
.watermark_back {
position: relative;
width: 126px;
height: 126px;
z-index: -1;
}
.watermark {
position: absolute;
left: -126px;
top: -126px;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.wx_watermark {
position: fixed;
width: 200vw;
height: 150vh;
top: -20vw;
left: -50vw;
color: rgba(200, 200, 200, 0.3);
font-size: 15px;
opacity: 1;
z-index: 1000000; // 放在顶层
pointer-events: none; // 点击穿透,不影响页面交互
transform: rotate(-45deg); // 水印倾斜角度
}
.wx_watermark-col {
display: inline-block;
padding: 50rpx 40rpx;
}
.wx_watermark-row {
white-space: nowrap;
}
.wx_watermark-row:nth-child(2n+1) {
transform: translateX(10%); // 奇偶数行水印错开
}
/* #endif */
</style>
业务代码
<template>
<view>
<!-- 其他业务代码 -->
<!-- 添加水印 app与H5 只需要调用一次,即可配置全局水印 微信小程序需要每个页面都引入 -->
<!-- #ifdef MP-WEIXIN || APP-PLUS -->
<crm-watermark :title="sWatermark"></crm-watermark>
<!-- #endif -->
</view>
</template>
<script>
data() {
return {
// 微信小程序需要每个页面都引入,并且设置水印
// #ifdef MP-WEIXIN || APP-PLUS
sWatermark: "",
// #endif
}
},
methods: {
showWatermark() {
// app与H5 只需要调用一次,即可配置全局水印
// #ifdef APP-PLUS || H5
uni.$emit('initCrmWatermark', sTitle);
// #endif
// 微信小程序需要每个页面都引入,并且设置水印
// #ifdef MP-WEIXIN || APP-PLUS
this.sWatermark = sTitle;
// #endif
},
}
如果渲染出来 在APP水印页面发现多了个 格格不入的水印,请进行 APP-PLUS 中代码的微调,这是正常的。下面让我来解释一下 (H5不会出现这个问题)
在 APP-PLUS 模式下,先是使用 uni.createCanvasContext('watermarkCanvas'),找到页面中的 canvas。并且对 canvas进行设置(如字体、旋转角度、字体颜色、文字对齐方式、文字水平对齐方式、及填充文字,并且进行 X、Y轴设置)
接着 使用 context.draw,进行渲染,这个时候,其实页面渲染成功就会出现一个 水印了,但后续调用 uni.canvasToTempFilePath(把当前画布指定区域的内容导出生成指定大小的图片) 在其中做了 for循环,将画布内容又生产了N个,并且通过plus.nativeObj.View方式加入到了页面中。这个时候页面就会多出一个看起来格格不入的水印,这个水印就是前面提到的 context.draw 的时候页面已经出来一个了。
那么如何解决这个格格不入的水印问题呢,我的方式是覆盖后将其删除。将一个通过context.draw渲染出来的水印,通过大小调整为与 uni.canvasToTempFilePath for 循环出来的 水印大小一致,将其盖在上面,这样虽然第一个水印看起来颜色深了一个点,但其他并无异常。在uni.canvasToTempFilePath for 循环成功后加入 context.clearRect(0, 0, 126, 126); context.draw(); 将其原来绘制的水印进行删除。
具体看我代码注释:下面是关键代码
width: 126, // 与下方 tarArr position width 保持一致
height: 126, // 与下方 tarArr position height 保持一致
context.clearRect(0, 0, 126, 126);
context.draw();
微信小程序由于无法实现全局水印,只能在每个页面都加入该组件,并且使用条件编译
<!-- #ifdef MP-WEIXIN-->
<crm-watermark :title="sWatermark"></crm-watermark>
<!-- #endif -->
// #ifdef MP-WEIXIN
sWatermark: "",
// #endif
// #ifdef MP-WEIXIN
this.sWatermark = sTitle;
// #endif