菜单

APP开发指南

下载

app开发说明

技术选型:考虑到开发人员掌握的开发相关技术以及多平台适应,目前开发框架首选使用uniapp,在uniapp开发时,可选vue2和vue3,目前vue官方已经不在对vue2进行更新,所以如无特殊情况,应该首选使用vue3。

注意事项:

复制代码
	1. 除了登录接口和注册接口,所有接口需要登录之后才能调用,需要使用到token
	2. 登录时密码需要加密,可以使用crypto-js这个库对密码加密,否则无法调用成功
	3. 开发的软件需要能自定义域名或者ip地址,默认使用https://cloud.nvisual.com/
	4. 需要严格按照UI原型及产品需求文档进行开发
	5. 如开发中需要使用到ht,需将使用到ht的部分用webView引入

对部分功能封装参考:

  1. 密码加密算法封装

    js 复制代码
    // 首先在项目根目录下打开控制台,执行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')
  2. 登录接口调用封装

    js 复制代码
    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
    }
  3. 请求接口封装

js 复制代码
//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
  1. 如还需访问模型库等图片资源可如此

    js 复制代码
    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地图开发说明

如在开发中使用到了地图,在app端的地图开发过程中,大部分使用的是gcj02坐标系,而在nvisual系统中,地图使用的是wgs84坐标系,如果需要将app中的数据同步到web端,是需要做坐标转换的,也就是将gcj02坐标转换为wgs84,在这里提供对应的算法:

js 复制代码
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

webview开发说明

如在app开发中使用到了webView,在webView中必然需要调用接口,但是接口的token如何获取是一个问题,此时就需要使用到webView同app之间的通信,将app端登录获取到的token传入webView端,如下示例:

js 复制代码
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页面:

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页面:

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中:

js 复制代码
// 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中:

js 复制代码
<script>
import { useDetailStore } from '@/store/index.js'
export default {
  setup() {
    const detailStore = useDetailStore()
    window.globalDetailStore = detailStore
  }
}
</script>

在状态管理文件中:

js 复制代码
import { defineStore } from 'pinia'

export default defineStore('detailStore', {
  state: () => {
    return {
      sendEventMessage: {}, // 接收传过来的数据
    }
  },
  actions: {
    getSendEventMessage(message) {
      this.sendEventMessage = message
    },
  },
})

在需要监听的页面中:

js 复制代码
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
    }
  }
})
上一个
二次开发入门介绍
下一个
APP体验模式二次开发对接说明
最近修改: 2024-09-13