1. 完成锁详情页UI及开关锁功能

This commit is contained in:
范鹏 2024-08-26 14:13:36 +08:00
parent e3a54bd1e6
commit 04376b94df
14 changed files with 438 additions and 11 deletions

View File

@ -19,3 +19,12 @@ export function bindLockAdmin(data) {
data
})
}
// 获取手机联网token
export function getLockNetTokenRequest(data) {
return request({
url: '/lock/getLockNetToken',
method: 'POST',
data
})
}

View File

@ -0,0 +1,95 @@
<template>
<view>
<view class="spinner-box" :style="{width: size + 'rpx', height: size + 'rpx'}">
<view v-if="show" class="circle-border" :style="{width: size * 0.75 + 'rpx', height: size * 0.75 + 'rpx'}">
</view>
<view v-else class="circle-border-stop" :style="{width: size * 0.75 + 'rpx', height: size * 0.75 + 'rpx'}"></view>
<view class="circle-core" :style="{width: size * 0.75 - 2 + 'rpx', height: size * 0.75 - 2 + 'rpx'}">
<image src="/static/images/icon_lock_transparent.png" mode="aspectFill" :style="{width: size * 0.35 + 'rpx',
height: size * 0.35 + 'rpx'}"></image>
</view>
</view>
</view>
</template>
<script>
export default {
name: "SwitchLoading",
props:{
size: Number,
},
data () {
return {
show: false
}
},
methods: {
open () {
this.show = true
},
close () {
this.show = false
}
}
};
</script>
<style>
@keyframes spin {
from {
transform: rotate(0);
}
to {
transform: rotate(359deg);
}
}
.spinner-box {
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
position: relative;
}
.circle-border {
padding: 3upx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: rgb(99, 184, 175);
background: linear-gradient(
0deg,
rgba(99, 184, 175, 0.1) 33%,
rgba(99, 184, 175, 1) 100%
);
animation: spin 0.8s linear 0s infinite;
position: absolute;
z-index: 0;
}
.circle-border-stop {
padding: 3upx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: rgb(99, 184, 175);
position: absolute;
z-index: 0;
}
.circle-core {
z-index: 1;
width: 100%;
height: 100%;
background-color: #FFFFFF;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -64,7 +64,6 @@
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { mapState, mapActions } from 'pinia'
import { getServerDatetime } from '@/api/check'
export default {
data() {
@ -106,7 +105,8 @@
timeFormat,
...mapActions(useUserStore, ['updateUserInfo', 'updateLoginStatus', 'login']),
...mapActions(useLockStore, ['getLockList', 'getRole', 'getTimeLimit']),
...mapActions(useBluetoothStore, ['getBluetoothStatus', 'initAndListenBluetooth', 'updateCurrentLockInfo', 'checkSetting']),
...mapActions(useBluetoothStore, ['getBluetoothStatus', 'initAndListenBluetooth', 'updateCurrentLockInfo',
'checkSetting', 'updateKeyId']),
...mapActions(useBasicStore, ['routeJump', 'getDeviceInfo']),
async nextPage() {
if(this.lockList.length < this.lockTotal) {
@ -172,7 +172,16 @@
result = await this.initAndListenBluetooth()
}
if(result) {
this.updateCurrentLockInfo(lock)
const data = {
...lock,
name: lock.bluetooth.bluetoothDeviceName,
deviceId: lock.bluetooth.bluetoothDeviceId,
commKey: lock.bluetooth.privateKey,
signKey: lock.bluetooth.signKey,
publicKey: lock.bluetooth.publicKey,
}
this.updateKeyId(lock.keyId)
this.updateCurrentLockInfo(data)
this.routeJump({
name: 'lockDetail'
})

View File

@ -1,17 +1,327 @@
<template>
<view>
<view class="top">
<image class="top-background" src="/static/images/background_main.jpg" mode="aspectFill"></image>
<view style="width: 100%;height: 50rpx">
<view class="power" @click="powerTip">
<image class="power-icon" src="/static/images/icon_power.png" mode="aspectFill"></image>
<view class="power-text">{{ currentLockInfo.electricQuantity }}%</view>
<image class="power-tips" src="/static/images/icon_tips.png" mode="aspectFill"></image>
</view>
</view>
<view class="switch" @click="openDoorOperate('open')" @longpress="openDoorOperate('close')">
<SwitchLoading :size="220" ref="loading"></SwitchLoading>
</view>
<view class="switch-text">点击开锁长按闭锁</view>
<view class="bottom">
<view class="bottom-side">
<image class="bottom-icon" src="/static/images/icon_role.png" mode="aspectFill"></image>
<view>{{ getRole(currentLockInfo.userType) }}</view>
</view>
<view class="bottom-side">
<image class="bottom-icon" :src=" currentLockInfo.lockSetting.appUnlockOnline ?
'/static/images/icon_cloud_active.png' : '/static/images/icon_cloud.png' "
mode="aspectFill" style="width: 40rpx;height: 40rpx;"></image>
<view :style="{color: currentLockInfo.lockSetting.appUnlockOnline ? '#63b8af' : '#a3a3a3'}">手机需联网</view>
</view>
</view>
</view>
<view class="menu">
<view class="menu-title">
<image class="menu-image" src="/static/images/icon_menu.png"></image>
<view>功能</view>
</view>
<view class="menu-main">
<view v-if="currentLockInfo.lockFeature.bluetoothRemoteControl || currentLockInfo.userType === 110301"
class="menu-main-view">
<image class="menu-main-image" src="/static/images/tabbar_key_select.png"></image>
<view>电子钥匙</view>
</view>
<view v-if="currentLockInfo.lockFeature.password || currentLockInfo.userType === 110301" class="menu-main-view">
<image class="menu-main-image" src="/static/images/icon_lock_transparent.png"></image>
<view>密码</view>
</view>
</view>
</view>
<view class="setting">
<image class="setting-image" src="/static/images/icon_setting.png"></image>
<view class="setting-text">设置</view>
<image class="setting-arrow" mode="aspectFill" src="/static/images/icon_arrow.png"></image>
</view>
</view>
</template>
<script>
import { useBluetoothStore } from '@/stores/bluetooth'
import { mapState, mapActions } from 'pinia'
import SwitchLoading from '@/components/SwitchLoading/SwitchLoading.vue'
import { useLockStore } from '@/stores/lock'
import { useUserStore } from '@/stores/user'
import { getLockNetTokenRequest } from '@/api/lock'
import { timeFormat } from 'uview-plus'
export default {
data () {
return {}
}
return {
time: 0,
onlineToken: '0',
pending: false
}
},
computed: {
...mapState(useBluetoothStore, ['currentLockInfo']),
...mapState(useUserStore, ['userInfo'])
},
components: {
SwitchLoading
},
onLoad() {
uni.setNavigationBarTitle({
title: this.currentLockInfo.lockAlias
})
this.getServeTime()
},
methods: {
...mapActions(useLockStore, ['getRole']),
...mapActions(useBluetoothStore, ['openDoor', 'updateServerTimestamp']),
powerTip() {
const that = this
uni.showModal({
title: '锁电量更新时间',
content: `${timeFormat(that.currentLockInfo.electricQuantityDate, 'yyyy-mm-dd h:M')}`,
showCancel: false
})
},
async getNetToken(){
const { code, data } = await getLockNetTokenRequest({
lockId: this.currentLockInfo.keyId
})
if(code === 0) {
this.onlineToken = data.token
return true
} else {
return false
}
},
async getServeTime() {
const { code, data } = await this.updateServerTimestamp()
if(code === 0) {
this.time = parseInt((data.date - new Date().getTime()) / 1000)
}
},
async openDoorOperate(type) {
if(this.pending) {
return
}
uni.vibrateLong()
this.pending = true
this.$refs.loading.open()
if(this.currentLockInfo.lockSetting.appUnlockOnline) {
const result = await this.getNetToken()
if(!result) {
this.$refs.loading.close()
this.pending = false
uni.showToast({
title: '联网开锁失败',
icon: 'none'
})
return
}
}
let openMode
if(type === 'close') {
openMode = this.currentLockInfo.lockSetting.appUnlockOnline ? 33 : 32
} else {
openMode = this.currentLockInfo.lockSetting.appUnlockOnline ? 1 : 0
}
const { code } = await this.openDoor({
name: this.currentLockInfo.name,
uid: this.userInfo.uid.toString(),
openMode: openMode,
openTime: parseInt(new Date().getTime() / 1000) + this.time,
onlineToken: this.onlineToken
})
if(code === 0) {
uni.showToast({
title: '开锁成功',
icon: 'none'
})
} else {
uni.showToast({
title: '开锁失败',
icon: 'none'
})
}
this.$refs.loading.close()
this.pending = false
}
},
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.top {
margin-top: 32rpx;
margin-left: 32rpx;
width: 686rpx;
height: 464rpx;
border-radius: 32rpx;
position: relative;
.top-background {
z-index: -1;
position: absolute;
width: 686rpx;
height: 464rpx;
border-radius: 32rpx;
}
.switch {
margin-top: 20rpx;
margin-left: 218rpx;
display: flex;
justify-content: center;
align-items: center;
width: 250rpx;
height: 250rpx;
background: #FFFFFF;
border-radius: 50%;
box-shadow: 0 8rpx 36rpx 0 rgba(0,0,0,0.12);
}
.power {
float: right;
padding-top: 18rpx;
//width: 100%;
display: flex;
align-items: center;
height: 50rpx;
justify-content: flex-end;
.power-icon {
width: 50rpx;
margin-right: 10rpx;
height: 50rpx;
}
.power-text {
font-size: 32rpx;
font-weight: bold;
margin-right: 10rpx;
line-height: 50rpx;
}
.power-tips {
margin-right: 32rpx;
width: 40rpx;
height: 40rpx;
}
}
.switch-text {
margin-top: 10rpx;
text-align: center;
}
}
.bottom {
width: 686rpx;
position: absolute;
bottom: 0;
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.1);
height: 48rpx;
line-height: 48rpx;
font-size: 32rpx;
color: #63b8af;
border-radius: 0 0 32rpx 32rpx;
justify-content: space-around;
.bottom-side {
display: flex;
align-items: center;
}
.bottom-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
}
.menu {
margin-top: 32rpx;
margin-left: 32rpx;
padding-bottom: 32rpx;
width: 686rpx;
background-color: #ffffff;
border-radius: 32rpx;
box-shadow: 0 8rpx 36rpx 0 rgba(0,0,0,0.12);
font-size: 40rpx;
.menu-title {
padding: 24rpx 32rpx;
display: flex;
align-items: center;
}
.menu-image {
margin-right: 40rpx;
width: 40rpx;
height: 40rpx;
}
.menu-main {
padding-top: 32rpx;
padding-bottom: 32rpx;
display: flex;
align-items: center;
font-size: 32rpx;
flex-wrap: wrap;
text-align: center;
margin-left: 43rpx;
.menu-main-view {
width: 150rpx;
.menu-main-image {
filter: sepia(100%) saturate(10000%) hue-rotate(180deg) brightness(0.1);
margin-bottom: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
}
}
.setting {
padding: 24rpx 0;
margin-top: 32rpx;
margin-left: 32rpx;
display: flex;
align-items: center;
width: 686rpx;
background-color: #ffffff;
border-radius: 32rpx;
box-shadow: 0 8rpx 36rpx 0 rgba(0,0,0,0.12);
font-size: 40rpx;
.setting-text {
margin-left: 32rpx;
}
.setting-arrow {
margin-right: 32rpx;
width: 48rpx;
height: 48rpx;
margin-left: auto;
}
.setting-image {
margin-left: 32rpx;
width: 48rpx;
height: 48rpx;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/images/icon_menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/images/icon_role.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/images/icon_tips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -67,6 +67,10 @@ export const useBluetoothStore = defineStore('ble', {
}
},
actions: {
// 更新keyId
updateKeyId(keyId) {
this.keyId = keyId
},
// 二进制转字符串
uint8ArrayToString(uint8Array) {
let str = ''
@ -900,11 +904,12 @@ export const useBluetoothStore = defineStore('ble', {
conentArray.set(this.timestampToArray(openTime), 63)
conentArray.set(this.currentLockInfo.token, 67)
conentArray.set(this.currentLockInfo.token || this.timestampToArray(openTime), 67)
conentArray[71] = 16
const md5Array = this.md5Encrypte(name + uid, this.currentLockInfo.token, this.currentLockInfo.signKey)
const md5Array = this.md5Encrypte(name + uid, this.currentLockInfo.token || this.timestampToArray(openTime), this.currentLockInfo.signKey)
conentArray.set(md5Array, 72)
@ -915,7 +920,6 @@ export const useBluetoothStore = defineStore('ble', {
const cebArray = sm4.encrypt(conentArray, this.currentLockInfo.commKey, { mode: 'ecb', output: 'array' })
const packageArray = this.createPackageEnd(headArray, cebArray)
await this.writeBLECharacteristicValue(packageArray)
return this.getWriteResult(this.openDoor, data)

View File

@ -22,7 +22,7 @@ export const useUserStore = defineStore('user', {
this.isLogin = status
},
async login() {
uni.setStorageSync('token', '3023|pZ1aoVlMKJBTBTGWlsZPpbLvxc8txcHbrJx2ljrf49c7efe0')
uni.setStorageSync('token', '3028|6WkZCHj5yzLlXW3z3ylc1TDhtKYvF3jHB6e4eTWr22681e3e')
const { code, data } = await getUserInfoRequest()
await useLockStore().getLockList({
pageNo: 1,