Merge branch 'fanpeng' into 'develop'

Fanpeng

See merge request StarlockTeam/wx-starlock!36
This commit is contained in:
范鹏 2025-04-08 07:41:06 +00:00
commit 1c5d8ed714
23 changed files with 886 additions and 471 deletions

View File

@ -8,7 +8,8 @@ module.exports = {
globals: {
uni: 'writable',
getApp: 'writable',
wx: 'writable'
wx: 'writable',
getCurrentPages: 'writable'
},
// 指定如何解析语法
parser: 'vue-eslint-parser',
@ -41,17 +42,15 @@ module.exports = {
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['error', { max: 1 }], // 不允许多个空行
'prefer-const': 'off', // 使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
'no-use-before-define': 'error', // 禁止在 函数/类/变量 定义之前使用它们
'no-use-before-define': 'off', // 禁止在 函数/类/变量 定义之前使用它们
'no-irregular-whitespace': 'off', // 禁止不规则的空白
'no-undef': 'error', // 禁止使用未声明的变量
'vue/script-setup-uses-vars': 'off', // 关闭此规则因为它可能会干扰no-undef的检测
'no-unused-vars': 'error', // 禁止出现未使用过的变量
'vue/script-setup-uses-vars': 'error', // 确保script setup中的变量必须正确定义
'vue/no-undef-components': 'off', // 关闭组件未定义的检查
'vue/no-undef-properties': [
'error',
{
ignore: ['getDeviceInfo', 'getBluetoothDevices', 'stopGetBluetoothDevices']
}
], // 忽略特定方法的未定义检查
'vue/no-undef-properties': 'off', // 关闭属性未定义的检查
'vue/no-unused-vars': 'error', // 禁止Vue组件中出现未使用的变量
'import/no-unused-modules': 'off', // 关闭模块导出检查
'import/no-cycle': 0,
'no-nested-ternary': 0,
'import/prefer-default-export': 0,

20
App.vue
View File

@ -39,6 +39,8 @@
this.updateMiniProgram()
//
this.onBluetoothState()
// voip
this.setVoipConfig()
//
const checkResult = await this.checkSetting()
console.log(checkResult)
@ -65,6 +67,24 @@
'initAndListenBluetooth'
]),
...mapActions(useUserStore, ['updateLoginStatus']),
// voip
setVoipConfig() {
const wmpfVoip = requirePlugin('wmpf-voip').default
wmpfVoip.setUIConfig({
btnText: '去开门',
customBoxHeight: '75vh',
listenerUI: {
cameraRotation: 270,
objectFit: 'fill',
enableToggleCamera: false
}
})
wmpfVoip.setVoipEndPagePath({
url: '/pages/main/home',
key: 'Call',
routeType: 'switchTab'
})
},
//
updateMiniProgram() {
const updateManager = uni.getUpdateManager()

View File

@ -1,12 +0,0 @@
import request from '../utils/request'
// p2p 模块
// 获取p2pInfo
export function getP2pInfo(data) {
return request({
url: '/v1/tencentYun/getDeviceDetail',
method: 'POST',
data
})
}

12
api/sdk.js Normal file
View File

@ -0,0 +1,12 @@
import request from '../utils/request'
// sdk 模块
// 透传
export function passthrough(data) {
return request({
url: '/passthrough',
method: 'POST',
data
})
}

View File

@ -9,14 +9,17 @@ uni.getSystemInfo({
const DEV = {
name: 'dev',
baseUrl: 'https://dev.lock.star-lock.cn/api',
webviewBaseUrl: 'https://dev.lock.star-lock.cn',
appName: '星星锁Lite',
baseUrl: 'https://lock.dev.star-lock.cn/api',
webviewBaseUrl: 'https://lock.dev.star-lock.cn',
version,
buildNumber
}
const PRE = {
name: 'pre',
baseUrl: 'https://pre.lock.star-lock.cn/api',
appName: '星星锁Lite',
baseUrl: 'https://lock.pre.star-lock.cn/api',
webviewBaseUrl: 'https://lock.xhjcn.ltd',
version,
buildNumber
@ -24,6 +27,7 @@ const PRE = {
const XHJ = {
name: 'xhj',
appName: '星星锁Lite',
baseUrl: 'https://lock.xhjcn.ltd/api',
webviewBaseUrl: 'https://lock.xhjcn.ltd',
version,
@ -32,6 +36,7 @@ const XHJ = {
const SKY = {
name: 'sky',
appName: '星星锁Lite',
baseUrl: 'https://lock.skychip.top/api',
webviewBaseUrl: 'https://lock.skychip.top',
version,
@ -40,6 +45,7 @@ const SKY = {
const GE = {
name: 'ge',
appName: '星星锁Lite',
baseUrl: 'http://lock.ge.star-lock.cn/api',
webviewBaseUrl: 'http://lock.ge.star-lock.cn',
version,

28
jsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./*"
]
},
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"strict": true,
"skipLibCheck": true
},
"include": [
"**/*.js",
"**/*.jsx",
"**/*.vue"
],
"exclude": [
"node_modules",
"unpackage",
"dist"
]
}

View File

@ -30,14 +30,14 @@
"provider": "wx1319af22356934bf",
"export": "exportForXp2pPlugin.js"
},
"wechat-p2p-player": {
"version": "latest",
"provider": "wx9e8fbc98ceac2628",
"export": "exportForPlayerPlugin.js"
},
"wmpf-voip": {
"version": "latest",
"provider": "wxf830863afde621eb"
"provider": "wxf830863afde621eb",
"genericsImplementation": {
"call-page-plugin": {
"custombox": "pages/main/customBox"
}
}
}
}
},

View File

@ -16,6 +16,7 @@
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "监控",
"disableScroll": true,
"mp-weixin": {
"usingComponents": {
"iot-p2p-player-with-mjpg": "plugin://xp2p/iot-p2p-player-with-mjpg",
@ -23,8 +24,22 @@
}
}
}
},
{
"path": "authorizeWechat",
"style": {
"navigationBarTitleText": "微信授权",
"disableScroll": true
}
}
]
],
"plugins": {
"wechat-p2p-player": {
"version": "latest",
"provider": "wx9e8fbc98ceac2628",
"export": "exportForPlayerPlugin.js"
}
}
},
{
"root": "pages/addDevice",
@ -560,6 +575,9 @@
{
"path": "pages/main/mine"
},
{
"path": "pages/main/customBox"
},
{
"path": "pages/main/notificationList",
"style": {

View File

@ -12,10 +12,10 @@
</template>
<script setup>
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
const bluetoothStore = useBluetoothStore()
const basicStore = useBasicStore()

View File

@ -134,6 +134,9 @@
address: this.currentLockInfo.position.address
}
}
if (this.currentLockInfo.tencentYunLock) {
params.tencentYunLock = this.currentLockInfo.tencentYunLock
}
const { code, message } = await bindLockAdmin(params)
console.log('添加锁返回', code, message)
if (code === 0) {

View File

@ -1,212 +1,163 @@
<template>
<view>
<view class="mt-4">
<up-steps :current="current" activeColor="#63b8af" class="custom-steps">
<up-steps-item title="设置WiFi" :itemStyle="{ fontSize: '48rpx' }"> </up-steps-item>
<up-steps-item title="连接设备蓝牙" :itemStyle="{ fontSize: '48rpx' }"></up-steps-item>
<up-steps-item title="开始配网" :itemStyle="{ fontSize: '48rpx' }"></up-steps-item>
</up-steps>
<view class="mt-8 mx-8 flex flex-col h-[calc(100vh-300rpx)]">
<view v-if="current === 0">
<view class="text-lg font-bold mb-4">请选择WiFi并输入密码</view>
<view class="pt-2 pb-3 border-b-2 border-b-solid border-gray-200">
<picker
mode="selector"
:range="wifiList"
:value="wifiIndex"
@change="changeWifi"
range-key="SSID"
>
<view class="flex items-center">
<view class="mr-4">WiFi</view>
<view>{{ wifiList[wifiIndex]?.SSID ?? '加载中...' }}</view>
<view class="ml-a">
<up-icon name="arrow-right" size="24rpx"></up-icon>
</view>
</view>
</picker>
</view>
<view class="py-2 border-b-2 border-b-solid border-gray-200 flex items-center">
<view>密码</view>
<view class="flex-1">
<!-- <up-input
:customStyle="{
padding: '0 28rpx',
outline: 'none',
height: '80rpx',
backgroundColor: '#FFFFFF',
border: 0
}"
placeholder-class="!text-base !line-height-[80rpx]"
v-model="password"
placeholder="请输入密码"
:type="showPassword ? 'text' : 'password'"
>
<template #suffix>
<up-icon
:name="showPassword ? 'eye-fill' : 'eye-off'"
size="42rpx"
@click="togglePassword"
></up-icon>
</template>
</up-input> -->
</view>
</view>
</view>
<view v-if="current === 1" class="flex flex-col h-[calc(100vh-230rpx)]">
<view class="text-lg font-bold mb-2">请连接设备蓝牙</view>
<view class="text-[#999999]">已发现设备如下</view>
<scroll-view scroll-y class="flex-1 mt-4 overflow-hidden">
<view
v-for="item in deviceList"
:key="item.deviceId"
@click="connectDevice(item)"
class="bg-[#efedf1] rounded-xl py-4 px-3 mt-2 flex justify-between"
>
<view>{{ item.name }}</view>
<view class="text-[#63b8af]">连接</view>
</view>
</scroll-view>
<view class="flex justify-center items-center mt-2">
<up-loading-icon
size="70rpx"
text="搜索中"
:vertical="true"
textSize="28rpx"
></up-loading-icon>
</view>
</view>
<view v-if="current === 2">
<view class="flex justify-center mt-10">
<image
src="https://oss-lock.xhjcn.ltd/mp/cloud_server.png"
mode="aspectFill"
class="w-200rpx h-200rpx p-4"
></image>
</view>
<view
v-for="(item, index) in stepList"
:key="item"
class="flex items-center mt-4 justify-center w-full"
<view class="mt-18 mx-8 flex flex-col">
<view class="text-lg font-bold mb-4">请选择WiFi并输入密码</view>
<view class="pt-2 pb-3 border-b-2 border-b-solid border-gray-200">
<picker
mode="selector"
:range="wifiList"
:value="wifiIndex"
@change="changeWifi"
range-key="SSID"
>
<view class="flex items-center justify-start w-400rpx">
<up-loading-icon mode="circle" v-if="step < index + 1" size="36rpx"></up-loading-icon>
<up-icon name="checkbox-mark" color="#63b8af" size="36rpx" v-else></up-icon>
<view class="ml-3">{{ item }}</view>
<view class="flex items-center">
<view class="mr-4">WiFi</view>
<view>{{ wifiList[wifiIndex]?.SSID ?? '搜索中...' }}</view>
<view class="ml-a">
<up-icon name="arrow-right" size="24rpx"></up-icon>
</view>
</view>
</picker>
</view>
<view class="py-2 border-b-2 border-b-solid border-gray-200 flex items-center">
<view>密码</view>
<view class="flex-1">
<up-input
:customStyle="{
padding: '0 28rpx',
outline: 'none',
height: '80rpx',
backgroundColor: '#FFFFFF',
border: 0
}"
:maxlength="20"
placeholder-class="!text-base !line-height-[80rpx]"
placeholder="请输入密码"
:type="showPassword ? 'text' : 'password'"
@change="handleInput"
>
<template #suffix>
<up-icon
:name="showPassword ? 'eye-fill' : 'eye-off'"
size="42rpx"
@click="togglePassword"
></up-icon>
</template>
</up-input>
</view>
</view>
</view>
<view
v-if="current === 0"
class="fixed bottom-[calc(env(safe-area-inset-bottom)+32rpx)] w-686rpx mx-4 h-88rpx text-center text-white bg-[#63b8af] leading-[88rpx] rounded-44rpx font-bold"
@click="handleNext"
>下一步</view
@click="connectWifi"
>
连接
</view>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useBluetoothStore } from '@/stores/bluetooth'
// import { useUserStore } from '@/stores/user'
import { useBasicStore } from '@/stores/basic'
import { passthrough } from '@/api/sdk'
const $bluetooth = useBluetoothStore()
// const $user = useUserStore()
const $basic = useBasicStore()
const current = ref(0)
const wifiList = ref([])
const wifiIndex = ref()
const password = ref('')
const step = ref(1)
const deviceList = ref([])
const showPassword = ref(false)
const stepList = ref([
'手机与设备连接成功',
'向设备发送信息成功',
'设备连接云端成功',
'初始化成功'
])
const pending = ref(false)
const wifiInfo = ref({
SSID: '',
password: ''
onMounted(async () => {
uni.showLoading({
title: '搜索中'
})
// const result = await $bluetooth.getWifiList({
// uid: $user.userInfo.uid.toString()
// })
// if (result.code !== 0) {
// uni.showModal({
// title: '',
// content: '',
// showCancel: false,
// success: () => {
// uni.navigateBack()
// }
// })
// }
listenEvent()
setTimeout(() => {
uni.hideLoading()
wifiList.value = [
{
SSID: '测试1',
rssi: 10
},
{
SSID: '测试2',
rssi: 20
},
{
SSID: '测试3',
rssi: 30
}
]
wifiIndex.value = 0
}, 2000)
})
const deviceInfo = $basic.deviceInfo
onUnmounted(() => {
uni.$off('wifiList')
uni.$off('distributionNetworkResult')
})
onMounted(() => {
if (deviceInfo.platform !== 'android' && deviceInfo.platform !== 'ios') {
uni.showToast({
title: '当前设备不支持WiFi功能',
icon: 'none'
})
return
}
uni.authorize({
scope: 'scope.userLocation',
success: () => {
const getWifiListFn = () => {
uni.getWifiList({
success: () => {
uni.onGetWifiList(res => {
const uniqueWifiList = res.wifiList
.filter(wifi => wifi.SSID && wifi.SSID.trim() !== '')
.reduce((acc, current) => {
const exists = acc.find(item => item.SSID === current.SSID)
if (!exists) {
acc.push(current)
}
return acc
}, [])
wifiList.value = uniqueWifiList
wifiIndex.value = 0
})
},
fail: err => {
console.error('获取WiFi列表失败', err)
uni.showToast({
title: '获取WiFi列表失败',
icon: 'none'
})
}
})
}
if (deviceInfo.platform === 'android') {
uni.startWifi({
success: () => {
getWifiListFn()
},
fail: err => {
console.error('初始化WiFi失败', err)
uni.showToast({
title: '初始化WiFi失败',
icon: 'none'
})
}
})
} else {
getWifiListFn()
}
},
fail: () => {
const listenEvent = () => {
uni.$on('wifiList', async data => {
if (data.status === 0) {
wifiList.value = data.wifiList
uni.hideLoading()
} else {
uni.showModal({
title: '提示',
content: '需要获取位置权限才能使用WiFi功能',
success: res => {
if (res.confirm) {
uni.openSetting()
}
content: '搜索失败,请返回重试',
showCancel: false,
success: () => {
uni.navigateBack()
}
})
}
})
})
uni.$on('distributionNetworkResult', async data => {
uni.hideLoading()
pending.value = false
if (data.status === 0) {
$basic.routeJump({
type: 'redirectTo',
name: 'selectAddress'
})
setTimeout(() => {
uni.showToast({
title: '连接成功',
icon: 'none'
})
}, 1000)
} else {
uni.showToast({
title: '连接失败,请重试',
icon: 'none'
})
}
})
}
const togglePassword = () => {
showPassword.value = !showPassword.value
@ -216,62 +167,7 @@
wifiIndex.value = e.detail.value
}
function bleComboConfigure() {}
const connectDevice = async device => {
// try {
// const deviceAdapter = await bluetoothAdapter.connectDevice(device)
// console.log(1111, deviceAdapter)
// bleComboConfigure({
// token: '1234567890',
// wifiInfo: wifiInfo.value,
// familyId: 'default',
// roomId: 'default',
// deviceAdapter
// })
// current.value++
// } catch (err) {
// console.error('', err)
// }
}
const searchDevice = async () => {
// try {
// await bluetoothAdapter.startSearch({
// onError: error => {
// console.log('', error)
// bluetoothAdapter.stopSearch()
// },
// onSearch: devices => {
// if (devices.length > 0) {
// console.log('', devices)
// deviceList.value = devices
// }
// },
// timeout: 1.4 * 15 * 1000
// })
// } catch (error) {
// console.log('1', error)
// }
}
const handleNext = () => {
current.value++
searchDevice()
if (wifiIndex.value === undefined) {
uni.showToast({
title: '请选择WiFi',
icon: 'none'
})
return
}
if (password.value === '') {
uni.showToast({
title: '请输入密码',
icon: 'none'
})
return
}
const connectWifi = async () => {
if (password.value.length < 8) {
uni.showToast({
title: '密码长度不能小于8位',
@ -280,25 +176,59 @@
return
}
wifiInfo.value = {
SSID: wifiList.value[wifiIndex.value].SSID,
password: password.value
}
if (pending.value) return
pending.value = true
uni.showLoading({
title: '连接中...'
})
uni.offGetWifiList()
if (deviceInfo.platform === 'android') {
uni.stopWifi()
}
current.value++
const result = await passthrough({
request_method: 'GET',
request_uri: '/api/v1/tencentYun/getTencentTriple',
post_args: {
serialNum0: $bluetooth.currentLockInfo.lockConfig.serialNum0
}
})
if (result.code === 0) {
$bluetooth.updateCurrentLockInfo({
...$bluetooth.currentLockInfo,
tencentYunLock: {
productId: result.data.productId,
deviceName: result.data.deviceName,
devicePsk: result.data.devicePsk
}
})
// const result = await $bluetooth.distributionNetwork({
// SSID: wifiList.value[wifiIndex.value].SSID,
// password: password.value,
// json: JSON.stringify({
// productId: result.data.productId,
// deviceName: result.data.deviceName,
// devicePsk: result.data.devicePsk
// })
// })
// if (result.code !== 0) {
// uni.showToast({
// title: '',
// icon: 'none'
// })
// }
searchDevice()
setTimeout(() => {
$basic.routeJump({
type: 'redirectTo',
name: 'selectAddress'
})
}, 3000)
} else {
uni.showToast({
title: result.message,
icon: 'none'
})
}
}
const handleInput = e => {
password.value = e
}
</script>
<style lang="scss" scoped>
.custom-steps {
:deep(.u-text__value) {
font-size: 28rpx !important;
}
}
</style>

View File

@ -139,7 +139,7 @@
}
this.routeJump({
type: 'redirectTo',
name: 'selectAddress'
name: false ? 'selectAddress' : 'distributionNetwork'
})
} else {
uni.hideLoading()

View File

@ -384,36 +384,36 @@
<style lang="scss" scoped>
.search {
padding: 32rpx;
width: 686rpx !important;
padding: 32rpx;
}
.button {
display: flex;
align-items: center;
position: fixed;
bottom: calc(env(safe-area-inset-bottom) + 20rpx);
display: flex;
align-items: center;
font-weight: bold;
.button-reset {
margin-left: 50rpx;
width: 300rpx;
height: 88rpx;
background-color: #df282d;
margin-left: 50rpx;
line-height: 88rpx;
color: white;
text-align: center;
line-height: 88rpx;
background-color: #df282d;
border-radius: 44rpx;
}
.button-create {
margin-left: 50rpx;
width: 300rpx;
height: 88rpx;
background-color: #63b8af;
margin-left: 50rpx;
line-height: 88rpx;
color: white;
text-align: center;
line-height: 88rpx;
background-color: #63b8af;
border-radius: 44rpx;
}
}
@ -421,29 +421,29 @@
.item {
display: flex;
align-items: center;
background-color: #ffffff;
height: 120rpx;
width: 750rpx;
height: 120rpx;
background-color: #ffffff;
.item-left {
margin-left: 32rpx;
width: 80rpx;
height: 80rpx;
margin-left: 32rpx;
}
.item-right {
width: 574rpx;
margin-right: 32rpx;
margin-left: 32rpx;
width: 574rpx;
.item-right-top {
max-width: 400rpx;
font-size: 32rpx;
font-weight: bold;
padding-bottom: 6rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 32rpx;
font-weight: bold;
white-space: nowrap;
}
.item-right-bottom {
@ -467,9 +467,9 @@
}
.empty-list-text {
text-align: center;
font-size: 32rpx;
color: #999999;
text-align: center;
}
.status {

191
pages/main/customBox.vue Normal file
View File

@ -0,0 +1,191 @@
<template>
<view class="dialog">
<image
class="top-background"
src="https://oss-lock.xhjcn.ltd/mp/background_main.jpg"
mode="aspectFill"
></image>
<view class="switch" @click="openDoorOperate">
<SwitchLoading :size="220" ref="sLoading"></SwitchLoading>
</view>
<view class="switch-text">点击开锁</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import SwitchLoading from '@/components/SwitchLoading/SwitchLoading.vue'
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { useUserStore } from '@/stores/user'
import { getLockDetailRequest, getLockNetTokenRequest } from '@/api/lock'
const $bluetooth = useBluetoothStore()
const $basic = useBasicStore()
const $user = useUserStore()
const sLoading = ref(null)
const pending = ref(false)
const lockInfo = ref(null)
const onlineToken = ref('0')
const lockId = ref(2712)
const time = ref(0)
onMounted(async () => {
const { code, data, message } = await getLockDetailRequest({
lockId: lockId.value
})
if (code === 0) {
lockInfo.value = data
$bluetooth.updateCurrentLockInfo({
...lockInfo.value,
name: lockInfo.value.lockName,
deviceId: lockInfo.value.lockName,
commKey: lockInfo.value.privateKey
})
} else {
uni.showToast({
title: message,
icon: 'none'
})
}
await getServeTime()
})
const getServeTime = async () => {
const { code, data } = await $bluetooth.updateServerTimestamp()
if (code === 0) {
time.value = parseInt((data.date - new Date().getTime()) / 1000, 10)
}
}
const openDoorOperate = async () => {
const timestamp = new Date().getTime()
if (pending.value) {
return
}
const netWork = await $basic.getNetworkType()
if (!netWork) {
return
}
if (lockInfo.value.appUnlockOnline) {
const result = await getNetToken()
if (!result) {
sLoading.value.close()
pending.value = false
return
}
}
uni.vibrateLong()
pending.value = true
sLoading.value.open()
const openMode = lockInfo.value.appUnlockOnline ? 1 : 0
const { code } = await $bluetooth.openDoor({
name: lockInfo.value.lockName,
uid: $user.userInfo.uid.toString(),
openMode,
openTime: parseInt(new Date().getTime() / 1000, 10) + time.value,
onlineToken: onlineToken.value
})
$bluetooth
.syncRecord({
keyId: lockInfo.value.keyId.toString(),
uid: $user.userInfo.uid.toString()
})
.then(() => {
$bluetooth.closeBluetoothConnection()
})
uni.reportEvent('open_door', {
result: code,
duration: new Date().getTime() - timestamp
})
if (code === 0) {
uni.showToast({
title: `开门成功`,
icon: 'none'
})
} else if (code === 7) {
uni.showToast({
title: `钥匙过期`,
icon: 'none'
})
} else if (code === 13) {
uni.showToast({
title: `钥匙当前不可用`,
icon: 'none'
})
} else if (code === -1) {
uni.showToast({
title: `开锁失败`,
icon: 'none'
})
}
sLoading.value.close()
pending.value = false
}
const getNetToken = async () => {
const { code, data, message } = await getLockNetTokenRequest({
lockId: lockId.value
})
if (code === 0) {
onlineToken.value = data.token
return true
}
uni.showToast({
title: message,
icon: 'none'
})
return false
}
</script>
<style lang="scss" scoped>
.dialog {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 75vh;
background-color: #f3f3f3;
.top-background {
position: absolute;
width: 686rpx;
height: 464rpx;
border-radius: 32rpx;
}
.switch {
z-index: 99;
display: flex;
align-items: center;
justify-content: center;
width: 250rpx;
height: 250rpx;
margin-top: 20rpx;
background: #ffffff;
border-radius: 50%;
box-shadow: 0 8rpx 36rpx 0 rgba(0, 0, 0, 0.12);
}
.switch-text {
z-index: 99;
margin-top: 10rpx;
text-align: center;
}
}
</style>

View File

@ -221,6 +221,16 @@
></image>
<view>消息提醒</view>
</view>
<view
class="menu-main-view transform-scale-105"
@click="$basic.routeJump({ name: 'authorizeWechat' })"
>
<image
class="menu-main-image"
src="https://oss-lock.xhjcn.ltd/mp/icon_wechat_call.png"
></image>
<view>微信呼叫</view>
</view>
</view>
</view>
<view class="setting" @click="$basic.routeJump({ name: 'setting' })">

View File

@ -0,0 +1,148 @@
<template>
<view>
<view class="mt-10 text-center text-base font-bold text-[#999]">授权后设备可呼叫该微信</view>
<view v-if="requestFinish">
<view
v-if="!isAuthorized"
class="bg-[#63b8af] text-white rounded-full text-center leading-88rpx h-88rpx w-686 mx-4 mt-10"
@click="handleAuthorize"
>
授权
</view>
<view v-else>
<view v-if="!reject" class="text-center text-lg font-bold mt-4">您已授权</view>
<view v-else>
<view class="text-center text-lg font-bold mt-4"> 您已拒绝授权请去设置中 </view>
<view class="text-center text-lg font-bold mt-4"> 打开语音视频通话提醒开关 </view>
<view
class="bg-[#63b8af] text-white rounded-full text-center leading-88rpx h-88rpx w-686 mx-4 mt-10"
@click="openSetting"
>
打开设置
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { passthrough } from '@/api/sdk'
import { useBluetoothStore } from '@/stores/bluetooth'
import env from '@/config/env'
const $bluetooth = useBluetoothStore()
const requestFinish = ref(false)
const isAuthorized = ref(false)
const list = ref([])
const reject = ref(false)
const pending = ref(false)
onShow(() => {
uni.showLoading({
title: '加载中...'
})
wx.getDeviceVoIPList({
async success(res) {
list.value = res.list
if (res.list.length > 0) {
const result = await getInfo()
if (result.code === 0) {
const data = list.value.find(item => item.sn === result.data.WXIoTDeviceInfo.SN)
if (data) {
if (data.status === 1) {
reject.value = false
} else if (data.status === 0) {
reject.value = true
}
isAuthorized.value = true
requestFinish.value = true
uni.hideLoading()
} else {
requestFinish.value = true
uni.hideLoading()
}
} else {
uni.showToast({
title: result.message,
icon: 'none'
})
requestFinish.value = true
uni.hideLoading()
}
} else {
requestFinish.value = true
uni.hideLoading()
}
}
})
})
const openSetting = () => {
uni.openSetting({
success: res => {
console.log(res)
}
})
}
const getInfo = async () => {
const result = await passthrough({
request_method: 'GET',
request_uri: '/api/v1/tencentYun/getWechatDeviceTicket',
post_args: {
lockId: $bluetooth.currentLockInfo.lockId
}
})
return result
}
const handleAuthorize = async () => {
if (pending.value) return
pending.value = true
uni.showLoading({
title: '授权中...'
})
const result = await getInfo()
if (result.code === 0) {
wx.requestDeviceVoIP({
sn: result.data.WXIoTDeviceInfo.SN,
snTicket: result.data.WXIoTDeviceInfo.SNTicket,
modelId: result.data.WXIoTDeviceInfo.ModelId,
deviceName: await env[await getApp().globalData.getEnvConfig()].appName,
success() {
isAuthorized.value = true
uni.hideLoading()
pending.value = false
uni.showToast({
title: '授权成功',
icon: 'none'
})
},
fail() {
isAuthorized.value = true
reject.value = true
uni.hideLoading()
pending.value = false
uni.showToast({
title: '授权失败',
icon: 'none'
})
}
})
} else {
uni.hideLoading()
pending.value = false
uni.showToast({
title: result.message,
icon: 'none'
})
}
}
</script>

View File

@ -1,20 +1,50 @@
<template>
<view>
<iot-p2p-player-with-mjpg
v-if="deviceInfo"
id="playerRef"
:deviceInfo="deviceInfo"
:xp2pInfo="xp2pInfo"
:rotate="90"
:muted="isMute"
streamQuality="high"
mode="RTC"
:acceptPlayerEvents="true"
soundMode="speaker"
sceneType="live"
:streamQuality="range[index].value"
:minCache="0.2"
:maxCache="0.8"
:fill="true"
orientation="horizontal"
@statechange="handleStateChange"
@playsuccess="handlePlaySuccess"
>
</iot-p2p-player-with-mjpg>
<view
v-if="buttonInfo"
:style="{
top: buttonInfo.bottom + 15 + 'px'
}"
class="bg-[rgba(0,0,0,0.35)] rounded-full px-2 py-1.5 fixed right-32"
>
<picker :value="index" mode="selector" :range="range" range-key="name" @change="changeEvent">
<up-icon
:label="range[index].name"
color="#ffffff"
label-color="#ffffff"
labelPos="left"
name="arrow-down-fill"
size="32rpx"
space="16rpx"
></up-icon>
</picker>
</view>
<image
v-if="!isVideoLoaded"
src="https://oss-lock.xhjcn.ltd/mp/background_monitor.png"
class="w-full h-full absolute top-0 left-0"
></image>
<iot-p2p-voice
v-if="deviceInfo"
id="voiceComponent"
:deviceInfo="deviceInfo"
:xp2pInfo="xp2pInfo"
@ -23,6 +53,7 @@
</iot-p2p-voice>
<view
v-if="isVideoLoaded"
class="fixed bottom-[calc(32rpx+env(safe-area-inset-bottom))] bg-[rgba(0,0,0,0.3)] rounded-xl p-4 shadow-lg mx-4 w-622"
>
<view class="flex items-center justify-around mx-10">
@ -72,21 +103,39 @@
</view>
</view>
</view>
<view
v-if="!isVideoLoaded"
class="fixed bottom-[calc(48rpx+env(safe-area-inset-bottom))] w-full flex justify-center"
>
<up-loading-icon
size="70rpx"
:vertical="true"
textSize="28rpx"
text="连接中"
mode="circle"
></up-loading-icon>
</view>
</view>
</template>
<script setup>
import { getXp2pManager } from './xp2pManager'
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { onMounted, ref } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
import { getP2pInfo } from '@/api/p2p'
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { passthrough } from '@/api/sdk'
const $bluetooth = useBluetoothStore()
const $basic = useBasicStore()
let xp2pManager = null
const buttonInfo = ref(null)
const index = ref(1)
const range = ref([
{ name: '标清', value: 'standard' },
{ name: '高清', value: 'high' },
{ name: '超清', value: 'super' }
])
const deviceInfo = ref()
const xp2pInfo = ref()
@ -94,28 +143,26 @@
const isVoice = ref(false)
const isMute = ref(false)
const isVideoLoaded = ref(false)
onMounted(async () => {
if (!xp2pManager) {
xp2pManager = getXp2pManager()
}
buttonInfo.value = await $basic.getButtonInfo()
const { code, data, message } = await getP2pInfo({
lockId: $bluetooth.currentLockInfo.lockId
const { code, data, message } = await passthrough({
request_method: 'GET',
request_uri: '/api/v1/tencentYun/getDeviceDetailData',
post_args: {
lockId: $bluetooth.currentLockInfo.lockId
}
})
if (code === 0) {
deviceInfo.value = {
deviceId: `${data.productId}/${data.deviceId}`,
deviceId: `${data.productId}/${data.deviceName}`,
productId: data.productId,
deviceName: data.deviceName
}
xp2pInfo.value = data.xp2pInfo
await xp2pManager.startP2PService({
deviceInfo: deviceInfo.value,
xp2pInfo: xp2pInfo.value,
caller: 1
})
} else {
$basic.backAndToast(message)
}
@ -129,6 +176,10 @@
}
})
const changeEvent = e => {
index.value = e.detail.value
}
const handleScreenshot = () => {
const page = getCurrentPages().pop()
const player = page.selectComponent('#playerRef')
@ -180,8 +231,8 @@
voice.stopVoice()
}
const handleStateChange = state => {
console.log(11111111, state)
const handlePlaySuccess = () => {
isVideoLoaded.value = true
}
</script>

View File

@ -1,29 +0,0 @@
export const getUserId = () => {
return 1
}
export function compareVersion(ver1, ver2) {
const v1 = ver1.split('.')
const v2 = ver2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}

View File

@ -1,118 +0,0 @@
import { getUserId, compareVersion } from './utils'
let xp2pManager = null
export const getXp2pManager = () => {
if (!xp2pManager) {
let xp2pPlugin = requirePlugin('xp2p')
console.log(11111, xp2pPlugin)
const iotExports = xp2pPlugin.iot
const app = getApp()
// 用户id微信用户在此小程序中的唯一标识
iotExports?.setUserId?.(getUserId() || 'demo')
// 开发版才打插件log
if (app.pluginLogger && uni.getAccountInfoSync().miniProgram.envVersion === 'develop') {
iotExports?.setPluginLogger?.(app.pluginLogger)
}
// 设置优先使用的打洞协议
if (
compareVersion(uni.getSystemInfoSync().SDKVersion, '3.4.1') >= 0 &&
xp2pPlugin.p2p.setTcpFirst
) {
const tcpFirstKey = 'tcpFirst'
const tcpFirstTime = parseInt(uni.getStorageSync(tcpFirstKey), 10)
const tcpFirst = !!(tcpFirstTime && Date.now() - tcpFirstTime < 3600000 * 24) // 24小时内有效
console.log('tcpFirst', tcpFirst)
xp2pPlugin.p2p.setTcpFirst(tcpFirst)
// 给index页用方便测试时调整tcpFirst
app.tcpFirst = tcpFirst
app.toggleTcpFirst = async () => {
const modalRes = await uni.showModal({
title: '确定切换 tcpFirst 吗?',
content: '切换后需要重新进入小程序'
})
if (!modalRes || !modalRes.confirm) {
return
}
uni.setStorageSync(tcpFirstKey, tcpFirst ? '' : Date.now())
app.restart()
}
}
// 设置是否连接对端stun
if (xp2pPlugin.p2p.setCrossStunTurn) {
const crossStunTurnKey = 'crossStunTurn'
const crossStunTurnTime = parseInt(uni.getStorageSync(crossStunTurnKey), 10)
const crossStunTurn = !!(crossStunTurnTime && Date.now() - crossStunTurnTime < 3600000 * 24) // 24小时内有效
xp2pPlugin.p2p.setCrossStunTurn(crossStunTurn)
// 给index页用方便测试时调整crossStunTurn
app.crossStunTurn = crossStunTurn
app.toggleCrossStunTurn = async () => {
const modalRes = await uni.showModal({
title: '确定切换 crossStunTurn 吗?',
content: '切换后需要重新进入小程序'
})
if (!modalRes || !modalRes.confirm) {
return
}
uni.setStorageSync(crossStunTurnKey, crossStunTurn ? '' : Date.now())
app.restart()
}
}
// 设置切换端口
if (xp2pPlugin.p2p.updateStunPort) {
const portKey = 'STUN_PORT'
const stunPort = uni.getStorageSync(portKey) || 20002
xp2pPlugin.p2p.updateStunPort(stunPort)
app.stunPort = stunPort
app.togglePort = async port => {
const modalRes = await uni.showModal({
title: '确定切换 stunPort 吗?',
content: '切换后需要重新进入小程序'
})
if (!modalRes || !modalRes.confirm) {
return
}
uni.setStorageSync(portKey, port)
app.restart()
}
}
// 设置切换配置跟随
if (xp2pPlugin.p2p.setUseDeliveryConfig) {
const useDeliveryConfigKey = 'useDeliveryConfig'
const useDeliveryConfigTime = parseInt(uni.getStorageSync(useDeliveryConfigKey), 10)
const useDeliveryConfig = !!(
useDeliveryConfigTime && Date.now() - useDeliveryConfigTime < 3600000 * 24
) // 24小时内有效
xp2pPlugin.p2p.setUseDeliveryConfig(useDeliveryConfig)
app.useDeliveryConfig = useDeliveryConfig
app.toggleUseDeliveryConfig = async () => {
const modalRes = await uni.showModal({
title: '确定切换 使用设备跟随 吗?',
content: '切换后需要重新进入小程序'
})
if (!modalRes || !modalRes.confirm) {
return
}
uni.setStorageSync(useDeliveryConfigKey, useDeliveryConfig ? '' : Date.now())
app.restart()
}
}
xp2pManager = iotExports.getXp2pManager()
app.logger?.log('xp2pManager', {
P2PPlayerVersion: xp2pManager.P2PPlayerVersion,
XP2PVersion: xp2pManager.XP2PVersion,
// uuid插件随机生成的id存储在小程序本地删除小程序后会重新生成
uuid: xp2pManager.uuid
})
}
return xp2pManager
}

View File

@ -9,7 +9,7 @@
? catEyeMode[
$bluetooth.currentLockSetting.lockSettingInfo.catEyeConfig[0]?.catEyeMode
].name
: ""
: ''
}}
</view>
<up-icon name="arrow-right"></up-icon>

View File

@ -391,6 +391,11 @@ const pages = [
name: 'p2pPlayer',
path: '/pages/p2p/p2pPlayer',
tabBar: false
},
{
name: 'authorizeWechat',
path: '/pages/p2p/authorizeWechat',
tabBar: false
}
]

View File

@ -56,7 +56,15 @@ const cmdIds = {
// 锁遥控列表
lockRemoteList: 0x3026,
// 锁设置列表
lockSettingList: 0x302a
lockSettingList: 0x302a,
// 获取Wi-Fi列表
getWifiList: 0x30f6,
// Wi-Fi列表
wifiList: 0x30f7,
// 配网
distributionNetwork: 0x30f4,
// 配网结果
distributionNetworkResult: 0x30f5
}
// 子命令ID
@ -396,6 +404,34 @@ export const useBluetoothStore = defineStore('ble', {
})
}
break
case cmdIds.getWifiList:
characteristicValueCallback({
code: decrypted[2]
})
break
case cmdIds.wifiList:
const wifiList = []
for (let i = 0; i < decrypted[3]; i++) {
wifiList.push({
SSID: that.uint8ArrayToString(decrypted.slice(4 + 33 * i, 4 + 33 * i + 32)),
rssi: decrypted[33 + 33 * i]
})
}
uni.$emit('wifiList', {
status: decrypted[2],
wifiList
})
break
case cmdIds.distributionNetwork:
characteristicValueCallback({
code: decrypted[2]
})
break
case cmdIds.distributionNetworkResult:
uni.$emit('distributionNetworkResult', {
status: decrypted[2]
})
break
case cmdIds.addUser:
that.updateCurrentLockInfo({
...that.currentLockInfo,
@ -1558,6 +1594,123 @@ export const useBluetoothStore = defineStore('ble', {
return this.getWriteResult(this.getLockStatus, data)
},
// 获取Wi-Fi列表
async getWifiList(data) {
// 确认蓝牙状态正常
if (this.bluetoothStatus !== 0) {
console.log('写入未执行', this.bluetoothStatus)
this.getBluetoothStatus()
return {
code: -1
}
}
// 确认设备连接正常
if (!this.currentLockInfo.connected) {
const searchResult = await this.searchAndConnectDevice()
if (searchResult.code !== 0) {
return searchResult
}
this.updateCurrentLockInfo({
...this.currentLockInfo,
deviceId: searchResult.data.deviceId
})
console.log('设备ID', this.currentLockInfo.deviceId)
const result = await this.connectBluetoothDevice()
console.log('连接结果', result)
if (!result) {
return {
code: -1
}
}
}
const { uid } = data
const length = 2 + 20
const headArray = this.createPackageHeader(3, length)
const contentArray = new Uint8Array(length)
contentArray[0] = cmdIds.getWifiList / 256
contentArray[1] = cmdIds.getWifiList % 256
for (let i = 0; i < uid.length; i++) {
contentArray[i + 2] = uid.charCodeAt(i)
}
const cebArray = sm4.encrypt(contentArray, this.currentLockInfo.commKey, {
mode: 'ecb',
output: 'array'
})
const packageArray = this.createPackageEnd(headArray, cebArray)
await this.writeBLECharacteristicValue(packageArray)
return this.getWriteResult(this.getWifiList, data)
},
// 配网
async distributionNetwork(data) {
// 确认蓝牙状态正常
if (this.bluetoothStatus !== 0) {
console.log('写入未执行', this.bluetoothStatus)
this.getBluetoothStatus()
return {
code: -1
}
}
// 确认设备连接正常
if (!this.currentLockInfo.connected) {
const searchResult = await this.searchAndConnectDevice()
if (searchResult.code !== 0) {
return searchResult
}
this.updateCurrentLockInfo({
...this.currentLockInfo,
deviceId: searchResult.data.deviceId
})
console.log('设备ID', this.currentLockInfo.deviceId)
const result = await this.connectBluetoothDevice()
console.log('连接结果', result)
if (!result) {
return {
code: -1
}
}
}
const { SSID, password, json } = data
const length = 2 + 30 + 20 + json.length
const headArray = this.createPackageHeader(3, length)
const contentArray = new Uint8Array(length)
contentArray[0] = cmdIds.distributionNetwork / 256
contentArray[1] = cmdIds.distributionNetwork % 256
for (let i = 0; i < SSID.length; i++) {
contentArray[i + 2] = SSID.charCodeAt(i)
}
for (let i = 0; i < password.length; i++) {
contentArray[i + 32] = password.charCodeAt(i)
}
contentArray[52] = json.length / 256
contentArray[53] = json.length % 256
for (let i = 0; i < json.length; i++) {
contentArray[i + 54] = json.charCodeAt(i)
}
const cebArray = sm4.encrypt(contentArray, this.currentLockInfo.commKey, {
mode: 'ecb',
output: 'array'
})
const packageArray = this.createPackageEnd(headArray, cebArray)
await this.writeBLECharacteristicValue(packageArray)
return this.getWriteResult(this.distributionNetwork, data)
},
// 时间戳转二进制
timestampToArray(timestamp) {
const array = new Uint8Array(4)
@ -2732,12 +2885,12 @@ export const useBluetoothStore = defineStore('ble', {
const md5Array = this.md5Encrypte(
uid + keyId,
this.currentLockInfo.token || new Uint8Array([0, 0, 0, 0]),
this.currentLockInfo.bluetooth.publicKey
this.currentLockInfo.publicKey
)
contentArray.set(md5Array, 75)
const cebArray = sm4.encrypt(contentArray, this.currentLockInfo.bluetooth.privateKey, {
const cebArray = sm4.encrypt(contentArray, this.currentLockInfo.commKey, {
mode: 'ecb',
output: 'array'
})

View File

@ -29,13 +29,13 @@ const request = config => {
const timestamp = new Date().getTime()
timer = setTimeout(() => {
resolve({ code: -1, message: '网络访问失败,请检查网络是否正常' })
}, 7200)
}, 30200)
uni.request({
url: URL,
method,
header,
data,
timeout: 7000,
timeout: 30000,
async success(res) {
const { statusCode, data } = res
if (timer) {