技术选型:考虑到开发人员掌握的开发相关技术以及多平台适应,目前开发框架首选使用uniapp,在uniapp开发时,可选vue2和vue3,目前vue官方已经不在对vue2进行更新,所以如无特殊情况,应该首选使用vue3。
1. 除了登录接口和注册接口,所有接口需要登录之后才能调用,需要使用到token
2. 登录时密码需要加密,可以使用crypto-js这个库对密码加密,否则无法调用成功
3. 开发的软件需要能自定义域名或者ip地址,默认使用https://cloud.nvisual.com/
4. 需要严格按照UI原型及产品需求文档进行开发
5. 如开发中需要使用到ht,需将使用到ht的部分用webView引入
密码加密算法封装
// 首先在项目根目录下打开控制台,执行npm install crypto-js对该库进行下载
import CryptoJS from 'crypto-js';
// 加密
const encrypt = (word) => {
// 判断是否存在ksy,不存在就用定义好的key
const keyStr = 'xxxx'; // 由nvisual公司提供
const key = CryptoJS.enc.Utf8.parse(keyStr);
key.sigBytes = 16;
const srcs = CryptoJS.enc.Utf8.parse(word);
const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
}
//使用样例:encrypt('123456')
登录接口调用封装
import { baseUrl } from './apiConfig.js' // baseUrl为https://cloud.nvisual.com/diagramApi/wapi/
import controller from '../controller/index.js'
const login = (data) => {
// data必须包含username(用户名),password(密码), address可选
let address
// 正则验证是否是ip地址或者域名
if (/^(?:[hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)((?:[A-Za-z0-9-~]+\.)+[A-Za-z0-9-~]+)(?::\d{1,5})?(?:\/[^\s]*)?$/.test(data.address) ||
/(?:https?|ftp):\/\/(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?/.test(data.address)
) {
// 验证是否是一个地址
const last = data.address?.slice(-1)
const baseApi = last === '/' ? data.address : data.address + '/'
address = baseApi + 'diagramApi/wapi/'
} else {
address = baseUrl
}
return new Promise((resolve, reject) => {
uni.request({
url: `${address}v1/authenticate`, // 获取到完整的登录接口
method: 'POST',
data: {
...data,
password: controller.cryptoJs.encrypt(data.password), // 对密码进行加密
},
timeout: 10000,
success: (r) => {
if ([200, 5023, 5024].includes(r.data.code)) {
//保存token及用户相关信息
uni.setStorageSync('token', `{"userToken":"${r.data.data?.access_token}","expires":null,"path":"/"}`)
uni.setStorageSync('refreshToken', `{"refreshToken":"${r.data.data?.refresh_token}","expires":null,"path":"/"}`)
uni.setStorageSync('power', r.data.data?.authority)
uni.setStorageSync('userName', r.data.data?.user)
uni.setStorageSync('userId', r.data.data?.userId)
uni.setStorageSync('username', data.username)
uni.setStorageSync('password', data.password)
if (/^(?:[hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)((?:[A-Za-z0-9-~]+\.)+[A-Za-z0-9-~]+)(?::\d{1,5})?(?:\/[^\s]*)?$/.test(data.address) ||
/(?:https?|ftp):\/\/(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?/.test(data.address)
) {
// 验证是否是一个地址
const last = data.address?.slice(-1)
const address = last === '/' ? data.address : data.address + '/'
// 将输入的地址保存到本地,下次用户不需要在输入,但可以更改
uni.setStorageSync('apiAddress', address)
}
}
resolve(r.data)
},
fail: (err) => {
console.log('失败的理由', err);
uni.hideLoading()
uni.showToast({
title: '请求失败',
duration: 2000,
icon: 'none',
})
},
})
})
}
//使用样例:login({username: '张山',password:'123456', address: 'http://release.nvisual.com'})
export default {
login
}
请求接口封装
//nvisual的登录接口token是有过期时间的,如在调用期间token过期了,需要调用refreshToken来重新获取token,将token重新加入请求头中
import { storeToRefs } from 'pinia'
import { baseUrl } from './apiConfig.js' // baseUrl为https://cloud.nvisual.com/diagramApi/wapi/
import { useCommonStore } from '@/store/index.js'
let isRefreshing = false // 是否正在刷新的标记
let requests = [] // 重试队列,每一项将是一个待执行的函数形式
const request = (url, data, method = 'Get') => {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token') && JSON.parse(uni.getStorageSync('token'))
const refreshToken = uni.getStorageSync('refreshToken') && JSON.parse(uni.getStorageSync('refreshToken'))
if (!token) {
// 不存在token时,请自行处理(建议跳到登录界面)
return
}
let requestUrl
const apiAddress = uni.getStorageSync('apiAddress') ? uni.getStorageSync('apiAddress') + 'diagramApi/wapi/' : baseUrl
const isFullPath = url.includes('http')
if (isFullPath) {
// 是否是完整地址
requestUrl = url
} else {
requestUrl = `${apiAddress}${url}`
}
uni.request({
url: requestUrl,
method,
data,
header: {
Authorization: `Bearer ${(token && token.userToken) || ''}`,
'Refresh-Token': (refreshToken && refreshToken.refreshToken) || '',
},
success: (r) => {
const message = (r?.data?.message?.split(':')) || ['200'];
const tokenError = ['5019', '5001', '5020']
const { code } = r.data
let tokenRefresh = false // 是否token过期
if (tokenError.includes(message[0].trim()) || r.statusCode === 207 || code === 5054) {
// 代表token只是过期,只需要重新获取token就好了
tokenRefresh = true
} else {
if (r.data.code === 'token 错误需要退出') {
// 此时必须重新登录,请自行处理(建议跳到登录界面)
return
}
tokenRefresh = false
}
// 有些接口没有标准返回结构, 所以这种情况没有code , 在没有code的情况下, 默认为成功
if (url !== 'v1/refresh_token' && ((code && (code === 5002 || code === 5001)) || tokenRefresh)) {
// 说明token过期了,获取新的token.
resolve(
response({
url: requestUrl,
method,
data,
})
)
} else {
resolve(r.data)
}
if (r.statusCode === 404) {
// 请求未找到,自行处理
}
},
fail: (err) => {
console.log('err', err);
if (uni.getStorageSync('apiAddress')) {
uni.hideLoading()
// 请求出错,自行处理
}
}
})
})
}
const response = (response) => {
// 接下来会在这里进行token过期的逻辑处理
const config = response?.config || response
// 判断一下状态
if (!isRefreshing) {
// 修改状态,进入更新token阶段
isRefreshing = true
// 获取当前的请求
return refreshToken().then(async res => {
/**
* 5122,"找不到刷新令牌"
5125,"用户从另一台计算机登录"
5121,"无效的刷新令牌"
5123,"刷新令牌过期"
*/
if (res.code === 200) {
// 刷新token成功,将最新的token更新到header中,同时保存在localStorage中
uni.setStorageSync('token', `{"userToken":"${res.data.access_token}","expires":null,"path":"/"}`)
uni.setStorageSync('refreshToken', `{"refreshToken":"${res.data.refresh_token}","expires":null,"path":"/"}`)
uni.setStorageSync('power', res.data.authority)
uni.setStorageSync('userName', res.data.user)
uni.setStorageSync('userId', res.data.userId)
uni.setStorageSync('organization', res.data.organization)
// 已经刷新了token,将所有队列中的请求进行重试 reverse()
requests.forEach(cb => cb())
// 完成之后在关闭状态且清空request
isRefreshing = false
requests = []
return request(config.url, config.data, config.method)
} else {
// 重新登录,自行处理(建议跳到登录界面)
}
}).catch(res => {
console.error(res);
uni.hideLoading()
})
} else {
// 正在刷新token
// 将函数放进队列,用一个函数形式来保存,等token刷新后直接执行
return new Promise(resolve => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push(() => {
resolve(request(config.url, config.data, config.method))
})
})
}
}
const refreshToken = () => {
return request('v1/refresh_token', null, 'Post')
}
// 使用示例:
// 1. get请求:request('v1/xxxxx')
// 2. post请求:request('v1/xxxxx', {xxx:xxx,xxx:xxx}, 'Post')
// 3. 其他请求方式参上
export default request
如还需访问模型库等图片资源可如此
import { storeToRefs } from 'pinia'
import { sourceUrl } from '@/request/apiConfig.js' // sourceUrl默认为https://cloud.nvisual.com/
import { useDetailStore } from '@/store/index.js'
const getSourceUrl = (name, folder) => {
const apiAddress = uni.getStorageSync('apiAddress') || sourceUrl
if (folder) {
const detailStore = useDetailStore()
const { cloudDirectory } = storeToRefs(detailStore)
return `${apiAddress}${cloudDirectory.value}/${folder}/${name}`
}
return `${apiAddress}img/nvisual/modelLibrary/${name}`
}
// 使用示例:
// 1. 有文件夹 getSourceUrl('xxx.jpg', 'backgrounds')
// 2. 无文件夹 getSourceUrl('xxx.jpg')
export default getSourceUrl
// cloudDirectory获取来源:
// 1.先登录,在调用v1/global_settings/front_end接口获取
const settingList = await request('v1/global_settings/front_end')
settingList?.forEach(item => {
if (item.name === 'cloud_directory') {
//cloudDirectory = item.value
detailStore.changeCloudDirectory(item.value)
}
})
// 2. 先登录,在调用v1/diagram/xxx接口获取
const diagram = await request('v1/diagram/24000000086788')
//cloudDirectory = diagram.cloud_directory
detailStore.changeCloudDirectory(diagram.cloud_directory)
如在开发中使用到了地图,在app端的地图开发过程中,大部分使用的是gcj02坐标系,而在nvisual系统中,地图使用的是wgs84坐标系,如果需要将app中的数据同步到web端,是需要做坐标转换的,也就是将gcj02坐标转换为wgs84,在这里提供对应的算法:
const PI = 3.14159265358979324;
function transformGCJ2WGS(gcjLon, gcjLat) {
const d = delta(gcjLon, gcjLat)
return [gcjLon - d.lon, gcjLat - d.lat]
}
function delta(lon, lat) {
const a = 6378245.0 // a: 卫星椭球坐标投影到平面地图坐标系的投影因子。
const ee = 0.00669342162296594323 // ee: 椭球的偏心率。
let dLat = transformLat(lon - 105.0, lat - 35.0)
let dLon = transformLon(lon - 105.0, lat - 35.0)
const radLat = lat / 180.0 * PI
let magic = Math.sin(radLat)
magic = 1 - ee * magic * magic
const sqrtMagic = Math.sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI)
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * PI)
return {
lat: dLat,
lon: dLon
}
}
function transformLat(x, y) {
let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0
ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0
return ret
}
function transformLon(x, y) {
let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x))
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0
ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0
return ret
}
export default transformGCJ2WGS
如在app开发中使用到了webView,在webView中必然需要调用接口,但是接口的token如何获取是一个问题,此时就需要使用到webView同app之间的通信,将app端登录获取到的token传入webView端,如下示例:
import { ref, getCurrentInstance } from 'vue'
setup() {
const wv = ref(null) // 定义(app)webview对象节点
const params = ref(null) // 需要传入app端的信息
const { proxy } = getCurrentInstance()
onLoad((option) => {
if (option.id) {
const paramsData = {}
paramsData.id = option.id
paramsData.token = uni.getStorageSync('token')
paramsData.refreshToken = uni.getStorageSync('refreshToken')
paramsData.apiAddress = uni.getStorageSync('apiAddress')
params.value = paramsData
}
})
onReady(() => {
// #ifdef APP-PLUS
var currentWebview = proxy.$scope.$getAppWebview() //此对象相当于html5plus里的plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效
setTimeout(function() {
wv.value = currentWebview.children()[0]
if (params.value.id) {
wv.value.evalJS(`requestData(${JSON.stringify(params.value)})`) // 像app端传输数据
}
}, 1000);
})
}
而在h5页面中,需要监听到app端发送过来的数据,注意1:一般打开网页时就需要请求数据,此时必须要在接收到数据完成后才进行对vue页面的挂载,否则无法获取token,此时可以使用对象来巧妙完成这一步;注意2:在h5页面中必须引入uni-webview-js这个包才能与app端进行通信,否则在h5页面中是没有uni对象的。
index.html页面:
// index.html页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nvisual</title>
<script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
let appSendData = null
// 用来监听数据发送成功在执行挂载
class ListenerSend {
getListenerFn(listenerFn) {
this.listenerFn = listenerFn
}
action() {
this.listenerFn()
}
}
const listener = new ListenerSend()
document.addEventListener('UniAppJSBridgeReady', function () {
uni.getEnv(function (res) {
console.log('当前环境:' + JSON.stringify(res));
});
});
// HTML 接受APP发送过来的消息(APP端)
function requestData(data) {
appSendData = data
// 执行vue的挂载
listener.action()
}
</script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
main.js页面:
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router"
const pinia = createPinia()
const mountApp = () => {
createApp(App).use(router).use(pinia).mount('#app')
}
// 将挂载函数存入
listener.getListenerFn(mountApp)
如还需在h5中动态监听app的消息,可在全局状态管理中定义一个变量,在程序中通过watch来监听数据变化。但是h5监听app数据需在index.html中,所以我们需在app.vue中将改变数据的ation挂载到window对象上,这样在index.html中才能动态改变store中的数据。如下示例:
在index.html中:
// HTML 接受APP发送过来的消息(APP端)
function requestData(data) {
if (data.type === 'init') {
appSendData = data
listener.action()
} else if (data.type === 'event') {
window.globalDetailStore.getSendEventMessage(data)
}
}
在app.vue中:
<script>
import { useDetailStore } from '@/store/index.js'
export default {
setup() {
const detailStore = useDetailStore()
window.globalDetailStore = detailStore
}
}
</script>
在状态管理文件中:
import { defineStore } from 'pinia'
export default defineStore('detailStore', {
state: () => {
return {
sendEventMessage: {}, // 接收传过来的数据
}
},
actions: {
getSendEventMessage(message) {
this.sendEventMessage = message
},
},
})
在需要监听的页面中:
watch(() => detailStore.sendEventMessage, (newValue) => {
if (newValue) {
if (newValue.data.type === 'onmarked') {
// 监听到了扫码结果
onmarked(newValue)
} else if (newValue.data.type === 'endScan') {
// 监听到了扫码结束
} else if (newValue.data.type === 'renderDetail') {
renderDetailRe(newValue.data.value)
lastRenderId = newValue.data.value
}
}
})