钟铮锁App web部分 需要打包放进对应的安卓项目 生成apk 才能正常使用功能
he wei
2025-01-19 ba2cfa9907623c094e6e2d52d12dc3055ddd587a
U 整理提交
14个文件已修改
14个文件已添加
2372 ■■■■ 已修改文件
package-lock.json 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/axios.js 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/getWsUrl.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/permissionUtils.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/pinia.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/tools/throttle.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/tree.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/uname.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/icons/iconLock.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useBLELock.js 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/apis.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.js 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/user.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/alarm/apis.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/alarm/index.vue 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/home/apis.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/home/index.vue 206 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/home/viewer.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login/apis.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login/index.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/apis.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/index.vue 426 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/user/apis.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/user/index.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
@@ -15,12 +15,12 @@
        "@capacitor/browser": "^6.0.4",
        "@capacitor/camera": "^6.1.2",
        "@capacitor/core": "6.2.0",
        "@capacitor/filesystem": "^6.0.3",
        "@capacitor/geolocation": "^6.1.0",
        "@ionic-native/bluetooth-le": "^5.36.0",
        "@ionic-native/bluetooth-serial": "^5.36.0",
        "@ionic-native/http": "^5.36.0",
        "axios": "^1.7.2",
        "buffer": "^6.0.3",
        "cordova-plugin-bluetooth-serial": "^0.4.7",
        "crc": "^4.3.2",
        "echarts": "^5.5.1",
        "element-plus": "^2.7.3",
@@ -32,7 +32,8 @@
        "videojs-flash": "^2.2.1",
        "vue": "^3.5.12",
        "vue-qrcode-reader": "^5.6.0",
        "vue-router": "^4.3.0"
        "vue-router": "^4.3.0",
        "vue-virtual-scroller": "^2.0.0-beta.8"
      },
      "devDependencies": {
        "@element-plus/icons": "^0.0.11",
@@ -185,6 +186,24 @@
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.1.0"
      }
    },
    "node_modules/@capacitor/filesystem": {
      "version": "6.0.3",
      "resolved": "https://registry.npmmirror.com/@capacitor/filesystem/-/filesystem-6.0.3.tgz",
      "integrity": "sha512-PdIP/yOGAbG1lq1wbFbSPhXQ9/5lpTpeiok2NneawJOk6UXvy9W7QZXRo7wXAP7J6FdzU7bKfOORRXpOJpgXyw==",
      "license": "MIT",
      "peerDependencies": {
        "@capacitor/core": "^6.0.0"
      }
    },
    "node_modules/@capacitor/geolocation": {
      "version": "6.1.0",
      "resolved": "https://registry.npmmirror.com/@capacitor/geolocation/-/geolocation-6.1.0.tgz",
      "integrity": "sha512-jEY5DcZirxX1gOBuOvf/FL7FlPMOKcsnF8PlfYxu7OFwRDD7HRwehEPWtJXR6wSYLgsCujm7yRqKrjtyN+f14g==",
      "license": "MIT",
      "peerDependencies": {
        "@capacitor/core": "^6.0.0"
      }
    },
    "node_modules/@ctrl/tinycolor": {
@@ -633,19 +652,6 @@
      "version": "5.36.0",
      "resolved": "https://registry.npmmirror.com/@ionic-native/bluetooth-le/-/bluetooth-le-5.36.0.tgz",
      "integrity": "sha512-VJYX2gx85PH1SXVVm+wOeMXVt5U0W5t8HAAx4MWBaAjRoREQSYtKVxWtLURhYKwS4Pu82v/EQmRbwz2AyEg+Wg==",
      "license": "MIT",
      "dependencies": {
        "@types/cordova": "latest"
      },
      "peerDependencies": {
        "@ionic-native/core": "^5.1.0",
        "rxjs": "^5.5.0 || ^6.5.0"
      }
    },
    "node_modules/@ionic-native/bluetooth-serial": {
      "version": "5.36.0",
      "resolved": "https://registry.npmmirror.com/@ionic-native/bluetooth-serial/-/bluetooth-serial-5.36.0.tgz",
      "integrity": "sha512-etb2xbKQBbfI0mGkxgXDuQL40ndu0lJA48Ukz7XUaphUHSP62TofBoPZkXY/CmNvx2ZTCVBB9+2zOFsyyo6iQg==",
      "license": "MIT",
      "dependencies": {
        "@types/cordova": "latest"
@@ -1589,12 +1595,6 @@
        "url": "https://github.com/sponsors/mesqueeb"
      }
    },
    "node_modules/cordova-plugin-bluetooth-serial": {
      "version": "0.4.7",
      "resolved": "https://registry.npmmirror.com/cordova-plugin-bluetooth-serial/-/cordova-plugin-bluetooth-serial-0.4.7.tgz",
      "integrity": "sha512-Z62yZwl77CNoTLW3SwLvvvjPGq+sA1LBsSpuvdC3LePICiJHzFu2Zi5CtGSkjmkj+qOrgHoawOeXVgjN3LrPag==",
      "license": "Apache-2.0"
    },
    "node_modules/crc": {
      "version": "4.3.2",
      "resolved": "https://registry.npmmirror.com/crc/-/crc-4.3.2.tgz",
@@ -2246,6 +2246,12 @@
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/mitt": {
      "version": "2.1.0",
      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-2.1.0.tgz",
      "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==",
      "license": "MIT"
    },
    "node_modules/mlly": {
      "version": "1.7.3",
@@ -3128,6 +3134,15 @@
        }
      }
    },
    "node_modules/vue-observe-visibility": {
      "version": "2.0.0-alpha.1",
      "resolved": "https://registry.npmmirror.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz",
      "integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==",
      "license": "MIT",
      "peerDependencies": {
        "vue": "^3.0.0"
      }
    },
    "node_modules/vue-qrcode-reader": {
      "version": "5.6.0",
      "resolved": "https://registry.npmmirror.com/vue-qrcode-reader/-/vue-qrcode-reader-5.6.0.tgz",
@@ -3141,6 +3156,15 @@
        "node": ">=18.0.0"
      }
    },
    "node_modules/vue-resize": {
      "version": "2.0.0-alpha.1",
      "resolved": "https://registry.npmmirror.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
      "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
      "license": "MIT",
      "peerDependencies": {
        "vue": "^3.0.0"
      }
    },
    "node_modules/vue-router": {
      "version": "4.4.5",
      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.5.tgz",
@@ -3156,6 +3180,20 @@
        "vue": "^3.2.0"
      }
    },
    "node_modules/vue-virtual-scroller": {
      "version": "2.0.0-beta.8",
      "resolved": "https://registry.npmmirror.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz",
      "integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==",
      "license": "MIT",
      "dependencies": {
        "mitt": "^2.1.0",
        "vue-observe-visibility": "^2.0.0-alpha.1",
        "vue-resize": "^2.0.0-alpha.1"
      },
      "peerDependencies": {
        "vue": "^3.2.0"
      }
    },
    "node_modules/webpack-virtual-modules": {
      "version": "0.6.2",
      "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
package.json
@@ -19,12 +19,12 @@
    "@capacitor/browser": "^6.0.4",
    "@capacitor/camera": "^6.1.2",
    "@capacitor/core": "6.2.0",
    "@capacitor/filesystem": "^6.0.3",
    "@capacitor/geolocation": "^6.1.0",
    "@ionic-native/bluetooth-le": "^5.36.0",
    "@ionic-native/bluetooth-serial": "^5.36.0",
    "@ionic-native/http": "^5.36.0",
    "axios": "^1.7.2",
    "buffer": "^6.0.3",
    "cordova-plugin-bluetooth-serial": "^0.4.7",
    "crc": "^4.3.2",
    "echarts": "^5.5.1",
    "element-plus": "^2.7.3",
@@ -36,7 +36,8 @@
    "videojs-flash": "^2.2.1",
    "vue": "^3.5.12",
    "vue-qrcode-reader": "^5.6.0",
    "vue-router": "^4.3.0"
    "vue-router": "^4.3.0",
    "vue-virtual-scroller": "^2.0.0-beta.8"
  },
  "devDependencies": {
    "@element-plus/icons": "^0.0.11",
src/assets/js/axios.js
@@ -1,40 +1,43 @@
import axios from "axios";
import pinia from './pinia.js';
import { storeToRefs } from 'pinia'
import { useUserStore } from "@/stores/user.js";
const { serverIp } = storeToRefs(useUserStore(pinia));
import config from "./config";
const { serviceIp } = config;
if (process.env.NODE_ENV == "development") {
  // 跨域请求
  axios.defaults.baseURL = "http://localhost:8100/bl/";
  axios.defaults.withCredentials = true; // 保持请求头
} else {
  axios.defaults.baseURL = location.protocol + "//" + serviceIp + ":8100/bl/";
  // axios.defaults.baseURL = 'http:' + "//" + serviceIp + ":8100/bl/";
  // axios.defaults.baseURL = location.protocol + "//" + serviceIp + ":8443/bl/";
}
// 添加请求拦截器
axios.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
const axiosInstance = axios.create();
axiosInstance.defaults.withCredentials = true; // 保持请求头
axiosInstance.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么,例如添加 token
    return config;
  },
  function (error) {
  error => {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);
// 添加响应拦截器
axios.interceptors.response.use(
  function (response) {
// 响应拦截器
axiosInstance.interceptors.response.use(
  response => {
    // 对响应数据做点什么
    return response;
  },
  function (error) {
  error => {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);
export default axios;
export default async function request(config) {
  // 构建完整的 URL
  const fullUrl = `${location.protocol}//${serverIp.value}:8100/bl/${config.url}`; // 直接使用 userStore.serverIp
  // 发送请求,使用提取或默认的配置
  const response = await axiosInstance({
    ...config,
    url: fullUrl, // 覆盖原始配置中的 url
  });
  return response;
}
src/assets/js/getWsUrl.js
@@ -1,6 +1,8 @@
import config from "./config";
const { serviceIp } = config;
// import config from "./config";
// const { serviceIp } = config;
import { useUserStore } from "@/stores/user.js";
import pinia from './pinia.js';
const {  serverIp } = useUserStore(pinia);
/**
 * 获取Websocket的连接
 * @param action
@@ -13,12 +15,14 @@
  if (window.location.protocol == "https:") {
    wsProtocol = "wss://";
  }
  if (process.env.NODE_ENV == "development") {
    hostname = "localhost";
  } else {
    hostname = serviceIp;
    _port = window.location.port;
  }
  // if (process.env.NODE_ENV == "development") {
  //   hostname = "localhost";
  // } else {
  // }
  hostname = serverIp;
  // _port = window.location.port;
  // 处理端口为80
  _port = _port == 80 ? "" : ":" + _port;
  return wsProtocol + hostname + _port + "/bl/" + action;
src/assets/js/permissionUtils.js
New file
@@ -0,0 +1,82 @@
import { Capacitor } from '@capacitor/core'; // 如果你使用的是Capacitor
import { Geolocation } from '@capacitor/geolocation';
import { BluetoothLe } from '@capacitor-community/bluetooth-le';
export default async function requestLocationPermission() {
  // 对于 Android,请求 BLUETOOTH 和 BLUETOOTH_CONNECT 权限
  if (Capacitor.getPlatform() === 'android') {
    const permissionStatus = await Geolocation.requestPermissions();
    // console.log('BluetoothLe.requestPermissions', BluetoothLe, BluetoothLe.requestPermissions, '=============');
    try {
      const permissions = await BluetoothLe.requestPermissions();
      console.log('permissions', permissions, '=============');
    } catch (error) {
      console.log('申请蓝牙权限失败', error, '=============');
    }
    // if (permissions.bleScan && permissions.bleAdvertise && permissions.bleConnect && permissionStatus.location === 'granted') {
    if (permissionStatus.location === 'granted') {
      // 权限已授予,获取位置
      // const position = await Geolocation.getCurrentPosition();
      // console.log('Latitude:', position.coords.latitude);
      // console.log('Longitude:', position.coords.longitude);
      console.log('位置权限已授予');
      return true;
    } else {
      console.log('位置权限未授予');
      return false;
    }
  }
  // if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') { // 如果你使用的是Capacitor
  //   let _premission = null;
  //   switch (type) {
  //     case 'scan':
  //       _premission = permissionInstance.PERMISSION.BLUETOOTH_SCAN;
  //       break;
  //     case 'connect':
  //       // _premission = permissionInstance.PERMISSION.BLUETOOTH
  //       _premission = 'android.permission.BLUETOOTH_CONNECT';
  //       break;
  //     case 'location':
  //       _premission = permissionInstance.PERMISSION.ACCESS_FINE_LOCATION;
  //       break;
  //   }
  //   // const androidPermissions = await import('@ionic-native/android-permissions/ngx').then(mod => mod.AndroidPermissions);
  //   // 检查是否已经获得位置权限
  //   const hasPermission = await permissionInstance.checkPermission(_premission);
  //   if (!hasPermission) {
  //     // 如果没有获得权限,则请求权限
  //     const result = await permissionInstance.requestPermission(_premission);
  //     console.log('result1312313123213213', result, '=============');
  //     // 根据请求结果执行相应操作
  //     if (result.hasPermission) {
  //       console.log('Location permission granted 12313213.');
  //       // 继续执行需要位置权限的逻辑
  //       return true;
  //     } else {
  //       console.error('Location permission denied.');
  //       // 处理权限被拒绝的情况
  //       // 注意:在某些情况下,用户可能选择了“不再询问”,此时可能需要引导用户到应用的设置页面手动授予权限
  //       return false;
  //     }
  //   } else {
  //     console.log('Location permission already granted.');
  //     // 继续执行需要位置权限的逻辑(如果之前没有执行的话)
  //     return true
  //   }
  // }
  // 对于iOS,通常不需要在这里显式请求权限,因为系统会在需要时提示用户
  // 但你可以在Info.plist中添加必要的权限描述以确保应用能够请求这些权限
}
src/assets/js/pinia.js
New file
@@ -0,0 +1,3 @@
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
src/assets/js/tools/throttle.js
New file
@@ -0,0 +1,18 @@
// 节流
export const throttle = function (fn, delay = 300){
  var lastTime, timer;
  return function () {
    var args = arguments;
    var nowTime = Date.now();
    if(lastTime && nowTime - lastTime < delay){
      if (timer) clearTimeout(timer);
      timer = setTimeout(function () {
        lastTime = nowTime;
        fn.apply(null, args);
      }, delay)
    }else{
      lastTime = nowTime;
      fn.apply(null, args);
    }
  }
}
src/assets/js/tree.js
New file
@@ -0,0 +1,37 @@
export function formatAreaTree(item, ids, list) {
  // parentId 不在id列表中;
  // if (item.parentId === 0) {
  if (ids.indexOf(item.parentId) === -1) {
    list.push({
      label: item.areaName,
      id: item.id,
      data: item,
      areaDescript: item.areaDescript,
      charger: item.areaUsers.map((v) => v.uname).join(","),
      children: [],
    });
  } else {
    let isCurrentChild = false;
    for (let i = 0; i < list.length; i++) {
      const listItem = list[i];
      if (listItem.id === item.parentId) {
        isCurrentChild = true;
        listItem.children.push({
          label: item.areaName,
          id: item.id,
          data: item,
          areaDescript: item.areaDescript,
          charger: item.areaUsers.map((v) => v.uname).join(","),
          children: [],
        });
      }
    }
    for (let i = 0; i < list.length; i++) {
      const listItem = list[i];
      if (!isCurrentChild && listItem.children !== 0) {
        formatAreaTree(item, ids, listItem.children);
      }
    }
  }
}
src/assets/js/uname.js
New file
@@ -0,0 +1,6 @@
import pinia from './pinia.js';
import { storeToRefs } from 'pinia'
import { useUserStore } from "@/stores/user.js";
const { uname } = storeToRefs(useUserStore(pinia));
export default uname;
src/components/icons/iconLock.vue
New file
@@ -0,0 +1,11 @@
<template>
  <svg viewBox="0 0 1024 1024" version="1.1" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"
    fill="currentColor">
    <path
      d="M786.9 319.1H609.5V189.8c0-34.9-28.4-63.4-63.3-63.4H289.5c-34.9 0-63.3 28.4-63.3 63.4v642.4c0 35 28.4 63.4 63.3 63.4h256.7c34.9 0 63.3-28.4 63.3-63.4V445.9h177.3c8.4 0 15.2-6.8 15.2-15.2v-96.4c0.1-8.3-6.7-15.2-15.1-15.2zM546.3 865.2H289.5c-18.2 0-32.9-14.8-32.9-33V189.8c0-18.2 14.8-33 32.9-33h256.7c18.1 0 32.9 14.8 32.9 33v129.3h-90.5c-18.1-20.4-43.5-32.1-70.8-32.1-52.6 0-95.4 42.8-95.4 95.5s42.8 95.5 95.4 95.5c27.3 0 52.7-11.7 70.8-32.1h90.5v386.3h0.1c0 18.2-14.8 33-32.9 33z m225.6-449.7H481.7c-0.3 0-0.6 0.2-0.9 0.2-1.1 0.1-2.2 0.3-3.2 0.7-0.9 0.3-1.7 0.5-2.5 0.9-0.9 0.4-1.6 1-2.4 1.6-0.8 0.6-1.6 1.2-2.2 2-0.2 0.3-0.6 0.4-0.8 0.7-12.5 16.6-31.3 26.1-51.7 26.1-35.9 0-65-29.2-65-65.1 0-35.9 29.2-65.1 65-65.1 20.3 0 39.2 9.5 51.7 26.1 0.2 0.3 0.5 0.4 0.8 0.7 0.7 0.8 1.5 1.4 2.4 2.1 0.8 0.6 1.5 1.1 2.3 1.6 0.8 0.4 1.7 0.6 2.6 0.9 1 0.3 2 0.6 3.1 0.6 0.3 0 0.6 0.2 1 0.2h290v65.8z">
    </path>
    <path
      d="M461.3 794.1c2.9-3.2 4.3-7.5 3.8-11.8l-10.2-91.2c14.5-9.1 26.3-23 26.3-51.6 0-15.9-5.9-30.4-15.6-41.6-11.6-13.4-28.7-21.8-47.7-21.8-34.9 0-63.3 28.4-63.3 63.4 0 20.6 9.9 39.3 26.3 51.1l-10.2 91.7c-0.5 4.3 0.9 8.6 3.8 11.8 2.9 3.2 7 5.1 11.3 5.1H450c4.3 0 8.4-1.8 11.3-5.1z m-37.7-110c0 0.1-0.1 0.3 0 0.4l9.4 84.4h-30.2v-0.1l9.3-83.2 0.1-1.2c0-0.2-0.1-0.3 0-0.4 0.1-1.2-0.1-2.3-0.3-3.4-0.1-0.8-0.1-1.7-0.4-2.5-0.3-1-0.9-1.8-1.4-2.6s-0.8-1.7-1.4-2.4c-0.6-0.7-1.4-1.2-2-1.7-0.8-0.7-1.6-1.4-2.6-2-0.1-0.1-0.2-0.2-0.4-0.3-1.9-0.9-3.6-2-5.2-3.1-8.5-6.1-13.5-15.8-13.5-26.5 0-16.8 12.6-30.7 28.8-32.7 1.4-0.2 2.7-0.3 4.1-0.3 18.2 0 32.9 14.8 32.9 33 0 18.5-6 23.4-18.7 29.6-0.1 0.1-0.2 0.2-0.4 0.3-1 0.5-1.8 1.3-2.6 2-0.7 0.6-1.4 1-2 1.7-0.6 0.7-1 1.6-1.5 2.5s-1 1.6-1.3 2.6c-0.3 0.8-0.2 1.7-0.4 2.5-0.2 1.1-0.4 2.2-0.3 3.4z">
    </path>
  </svg>
</template>
src/hooks/useBLELock.js
@@ -1,7 +1,7 @@
import { BluetoothLE } from "@ionic-native/bluetooth-le";
import { ref, onMounted } from "vue";
import { Buffer } from "buffer";
import requestLocationPermission from '@/assets/js/permissionUtils';
import CRC from "crc";
@@ -93,7 +93,12 @@
      return;
    }
    // 初始化蓝牙
    BluetoothLE.initialize().subscribe(
    BluetoothLE.initialize(
      {
        request: true, // 表示请求权限
        statusReceiver: false // 可以根据需要设置为 true 或 false
      }
    ).subscribe(
      (result) => {
        console.log("蓝牙初始化成功", result);
@@ -161,39 +166,71 @@
    }
  };
  const scan = async () => {
    if (!MAC.value) {
      console.log("MAC 为空");
      return false;
    }
    try {
      return new Promise(
        async (resolve, reject) =>
          await BluetoothLE.startScan().subscribe({
            next: (device) => {
              console.log("发现设备", device);
              if (
                device.address &&
                device.address.toUpperCase() === MAC.value
              ) {
                BluetoothLE.stopScan();
                resolve(device);
              }
            },
            error: (error) => {
              console.error("扫描设备失败", error);
              resolve(false);
            },
            complete: () => {
              console.log("扫描设备完成");
            },
          })
      );
    } catch (error) {
      console.log("扫描失败", error);
      return false;
    }
  };
  // const scan = async () => {
  //   if (!MAC.value) {
  //     console.log("MAC 为空");
  //     return false;
  //   }
  //   try {
  //     return new Promise(
  //       async (resolve, reject) =>
  //         await BluetoothLE.startScan().subscribe({
  //           next: (device) => {
  //             console.log("发现设备", device);
  //             if (
  //               device.address &&
  //               device.address.toUpperCase() === MAC.value
  //             ) {
  //               BluetoothLE.stopScan();
  //               resolve(device);
  //             }
  //           },
  //           error: (error) => {
  //             console.error("扫描设备失败", error);
  //             resolve(false);
  //           },
  //           complete: () => {
  //             console.log("扫描设备完成");
  //           },
  //         })
  //     );
  //   } catch (error) {
  //     console.log("扫描失败", error);
  //     return false;
  //   }
  // };
  // const scan = async () => {
  //   if (!MAC.value) {
  //     console.log("MAC 为空");
  //     return false;
  //   }
  //   try {
  //     let deviceFound = null;
  //     const subscription = BluetoothLE.startScan().subscribe({
  //       next: (device) => {
  //         console.log("发现设备", device);
  //         if (device.address && device.address.toUpperCase() === MAC.value) {
  //           BluetoothLE.stopScan();
  //           deviceFound = device;
  //         }
  //       },
  //       error: (error) => {
  //         console.error("扫描设备失败", error);
  //         deviceFound = false;
  //       },
  //       complete: () => {
  //         console.log("扫描设备完成");
  //       }
  //     });
  //     // 等待一段时间,例如 10 秒
  //     await new Promise(resolve => setTimeout(resolve, 10000));
  //     subscription.unsubscribe();
  //     return deviceFound;
  //   } catch (error) {
  //     console.log("扫描失败", error);
  //     return false;
  //   }
  // };
  // 发现服务和 发现特征
  function getServer() {
@@ -280,9 +317,11 @@
    })
      .then(() => {
        console.log("写入数据成功");
        return true;
      })
      .catch((error) => {
        console.log("写入数据失败", error);
        return false;
      });
  }
@@ -308,29 +347,39 @@
    }
    // TODO 鉴权
    let a = await scan();
    console.log("a", a, "=============");
    if (!a || !a.address || a.address.toUpperCase() != MAC.value) {
      console.log("未发现设备", "=============");
    // let a = await scan();
    // console.log("a", a, "=============");
    // if (!a || !a.address || a.address.toUpperCase() != MAC.value) {
    //   console.log("未发现设备", "=============");
    //   return false;
    // }
    let hasPermission = await requestLocationPermission();
    if (!hasPermission) {
      console.log("用户没有授予蓝牙连接权限", "=============");
      return false;
    }
    let connected = await connect();
    let res = true;
    console.log('connected', connected, '=============');
    if (!connected) {
      console.log("连接失败", "=============");
      return false;
      res = false;
    }
    let serv = await getServer();
    console.log("serv", serv, "=============");
    let readRes = await read();
    console.log('readRes', readRes, '=============');
    let writeRes = await write();
    console.log("writeRes", writeRes, "=============");
    if (!writeRes) {
      console.log("写入失败", "=============");
      res = false;
    }
    disconnect();
    close();
    setMac("");
    return res;
    // return new Promise( async (resolve, reject) => {
    //   // let connected = await connect();
    //   // console.log("connected", connected, "=============");
@@ -349,5 +398,5 @@
    _initBL();
  });
  return { read, write, close, scan, connect, disconnect, open, setMac };
  return { read, write, close, connect, disconnect, open, setMac };
};
src/layout/apis.js
New file
@@ -0,0 +1,32 @@
import axios from '@/assets/js/axios';
import uname from '@/assets/js/uname';
/**
 * 获取当前用户是否有指定mac地址的锁具的蓝牙开锁权限
 * mac 全部转为大写
 */
export function getPrivilegeByMac(mac) {
  return axios({
    method: "GET",
    url: "authInf/getAuthByUidAndMac",
    params: {
      mac,
      uname: uname.value
    }
  });
}
/**
 * 开锁后 告诉后台开锁状态 便于记录
 */
export function setLogByUid(mac, result) {
  return axios({
    method: "GET",
    url: "app/setLogByUid",
    params: {
      mac,
      result,
      uname: uname.value
    }
  });
}
src/layout/index.vue
@@ -1,76 +1,137 @@
<script setup>
import { ref } from "vue";
import { RouterView } from "vue-router";
import scanView from "../views/home/viewer.vue";
import iconBluetooth from "@/components/icons/iconBluetooth.vue";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import useBLELock from "@/hooks/useBLELock";
import useElemnetVant from "@/hooks/useElementVant.js";
const { $loading , $toast} = useElemnetVant();
const active = ref("home");
const viewerVisible = ref(false);
const { open, setMac, close } = useBLELock();
    import { ref, watch } from "vue";
    import { RouterView, useRoute } from "vue-router";
    import scanView from "../views/home/viewer.vue";
    import iconBluetooth from "@/components/icons/iconBluetooth.vue";
    import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
    import useBLELock from "@/hooks/useBLELock";
    import useElemnetVant from "@/hooks/useElementVant.js";
    import { useUserStore } from "@/stores/user.js";
    import { getPrivilegeByMac, setLogByUid } from './apis';
    const { $loading, $toast } = useElemnetVant();
    const active = ref("home");
    const viewerVisible = ref(false);
    const { open, setMac, close } = useBLELock();
    const route = useRoute();
    const { urole } = useUserStore();
// 扫码成功 (文件/string)  得到蓝牙Mac; 请求后台是否有此锁的权限? 有就连接 开锁; 没有就提示
const gotQrCode = (data) => {
  // state.qrCodeVisible = false
  viewerVisible.value = false;
  // if (!_.isEmpty(data)) {
  // audio.play();
  // 处理结果
  // state.result = data
  console.log("data", data, "=============");
  // 验证是我们的蓝牙Mac
  let reg = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/;
  if (!reg.test(data)) {
    $toast("不是我们的锁具的二维码 无法开锁");
    return false;
  }
  setMac(data);
  let loading = $loading();
  // close();
  open()
    .then((res) => {
      loading.close();
    })
    .catch((err) => {
      console.log(err);
      loading.close();
    });
    // 扫码成功 (文件/string)  得到蓝牙Mac; 请求后台是否有此锁的权限? 有就连接 开锁; 没有就提示
    const gotQrCode = async (data) => {
        // state.qrCodeVisible = false
        viewerVisible.value = false;
        // if (!_.isEmpty(data)) {
        // audio.play();
        // 处理结果
        // state.result = data
        console.log("data", data, "=============");
        // 验证是我们的蓝牙Mac
        let reg = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/;
        if (!reg.test(data)) {
            $toast("二维码有误 无法开锁");
            return false;
        }
  // 这里可以进行后续处理,比如发送请求...
  // }
};
        $toast('正在查询权限');
        let pri = await getPrivilege(data.toUpperCase());
        if (!pri) {
            $toast('没有权限, 拒绝开锁');
            setLogByUid(data.toUpperCase(), 0);
            return false;
        }
async function checkCameraPermissions() {
  try {
    const permissionStatus = await Camera.checkPermissions();
    console.log("相机权限检查结果:", permissionStatus, "=============");
        $toast('权限验证通过, 正在开锁');
        setMac(data);
        let loading = $loading();
        // close();
        open()
            .then((res) => {
                loading.close();
                if (res) {
                    setLogByUid(data.toUpperCase(), 1);
                    $toast('开锁成功');
                } else {
                    $toast('开锁失败');
                    setLogByUid(data.toUpperCase(), 0);
                }
            })
            .catch((err) => {
                console.log(err);
                $toast('开锁失败');
                setLogByUid(data.toUpperCase(), 0);
                loading.close();
            });
    if (permissionStatus.camera === "granted") {
      // 相机权限已授予,可以进行相机操作
      console.log("相机权限已授予");
    } else if (permissionStatus.camera === "denied") {
      // 相机权限被拒绝,你可能需要提示用户开启权限
      console.log("相机权限被拒绝");
    } else if (permissionStatus.camera === "prompt") {
      // 可能需要重新请求权限,这可能是因为权限状态不确定
      console.log("可能需要重新请求相机权限");
    }
  } catch (error) {
    console.error("检查相机权限时出错:", error);
  }
}
        // 这里可以进行后续处理,比如发送请求...
        // }
    };
/**
 * 点蓝牙开锁 先扫描二维码 获取到设备id 然后再判断有没有权限开锁
 */
async function scanQr() {
  console.log("scanQr");
  await checkCameraPermissions();
  viewerVisible.value = true;
}
    async function getPrivilege(data) {
        let res = await getPrivilegeByMac(data);
        let { code, data: result } = res.data;
        if (code) {
            return result;
        } else {
            return false;
        }
    }
    async function checkCameraPermissions() {
        try {
            const permissionStatus = await Camera.checkPermissions();
            // console.log("相机权限检查结果:", permissionStatus, "=============");
            if (permissionStatus.camera === "granted") {
                // 相机权限已授予,可以进行相机操作
                console.log("相机权限已授予");
            } else if (permissionStatus.camera === "denied") {
                // 相机权限被拒绝,你可能需要提示用户开启权限
                console.log("相机权限被拒绝");
            } else if (permissionStatus.camera === "prompt") {
                // 可能需要重新请求权限,这可能是因为权限状态不确定
                console.log("可能需要重新请求相机权限");
            }
        } catch (error) {
            console.error("检查相机权限时出错:", error);
        }
    }
    /**
     * 点蓝牙开锁 先扫描二维码 获取到设备id 然后再判断有没有权限开锁
     */
    async function scanQr() {
        console.log("scanQr");
        await checkCameraPermissions();
        viewerVisible.value = true;
    }
    // 监听路由变化
    watch(
        () => route.path,
        (newPath) => {
            switch (newPath) {
                case "/home":
                    active.value = "home";
                    break;
                case "/monitor":
                    active.value = "search";
                    break;
                case "/user":
                    active.value = "user";
                    break;
                case '/alarm':
                    active.value = 'alarm';
                    break;
                default:
                    active.value = "home";
            }
        },
        { immediate: true }
    );
</script>
<template>
@@ -82,20 +143,17 @@
    </div>
    <div class="footer">
      <van-tabbar v-model="active" :fixed="false">
        <van-tabbar-item name="home" icon="home-o">首页</van-tabbar-item>
        <van-tabbar-item name="search" icon="search">监控</van-tabbar-item>
        <van-tabbar-item name="lock" @click="scanQr"
          ><span>蓝牙开锁</span
          ><template #icon
            ><el-icon class="ico"><icon-bluetooth /></el-icon></template
        ></van-tabbar-item>
        <van-tabbar-item name="alarm" icon="warning-o">告警</van-tabbar-item>
        <van-tabbar-item name="user" icon="user-o">我的</van-tabbar-item>
        <van-tabbar-item name="home" replace to="/home" icon="home-o" v-if="urole > 0">首页</van-tabbar-item>
        <van-tabbar-item name="search" replace to="/monitor" icon="search">监控</van-tabbar-item>
        <van-tabbar-item name="lock" @click="scanQr"><span>蓝牙开锁</span><template #icon><el-icon
              class="ico"><icon-bluetooth /></el-icon></template></van-tabbar-item>
        <van-tabbar-item name="alarm" replace to="/alarm" icon="warning-o" v-if="urole > 0">告警</van-tabbar-item>
        <van-tabbar-item name="user" replace to="/user" icon="user-o">我的</van-tabbar-item>
      </van-tabbar>
    </div>
    <!-- 扫描 -->
    <div class="viewer" v-if="viewerVisible">
      <scan-view @on-success="gotQrCode" />
      <scan-view @on-success="gotQrCode"  @on-close="viewerVisible = false" />
    </div>
  </div>
</template>
@@ -107,15 +165,19 @@
  display: flex;
  color: #000;
  flex-direction: column;
  .p-contain {
    flex: 1;
    position: relative;
  }
  .footer {
    margin-top: 12px;
    :deep(.van-tabbar::after) {
      content: none;
    }
    .ico {
      padding: 4px;
      color: #fff;
@@ -126,6 +188,7 @@
      outline: 6px solid rgba(18, 150, 219, 0.3);
    }
  }
  .viewer {
    position: fixed;
    z-index: 9;
@@ -133,6 +196,7 @@
    top: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
  }
}
</style>
src/main.js
@@ -2,15 +2,19 @@
import "./styles/global.less";
import { createApp } from "vue";
import { createPinia } from "pinia";
// import { createPinia } from "pinia";
import pinia from "./assets/js/pinia.js";
import ElementPlus from "element-plus";
import VueVirtualScroller from 'vue-virtual-scroller'
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import "element-plus/dist/index.css";
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import zhCn from "element-plus/es/locale/lang/zh-cn";
import { BluetoothLE } from '@ionic-native/bluetooth-le';
// import { BluetoothLE } from '@ionic-native/bluetooth-le';
import "@/assets/js/axios";
// import "@/assets/js/axios";
import "@/permission";
// Toast
@@ -32,17 +36,20 @@
import App from "./App.vue";
import router from "./router";
import "@/permission";
const app = createApp(App);
app.use(BluetoothLE);
// app.use(BluetoothLE);
app.use(VueVirtualScroller);
// app.config.globalProperties.$message = ElMessage;
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
app.use(createPinia());
// app.use(createPinia());
app.use(pinia);
// app.use(ElementPlus);
app.use(ElementPlus, { locale: zhCn });
app.use(router);
src/permission.js
@@ -1,9 +1,9 @@
import router from './router'
import { ElMessage } from "element-plus";
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// import NProgress from 'nprogress' // progress bar
// import 'nprogress/nprogress.css' // progress bar style
NProgress.configure({ showSpinner: false }) // NProgress Configuration
// NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async (to, from, next) => {
@@ -14,7 +14,7 @@
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
      // NProgress.done()
    } else {
      if (username) {
        next()
@@ -24,7 +24,7 @@
        } catch (error) {
          ElMessage.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
          // NProgress.done()
        }
      }
    }
@@ -34,12 +34,12 @@
      next()
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
      // NProgress.done()
    }
  }
})
router.afterEach(() => {
  // finish progress bar
  NProgress.done()
  // NProgress.done()
})
src/router/index.js
@@ -15,9 +15,27 @@
      component: layout,
      children: [
        {
          path: "/home",
          path: "home",
          name: "home",
          component: () => import("@/views/home/index.vue"),
        },
        // 监控
        {
          path: "monitor",
          name: "monitor",
          component: () => import("@/views/monitor/index.vue"),
        },
        // 告警
        {
          path: "alarm",
          name: "alarm",
          component: () => import("@/views/alarm/index.vue"),
        },
        // 用户
        {
          path: "user",
          name: "user",
          component: () => import("@/views/user/index.vue"),
        },
      ],
    },
@@ -27,12 +45,6 @@
      name: "login",
      component: () => import("@/views/login/index.vue"),
    },
    // 测试
    // {
    //   path: "/test",
    //   name: "test",
    //   component: test,
    // },
  ],
});
src/stores/user.js
@@ -4,7 +4,9 @@
  state() {
    return {
      uid: localStorage.getItem("uid"),
      uname: localStorage.getItem("uname")
      uname: localStorage.getItem("uname"),
      urole: localStorage.getItem("urole"),
      serverIp: localStorage.getItem("serverIp") || '192.168.10.82',
    };
  },
  actions: {
@@ -16,6 +18,14 @@
      this.uid = value;
      localStorage.setItem("uid", value);
    },
    setRole(value) {
      this.urole = value;
      localStorage.setItem("urole", value);
    },
    setIp(ip) {
      this.serverIp = ip;
      localStorage.setItem("serverIp", ip);
    }
  },
  getters: {},
});
src/views/alarm/apis.js
New file
@@ -0,0 +1,17 @@
import axios from '@/assets/js/axios';
import uname from '@/assets/js/uname';
/**
 * 查询区域管理员点击查看开锁操作异常记录
 */
export function getAlarmList(pageNum, pageSize) {
  return axios({
    url: 'app/getCtlog',
    method: 'GET',
    params: {
      pageNum,
      pageSize,
      uname: uname.value
    }
  });
}
src/views/alarm/index.vue
New file
@@ -0,0 +1,264 @@
<script setup>
    import { ref, onMounted } from "vue";
    import { getAlarmList } from './apis';
    import { throttle } from '@/assets/js/tools/throttle';
    const sum = ref(0);
    const errorNum = ref(0);
    const pageCurr = ref(1);
    const pageSize = 10;
    const loading = ref(false);
    const hasNextPage = ref(false);
    const list = ref([]);
    function getList() {
        loading.value = true;
        getAlarmList(pageCurr.value, pageSize).then((res) => {
            let { code, data, data2 } = res.data;
            let _list = [];
            let _sum = 0,
                _error = 0;
            let hasNext = false;
            if (code && data) {
                console.log(data2);
                _sum = data2.sumLog;
                _error = data2.errorLogNum;
                let logs = data2.allLogs;
                _list = logs.list.map(v => {
                    v.lockPath = v.areaInf?.areaPath;
                    return v;
                });
                hasNext = logs.hasNextPage;
            }
            list.value = [...list.value, ..._list];
            sum.value = _sum;
            errorNum.value = _error;
            hasNextPage.value = hasNext;
            loading.value = false;
        })
            .catch((err) => {
                loading.value = false;
                console.log(err);
            });
    }
    const handleScroll = (event) => {
        const scroller = event.target;
        if (loading.value) {
            return true;
        }
        // 是否有下一页 如果没有则退出
        if (!hasNextPage.value) {
            return false;
        }
        if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 100) {
            // 滚动位置接近底部(这里设置了100px的缓冲距离),加载下一页数据
            loadNextPage();
        }
    };
    const throttleHandleScroll = throttle(handleScroll, 1500);
    function loadNextPage() {
        pageCurr.value++;
        getList();
    }
    onMounted(() => {
        getList();
    });
</script>
<template>
  <div class="p-alarm">
    <div class="title">实时告警</div>
    <div class="info">
      <div class="item">
        <div class="label">异常告警</div>
        <div class="value">{{ sum }}</div>
      </div>
      <div class="item">
        <div class="label">开锁异常</div>
        <div class="value">{{ errorNum }}</div>
      </div>
      <div class="item">
        <div class="label">关锁异常</div>
        <div class="value">--</div>
      </div>
    </div>
    <div class="main">
      <div class="scroller">
        <RecycleScroller :class="['rec-scroller', {loading: loading, 'no-more': !loading && !hasNextPage}]" :items="list" :item-size="100" @touchmove="throttleHandleScroll" key-field="num"
          v-slot="{ item }">
          <div class="item">
            <div class="station">{{ item.lockPath }}</div>
            <div class="icon"></div>
            <div class="lock-name">{{item.lockName}}</div>
            <div class="lock-id">{{ item.lockId }}</div>
            <div class="event">开锁异常</div>
            <div class="date-time">{{ item.ctlTime }}</div>
          </div>
        </RecycleScroller>
      </div>
      <!-- 加载中 -->
      <!-- <van-loading v-if="loading" size="24px">加载中...</van-loading> -->
      <!-- 到底了 -->
      <!-- <div class="no-more" v-if="!loading && !hasNextPage">没有更多了</div> -->
    </div>
  </div>
</template>
<style scoped lang="less">
.p-alarm {
  height: 100%;
  background: #f2f2f2 linear-gradient(#81d3f8, #81d3f8) top center / 100% 20% no-repeat;
  display: flex;
  flex-direction: column;
  .title {
    height: 50px;
    line-height: 50px;
    text-align: center;
    background: #81d3f8;
    color: #027db4;
    font-size: 16px;
    font-weight: bold;
  }
  .info {
    display: flex;
    justify-content: space-around;
    margin: 0 1.6em;
    padding: 10px 0;
    // background: #81d3f8;
    background: #fff;
    border-radius: 10px 10px 0 0;
    color: #f00;
    font-size: 14px;
    font-weight: bold;
    .item {
      display: flex;
      align-items: center;
      font-size: 14px;
      .label {
        // color: #090;
        &::after {
          content: ':';
        }
      }
      .value {
        margin-left: 0.6em;
        // color: #027db4;
        font-weight: bold;
      }
    }
  }
  .main {
    flex: 1;
    position: relative;
    background: #ddd;
    .scroller {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      overflow-y: auto;
    }
    .rec-scroller {
      height: 100%;
      &.loading {
        &::after {
          content: '加载中...';
          display: block;
          text-align: center;
          color: #999;
          padding-bottom: 1em;
        }
      }
      &.no-more {
        &::after {
          content: '--没有更多了--';
          display: block;
          text-align: center;
          color: #999;
          padding-bottom: 1em;
        }
      }
    }
    .item {
      background: #f0f0f0;
      padding: 10px 0;
      margin-bottom: 10px;
      display: grid;
      gap: 4px;
      grid-template-columns: 1fr 6fr 4fr;
      grid-auto-rows: 24px 1.3fr 1fr;
      &::before {
        content: '';
      }
      .station {
        // align-self: center;
        grid-column: 1 e('/') 3;
        grid-row: 1 e('/') 2;
        font-size: 12px;
        display: flex;
        align-items: center;
        background: #81d3f8;
        border-radius: 0 20px 20px 0;
        color: #333;
        padding-left: 0.4em;
      }
      .icon {
        grid-column: 1 e('/') 2;
        grid-row: 2 e('/') 4;
        border-radius: 50%;
        background: url("data:image/svg+xml,%3csvg viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M786.9 319.1H609.5V189.8c0-34.9-28.4-63.4-63.3-63.4H289.5c-34.9 0-63.3 28.4-63.3 63.4v642.4c0 35 28.4 63.4 63.3 63.4h256.7c34.9 0 63.3-28.4 63.3-63.4V445.9h177.3c8.4 0 15.2-6.8 15.2-15.2v-96.4c0.1-8.3-6.7-15.2-15.1-15.2zM546.3 865.2H289.5c-18.2 0-32.9-14.8-32.9-33V189.8c0-18.2 14.8-33 32.9-33h256.7c18.1 0 32.9 14.8 32.9 33v129.3h-90.5c-18.1-20.4-43.5-32.1-70.8-32.1-52.6 0-95.4 42.8-95.4 95.5s42.8 95.5 95.4 95.5c27.3 0 52.7-11.7 70.8-32.1h90.5v386.3h0.1c0 18.2-14.8 33-32.9 33z m225.6-449.7H481.7c-0.3 0-0.6 0.2-0.9 0.2-1.1 0.1-2.2 0.3-3.2 0.7-0.9 0.3-1.7 0.5-2.5 0.9-0.9 0.4-1.6 1-2.4 1.6-0.8 0.6-1.6 1.2-2.2 2-0.2 0.3-0.6 0.4-0.8 0.7-12.5 16.6-31.3 26.1-51.7 26.1-35.9 0-65-29.2-65-65.1 0-35.9 29.2-65.1 65-65.1 20.3 0 39.2 9.5 51.7 26.1 0.2 0.3 0.5 0.4 0.8 0.7 0.7 0.8 1.5 1.4 2.4 2.1 0.8 0.6 1.5 1.1 2.3 1.6 0.8 0.4 1.7 0.6 2.6 0.9 1 0.3 2 0.6 3.1 0.6 0.3 0 0.6 0.2 1 0.2h290v65.8z' fill='%232F3C42' %3e%3c/path%3e%3cpath d='M461.3 794.1c2.9-3.2 4.3-7.5 3.8-11.8l-10.2-91.2c14.5-9.1 26.3-23 26.3-51.6 0-15.9-5.9-30.4-15.6-41.6-11.6-13.4-28.7-21.8-47.7-21.8-34.9 0-63.3 28.4-63.3 63.4 0 20.6 9.9 39.3 26.3 51.1l-10.2 91.7c-0.5 4.3 0.9 8.6 3.8 11.8 2.9 3.2 7 5.1 11.3 5.1H450c4.3 0 8.4-1.8 11.3-5.1z m-37.7-110c0 0.1-0.1 0.3 0 0.4l9.4 84.4h-30.2v-0.1l9.3-83.2 0.1-1.2c0-0.2-0.1-0.3 0-0.4 0.1-1.2-0.1-2.3-0.3-3.4-0.1-0.8-0.1-1.7-0.4-2.5-0.3-1-0.9-1.8-1.4-2.6s-0.8-1.7-1.4-2.4c-0.6-0.7-1.4-1.2-2-1.7-0.8-0.7-1.6-1.4-2.6-2-0.1-0.1-0.2-0.2-0.4-0.3-1.9-0.9-3.6-2-5.2-3.1-8.5-6.1-13.5-15.8-13.5-26.5 0-16.8 12.6-30.7 28.8-32.7 1.4-0.2 2.7-0.3 4.1-0.3 18.2 0 32.9 14.8 32.9 33 0 18.5-6 23.4-18.7 29.6-0.1 0.1-0.2 0.2-0.4 0.3-1 0.5-1.8 1.3-2.6 2-0.7 0.6-1.4 1-2 1.7-0.6 0.7-1 1.6-1.5 2.5s-1 1.6-1.3 2.6c-0.3 0.8-0.2 1.7-0.4 2.5-0.2 1.1-0.4 2.2-0.3 3.4z' fill='%232F3C42'%3e%3c/path%3e%3c/svg%3e") center center / contain no-repeat;
        // margin: 0 10px;
      }
      .lock-name {
        grid-area: 2 e('/') 2 e('/') 3 e('/') 3;
        font-size: 16px;
        color: #000;
        font-weight: bold;
      }
      .lock-id {
        grid-area: 3 e('/') 2 e('/') 4 e('/') 3;
        font-size: 12px;
        color: #999;
      }
      .event {
        font-size: 14px;
        color: #f00;
        margin: 0 10px;
      }
      .date-time {
        font-size: 12px;
        color: #999;
      }
    }
  }
}
</style>
src/views/home/apis.js
New file
@@ -0,0 +1,33 @@
import axios from '@/assets/js/axios';
import uname from '@/assets/js/uname';
/**
 * 锁具动态 分页 首页用查第一页
 */
export function getAllLogByUid(pageNum, pageSize) {
  return axios({
    url: 'app/getAllLogByUid',
    method: 'GET',
    params: {
      uname: uname.value,
      pageNum,
      pageSize
    }
  });
}
/**
 * 锁具告警 分页 首页用查第一页
 */
export function getErrLogByUid(pageNum, pageSize) {
  return axios({
    url: 'app/getErrLogByUid',
    method: 'GET',
    params: {
      uname: uname.value,
      pageNum,
      pageSize
    }
  });
}
src/views/home/index.vue
@@ -1,11 +1,108 @@
<script setup>
import { ref, onMounted, inject, watch, reactive } from "vue";
import { useRouter } from "vue-router";
    import { ref, onMounted, inject, watch, reactive } from "vue";
    import { useRouter } from "vue-router";
    import { getAllLogByUid, getErrLogByUid } from './apis';
    import { getAreaLockById, getAinfByManage } from '../monitor/apis';
    const router = useRouter();
    const pageSize = 10;
    const logList = ref([]);
    const alarmList = ref([]);
    const areaNum = ref(0);
    const sum = ref(0);
    const num_open = ref(0);
    const num_close = ref(0);
    async function getLogs() {
        try {
            let res = await getAllLogByUid(1, pageSize);
            console.log('res', res, '=============');
            const { code, data, data2 } = res.data;
            let list = [];
            if (code && data) {
                list = data2.list;
            }
            logList.value = list;
const router = useRouter();
        } catch (error) {
            console.log('error', error, '=============');
        }
    }
    async function getAlarms() {
        try {
            let res = await getErrLogByUid(1, pageSize);
            console.log('res', res, '=============');
            const { code, data, data2 } = res.data;
            let list = [];
            if (code && data) {
                list = data2.list;
            }
            alarmList.value = list;
        } catch (error) {
            console.log('error', error, '=============');
        }
    }
    async function getAreas() {
        try {
            let res = await getAinfByManage();
            const { code, data, data2 } = res.data;
            let _data = [];
        let len = 0;
            if (code && data) {
                _data = data2;
        len = data2.length;
            }
      areaNum.value = len;
            if (len) {
                getLocks(_data[0].id);
            }
        } catch (error) {
            console.log('error', error, '=============');
        }
    }
    function getLocks(id) {
        getAreaLockById(id).then((res) => {
            let { code, data, data2 } = res.data;
            let _sum = 0,
                _num_open = 0,
                _num_close = 0,
                _num_online = 0,
                _num_unLoad = 0,
                _num_offline = 0;
            let list = [];
            if (code && data) {
                _sum = data2.sumLinf;
                _num_open = data2.openNum;
                _num_close = data2.closeNum;
                _num_online = data2.onlineNum;
                _num_offline = data2.offLineNum;
                _num_unLoad = data2.unLoadNum;
            }
            sum.value = _sum;
            num_open.value = _num_open;
            num_close.value = _num_close;
            // num_online.value = _num_online;
            // num_offline.value = _num_offline;
            // num_unLoad.value = _num_unLoad;
        })
            .catch((err) => {
                console.log(err);
            });
    }
    onMounted(() => {
        getLogs();
        getAlarms();
        getAreas();
    });
</script>
@@ -14,7 +111,7 @@
    <!-- 头部 -->
    <div class="card header">
      <div class="title">鸿蒙电子智能锁</div>
      <div class="">
        <span>安全智能</span>
        <span>操作便捷</span>
@@ -26,23 +123,23 @@
    <div class="card disc">
      <div class="dis-item">
        <div class="icon house"></div>
        <div class="name">管理机房</div>
        <div class="num">4</div>
        <div class="name">管理区域</div>
        <div class="num">{{ areaNum }}</div>
      </div>
      <div class="dis-item">
        <div class="icon lock-ai"></div>
        <div class="name">管理锁具</div>
        <div class="num">4</div>
        <div class="num">{{ sum }}</div>
      </div>
      <div class="dis-item open">
        <div class="icon lock-open"></div>
        <div class="name">当前开启</div>
        <div class="num">4</div>
        <div class="num">{{ num_open }}</div>
      </div>
      <div class="dis-item close">
        <div class="icon lock-close"></div>
        <div class="name">当前关闭</div>
        <div class="num">4</div>
        <div class="num">{{ num_close }}</div>
      </div>
    </div>
    <!-- 锁具告警 -->
@@ -51,64 +148,27 @@
      <!-- 滚动区 -->
      <div class="scroll-wraper posR">
        <div class="scroll pos-full">
          <div class="alarm-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">开锁异常</div>
          </div>
          <div class="alarm-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">开锁异常</div>
          </div>
          <div class="alarm-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">开锁异常</div>
          </div>
          <div class="alarm-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
          <div class="alarm-item" v-for="(item, idx) in alarmList" :key="'alarm_' + idx">
            <div class="date">{{ item.ctlTime }}</div>
            <div class="id">{{ item.lockId }}</div>
            <div class="lockName">{{ item.lockName }}</div>
            <div class="state">开锁异常</div>
          </div>
        </div>
      </div>
      <!-- 更多告警 -->
      <a href="javascript:void(0);" class="">更多告警</a>
      <a href="javascript:void(0);" class="more" @click="router.push('/alarm')">更多告警</a>
    </div>
    <!-- 实时动态 -->
    <div class="card dynamic">
      <div class="title-level1">实时动态</div>
      <div class="scroll-wraper posR">
        <div class="scroll pos-full">
          <div class="rt-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">开锁成功</div>
          </div>
          <div class="rt-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">开锁成功</div>
          </div>
          <div class="rt-item">
            <div class="date">2024-11-06</div>
            <div class="time">17:25:10</div>
            <div class="id">ID2020000226514</div>
            <div class="lockName">蓝牙机柜锁1</div>
            <div class="state">闭锁成功</div>
          <div class="rt-item" v-for="(item, idx) in logList" :key="'rt_' + idx">
            <div class="date">{{ item.ctlTime }}</div>
            <div class="id">{{ item.lockId }}</div>
            <div class="lockName">{{ item.lockName }}</div>
            <div class="state">{{ item.ctlResult ? '开锁成功' : '开锁失败' }}</div>
          </div>
        </div>
      </div>
@@ -121,14 +181,17 @@
  height: 100%;
  display: flex;
  flex-direction: column;
  .card {
    margin-left: 6px;
    margin-right: 6px;
    margin-bottom: 12px;
    padding: 6px;
    background: #fff;
    border-radius: 6px;
    display: flex;
    flex-direction: column;
    &.header {
      display: flex;
      flex-direction: column;
@@ -136,30 +199,35 @@
      padding-left: 3em;
      font-size: 12px;
      color: #4f98f6;
      background: #81d3f8;
      background: #81d3f8 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24' %3e %3cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' color='currentColor' %3e %3cpath d='m12.308 18l-1.461-4.521a.72.72 0 0 0-.693-.479a.72.72 0 0 0-.693.479L8 18m7-5v5m-6.462-1.5h3.231' %3e%3c/path%3e %3cpath d='M4.268 18.845c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345' %3e%3c/path%3e %3cpath d='M7.5 9V6.5a4.5 4.5 0 0 1 9 0V9'%3e%3c/path%3e %3c/g%3e %3c/svg%3e") right center / auto 50% no-repeat;
      border-radius: 0;
      margin-left: 0;
      margin-right: 0;
      flex: 36;
      .title {
        font-weight: bold;
        font-size: 20px;
      }
      span {
        display: inline-block;
        margin-right: 2em;
      }
    }
    &.disc {
      flex: 19.6;
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 10px;
      .dis-item {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        .icon {
          background: #81d3f8;
          background-repeat: no-repeat;
@@ -168,34 +236,58 @@
          width: 18vw;
          height: 18vw;
          border-radius: 50%;
          &.house {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='currentColor' d='m12 5.15l-8 3.2V19h2v-6q0-.825.588-1.412T8 11h8q.825 0 1.413.588T18 13v6h2V8.35zM2 19V8.35q0-.625.338-1.125T3.25 6.5l8-3.2q.35-.15.75-.15t.75.15l8 3.2q.575.225.913.725T22 8.35V19q0 .825-.587 1.413T20 21h-4v-8H8v8H4q-.825 0-1.412-.587T2 19m7 2v-2h2v2zm2-3v-2h2v2zm2 3v-2h2v2zM8 11h8z'%3e%3c/path%3e%3c/svg%3e");
          }
          &.lock-ai {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24' %3e %3cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' color='currentColor' %3e %3cpath d='m12.308 18l-1.461-4.521a.72.72 0 0 0-.693-.479a.72.72 0 0 0-.693.479L8 18m7-5v5m-6.462-1.5h3.231' %3e%3c/path%3e %3cpath d='M4.268 18.845c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345' %3e%3c/path%3e %3cpath d='M7.5 9V6.5a4.5 4.5 0 0 1 9 0V9'%3e%3c/path%3e %3c/g%3e %3c/svg%3e");
          }
          &.lock-open {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='1.5' d='M14 10V7c0-2.21 1.79-4 4-4s4 1.79 4 4v3M4.6 10h10.8c.88 0 1.6.72 1.6 1.6v7c0 1.32-1.08 2.4-2.4 2.4H5.4C4.08 21 3 19.92 3 18.6v-7c0-.88.72-1.6 1.6-1.6'%3e%3c/path%3e%3c/svg%3e");
          }
          &.lock-close {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='1.5' d='M8 10V7c0-2.21 1.79-4 4-4s4 1.79 4 4v3m-9.4 0h10.8c.88 0 1.6.72 1.6 1.6v7c0 1.32-1.08 2.4-2.4 2.4H7.4C6.08 21 5 19.92 5 18.6v-7c0-.88.72-1.6 1.6-1.6'%3e%3c/path%3e%3c/svg%3e");
          }
        }
      }
    }
    &.alarm {
      flex: 29;
    }
    &.dynamic {
      flex: 29;
    }
    .scroll-wraper {
      flex: 1;
    }
    .scroll {
      // -webkit-overflow-scrolling: touch;
      overflow-y: auto;
      .alarm-item,
      .rt-item {
        color: #7f7f7f;
        padding: 4px 8px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-size: 12px;
      }
    }
  }
  .more {
    text-align: center;
    font-size: 12px;
    color: #999;
  }
}
</style>
src/views/home/viewer.vue
@@ -157,10 +157,15 @@
  canvas2d.value.strokeStyle = color;
  canvas2d.value.stroke();
}
const emit = defineEmits(["on-success"]);
const emit = defineEmits(["on-success", "on-close"]);
function getData(data) {
  emit("on-success", data);
  closeCamera();
}
function close() {
  emit("on-close");
  closeCamera();
}
@@ -183,17 +188,16 @@
<template>
  <div>
    <div class="canvasBox">
      <div class="close" @click="close">
        <el-icon><CircleClose /></el-icon>
      </div>
      <div class="box">
        <div class="line"></div>
        <div class="angle"></div>
      </div>
      <div v-if="isUseTorch" class="box2">
        <div class="track" @click="openTrack">
          <div class="flash-light" v-if="trackStatus">
            00
          </div>
          <div class="flash-light" v-else>
            11
          <div :class="['flash-light', {open : trackStatus}]">
          </div>
          {{ trackStatus ? "关闭闪光灯" : "打开闪光灯" }}
        </div>
@@ -245,7 +249,7 @@
  bottom: 0;
  left: 0;
  right: 0;
  background-image: linear-gradient(
  /* background-image: linear-gradient(
      0deg,
      transparent 24%,
      rgba(32, 255, 77, 0.1) 25%,
@@ -270,11 +274,18 @@
      transparent
    );
  background-size: 3rem 3rem;
  background-position: -1rem -1rem;
  background-position: -1rem -1rem; */
  z-index: 10;
  background-color: #1110;
}
.flash-light {
  width: 4em;
  height: 4em;
  background: url("data:image/svg+xml,%3csvg viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' fill='%23cdcdcd'%3e%3cpath d='M675.7 266.3H348.4c-46.1 0-83.6 37.5-83.6 83.6v96.4c0 70.9 33.3 137.2 89.6 179.7v275.2c0 46.1 37.5 83.7 83.7 83.7h148c46.1 0 83.7-37.5 83.7-83.7V626c56.3-42.5 89.6-108.8 89.6-179.7v-96.2c0.1-22.3-8.5-43.3-24.3-59.2-16-15.8-37-24.6-59.4-24.6z m-327.3 43.8h327.3c10.6 0 20.6 4.2 28.1 11.7 7.5 7.5 11.6 17.5 11.6 28.2v39.4H308.6V350c0-22 17.8-39.9 39.8-39.9z m287.1 286.5c-6 4.1-9.6 10.9-9.6 18.1v286.5c0 22-17.9 39.9-39.9 39.9H438c-22 0-39.9-17.9-39.9-39.9V614.8c0-7.3-3.6-14.1-9.6-18.1-49.5-33.4-79.1-88.7-79.8-148.2h406.6c-0.7 59.4-30.3 114.7-79.8 148.1z'%3e%3c/path%3e%3cpath d='M524 625.6h-23.8c-9.7 0-17.6 7.9-17.6 17.6v122.2c0 9.7 7.9 17.6 17.6 17.6H524c9.7 0 17.6-7.9 17.6-17.6V643.2c0-9.7-7.9-17.6-17.6-17.6z'%3e%3c/path%3e%3c/svg%3e") center center / contain no-repeat;
}
.flash-light.open {
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' fill='%23cde919'%3e%3cpath d='M675.7 266.3H348.4c-46.1 0-83.6 37.5-83.6 83.6v96.4c0 70.9 33.3 137.2 89.6 179.7v275.2c0 46.1 37.5 83.7 83.7 83.7h148c46.1 0 83.7-37.5 83.7-83.7V626c56.3-42.5 89.6-108.8 89.6-179.7v-96.2c0.1-22.3-8.5-43.3-24.3-59.2-16-15.8-37-24.6-59.4-24.6z m-327.3 43.8h327.3c10.6 0 20.6 4.2 28.1 11.7 7.5 7.5 11.6 17.5 11.6 28.2v39.4H308.6V350c0-22 17.8-39.9 39.8-39.9z m287.1 286.5c-6 4.1-9.6 10.9-9.6 18.1v286.5c0 22-17.9 39.9-39.9 39.9H438c-22 0-39.9-17.9-39.9-39.9V614.8c0-7.3-3.6-14.1-9.6-18.1-49.5-33.4-79.1-88.7-79.8-148.2h406.6c-0.7 59.4-30.3 114.7-79.8 148.1z'%3e%3c/path%3e%3cpath d='M524 625.6h-23.8c-9.7 0-17.6 7.9-17.6 17.6v122.2c0 9.7 7.9 17.6 17.6 17.6H524c9.7 0 17.6-7.9 17.6-17.6V643.2c0-9.7-7.9-17.6-17.6-17.6zM306.7 239.3c11.5 11.5 30.3 11.5 41.8 0 11.5-11.5 11.5-30.3 0-41.8l-37.4-37.4c-11.5-11.5-30.3-11.5-41.8 0-11.5 11.5-11.5 30.3 0 41.8l37.4 37.4zM675.5 239.3c11.5 11.5 30.3 11.5 41.8 0l37.4-37.4c11.5-11.5 11.5-30.3 0-41.8-11.5-11.5-30.3-11.5-41.8 0l-37.4 37.4c-11.4 11.5-11.4 30.3 0 41.8zM512.1 164c16.2 0 29.5-13.3 29.5-29.5V81.6c0-16.2-13.3-29.5-29.5-29.5s-29.5 13.3-29.5 29.5v52.8c0 16.3 13.2 29.6 29.5 29.6z'%3e%3c/path%3e%3c/svg%3e");
}
.box {
  width: 11.9375rem;
  height: 11.9375rem;
@@ -285,6 +296,14 @@
  overflow: hidden;
  border: 0.1rem solid rgba(0, 255, 51, 0.2);
  z-index: 11;
}
.close {
  position: absolute;
  top: 2rem;
  right: 1rem;
  font-size: 2.25rem;
  color: #fff;
  z-index: 32;
}
.line {
@@ -357,7 +376,7 @@
.track {
  position: absolute;
  bottom: -6.25rem;
  bottom: -8.25rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
src/views/login/apis.js
@@ -15,4 +15,14 @@
      usnId: encodeURIComponent(formatPassword(usnId)),
    },
  });
}
/**
 * 检查Ip的正确性
 */
export function checkServerIp() {
  return axios({
    url: 'heart/test',
    method: 'GET'
  });
}
src/views/login/index.vue
@@ -1,99 +1,166 @@
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import useElementVant from "@/hooks/useElementVant";
import { useUserStore } from "@/stores/user.js";
import { useRouter, useRoute } from "vue-router";
    import { ref, reactive, watch, onMounted } from "vue";
    import useElementVant from "@/hooks/useElementVant";
    import { useUserStore } from "@/stores/user.js";
    import { useRouter, useRoute } from "vue-router";
    import { storeToRefs } from 'pinia'
import { Dialog } from 'vant';
    import { Dialog } from 'vant';
const { $confirm, $message, $loading, $toast, Toast } = useElementVant();
const { setName, setId, addRemember, removeRemember } = useUserStore();
    const { $confirm, $message, $loading, $toast, Toast } = useElementVant();
    const { setName, setId, setRole, setIp } = useUserStore();
    const { serverIp } = storeToRefs(useUserStore());
import { login } from "./apis";
    import { login, checkServerIp } from "./apis";
const router = useRouter();
const route = useRoute();
    const router = useRouter();
    const route = useRoute();
const userName = ref("");
const password = ref("");
const platformName = ref("鸿蒙智能电子锁系统42334");
const redirect = ref();
    const userName = ref("");
    const password = ref("");
    const platformName = ref("鸿蒙智能电子锁系统");
    const redirect = ref();
    const ipEditVisible = ref(false);
    const ip = ref('');
    const ipState = ref('检测中...');
    let oldIp = '';
    onMounted(() => {
        let res = checkIp(serverIp.value);
        if (res) {
            ipState.value = '可用';
        } else {
            ipState.value = '不可用';
        }
    });
    watch(
        () => route,
        (route) => {
            redirect.value = route.query && route.query.redirect;
        },
        {
            immediate: true,
        }
    );
onMounted(() => {});
watch(
  () => route,
  (route) => {
    redirect.value = route.query && route.query.redirect;
  },
  {
    immediate: true,
  }
);
    // 登录
    function submit() {
        if (userName.value == "") {
            $toast("请输入账号!");
            return;
        }
        if (password.value == "") {
            $toast("请输入密码!");
            return;
        }
        // 开启等待框
        Toast.loading({
            message: "登录中...",
            duration: 0,
        });
        login(userName.value, password.value)
            .then((res) => {
                // console.log(res);
                let { code, data, data2, msg } = res.data;
                // // 对结果进行处理
                if (code && data) {
                    $toast("登录成功!");
                    handleLogin(data2);
                } else {
                    $toast(msg);
                }
            })
            .catch((error) => {
                console.log(error);
                // 关闭等待
                // console.log(error);
                $toast("网络异常" + error);
            });
    }
    // 登录验证
    function handleLogin(res) {
        setId(res.uid);
        setName(res.uname);
        setRole(res.urole);
        let url = res.urole != 0 ? '/home' : '/monitor';
        router.push({ path: redirect.value || url });
    }
// 登录
function submit() {
  if (userName.value == "") {
    $toast("请输入账号!");
    return;
  }
  if (password.value == "") {
    $toast("请输入密码!");
    return;
  }
  // 开启等待框
  Toast.loading({
    message: "登录中...",
    duration: 0,
  });
  login(userName.value, password.value)
    .then((res) => {
      // console.log(res);
      let {code,data,data2, msg} = res.data;
      Dialog({ message: JSON.stringify(res.data) });
      // // 对结果进行处理
      if (code && data) {
        $toast("登录成功!");
        handleLogin(data2);
      } else {
        $toast(msg);
      }
    })
    .catch((error) => {
    console.log(error);
    // 关闭等待
    // console.log(error);
    $toast("网络异常" + error);
  });
}
// 登录验证
function handleLogin(res) {
  setId(res.uid);
  setName(res.uname);
   router.push({ path: redirect.value || "/home" });
}
function initPageConfig() {
  // getRealTabsConfig()
  //   .then((res) => {
  //     let rs = res?.data?.data || [];
  //     let arr = [];
  //     for (let key in rs) {
  //       arr.push(...rs[key]);
  //     }
  //     // 设置pageConfig
  //     this.$store.dispatch("user/changeRealTabsConfig", arr);
  //     // 设置用户的权限
  //     this.$store.dispatch("user/getPermits");
  //     this.$toast("登录成功!");
  //     this.$router.push({
  //       path: "/home",
  //     });
  //   })
  //   .catch((error) => {
  //     // 设置pageConfig
  //     this.$store.dispatch("user/changeRealTabsConfig", []);
  //   });
}
    function changeIp() {
        console.log('serverIp', serverIp.value, '=============');
        ip.value = serverIp.value;
        oldIp = serverIp.value;
        ipEditVisible.value = true;
    }
    async function checkIp(Ip) {
        let loading = $loading();
        try {
            let res = await checkServerIp(Ip);
            console.log('res', res, res.status, '=============');
            loading.close();
            if (res.status == 200) {
                return true;
            } else {
                return false;
            }
        } catch (error) {
            console.log('error', error, '=============');
            loading.close();
            return false;
        }
    }
    function editCancel() {
        ipEditVisible.value = false;
    }
    async function editConfirm() {
        let _ip = ip.value.trim();
        // 校验ip
        let reg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
        if (!reg.test(_ip)) {
            return $toast("请输入正确的IP地址!");
        }
        setIp(_ip);
        console.log('_ip', _ip, serverIp.value, '=============');
        ipState.value = '检测中...';
        let res = await checkIp(_ip);
        if (res) {
            setIp(_ip);
            ipState.value = '可用';
            ipEditVisible.value = false;
            $toast('设置成功');
        } else {
            ipState.value = '不可用';
            $toast('ip不通, 设置失败');
            setIp(oldIp);
        }
    }
    function initPageConfig() {
        // getRealTabsConfig()
        //   .then((res) => {
        //     let rs = res?.data?.data || [];
        //     let arr = [];
        //     for (let key in rs) {
        //       arr.push(...rs[key]);
        //     }
        //     // 设置pageConfig
        //     this.$store.dispatch("user/changeRealTabsConfig", arr);
        //     // 设置用户的权限
        //     this.$store.dispatch("user/getPermits");
        //     this.$toast("登录成功!");
        //     this.$router.push({
        //       path: "/home",
        //     });
        //   })
        //   .catch((error) => {
        //     // 设置pageConfig
        //     this.$store.dispatch("user/changeRealTabsConfig", []);
        //   });
    }
</script>
<template>
  <div class="loginDiv">
@@ -106,14 +173,26 @@
      </div>
      <div class="lineInput">
        <img src="../../assets/img/login-ico2.png" class="ico2" />
        <van-field
          v-model="password"
          placeholder="请输入密码"
          type="password"
        />
        <van-field v-model="password" placeholder="请输入密码" type="password" />
      </div>
      <div class="subBtn" @click="submit">登录</div>
      <div class="info">
        <div class="label">入口Ip</div>
        <div class="value">{{ serverIp }} <span>({{ ipState }})</span></div>
        <div class="btn" @click="changeIp">切换</div>
      </div>
    </div>
    <el-dialog v-model="ipEditVisible" title="切换入口Ip" width="80%" align="center" center>
      <van-field v-model="ip" placeholder="请输入服务器IP" />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="editCancel">取消</el-button>
          <el-button type="primary" @click="editConfirm">
            确认
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -200,4 +279,30 @@
  font-size: 22px;
  margin-top: 4vh;
}
.info {
  margin-top: 1.8em;
  display: flex;
  justify-content: space-around;
  align-items: center;
  .label {
    color: #999;
    font-size: 14px;
    &::after {
      content: ":";
    }
  }
  .value {
    color: #08aeec;
    flex: 1;
    padding-left: 0.4em;
  }
  .btn {
    color: #08aeec;
  }
}
</style>
src/views/monitor/apis.js
New file
@@ -0,0 +1,46 @@
import axios from '@/assets/js/axios';
import uname from '@/assets/js/uname';
/**
 * 区域管理员登录查看自己管理的区域 树
 */
export function getAinfByManage() {
  return axios({
    method: "GET",
    // url: "app/getAinfByManage",
    url: 'areaInf/getAllAreaInf',
    params: {
      uname: uname.value
    }
  });
}
/**
 * 区域管理员点击指定区域查看所有的锁信息
 */
export function getlinfByAid(id) {
  return axios({
    method: "GET",
    url: "app/getlinfByAid",
    params: {
      id,
      uname: uname.value
    },
  });
}
/**
 * 查询区域管理员查看指定区域的锁的状态等信息
 * id 从所管理区域的树中获取
 */
export function getAreaLockById(id) {
  return axios({
    url: 'app/getAreaLockById',
    method: 'GET',
    params: {
      id,
      uname: uname.value
    }
  });
}
src/views/monitor/index.vue
New file
@@ -0,0 +1,426 @@
<script setup name="monitor">
    import { ref, onMounted, computed, watchEffect } from "vue";
    import IconLock from "@/components/icons/iconLock.vue";
    import { useUserStore } from "@/stores/user.js";
    import { getAuthByUid } from '@/views/user/apis';
    import { getAinfByManage, getAreaLockById } from './apis';
    import { formatAreaTree } from '@/assets/js/tree.js';
    import useWebSocket from '@/hooks/useWebSocket';
    const { urole, uname } = useUserStore();
  let wsUrl = '';
  if (urole == 0) {
    wsUrl = 'authUname'
  } else {
    wsUrl = 'areaLockState';
  }
    const { sendData, message } = useWebSocket(wsUrl);
    const lockList = ref([]);
    const areaVisible = ref(false);
    const tableData = ref([]);
    const tableRef = ref();
    const currentArea = ref('');
    const currentId = ref('');
    const sum = ref(0);
    const num_open = ref(0);
    const num_close = ref(0);
    const num_online = ref(0);
    const num_offline = ref(0);
    const num_unLoad = ref(0);
    // const sum = computed(() => {
    //     return lockList.value.length;
    // });
    // const num_open = computed(() => {
    //     return lockList.value.filter(v => v.state == 1).length;
    // });
    // const num_close = computed(() => {
    //     return lockList.value.filter(v => v.state == 0).length;
    // });
    function getLockList(res) {
            let { code, data, data2 } = res;
            let list = [];
      let _sum = 0,
                _num_open = 0,
                _num_close = 0,
                _num_online = 0,
                _num_unLoad = 0,
                _num_offline = 0;
            if (code && data) {
                console.log('data2', data2, '=============');
        for ( let i = 0; i < data2.length; i++ ) {
          let item = data2[i];
          list.push({
            ...item,
            stateStr: ['闭锁', '开锁'][item.lockState] || '--',
          });
          _num_close += item.lockState == 0 ? 1 : 0;
          _num_open += item.lockState == 1 ? 1 : 0;
          _num_unLoad += item.lockState == -1 ? 1 : 0;
          _num_online += item.lockOnline == 1 ? 1 : 0;
          _num_offline += item.lockOnline == 0 ? 1 : 0;
        }
          _sum = data2.length;
            }
      sum.value = _sum;
            num_open.value = _num_open;
            num_close.value = _num_close;
            num_online.value = _num_online;
            num_offline.value = _num_offline;
            num_unLoad.value = _num_unLoad;
            lockList.value = list;
    }
    function getAreas() {
        getAinfByManage().then((res) => {
            let { code, data, data2 } = res.data;
            let _data = [];
            if (code && data) {
                _data = data2;
            }
            console.log('_data', _data, '=============');
            const treeList = [];
            let ids = _data.map((v) => v.id);
            for (let i = 0; i < _data.length; i++) {
                formatAreaTree(_data[i], ids, treeList);
            }
            // console.log(_data, 'data');
            console.log(treeList, "treeList");
            tableData.value = treeList;
            if (treeList.length > 0) {
                select({ row: treeList[0] });
            }
        })
            .catch((err) => {
                console.log(err);
            });
    }
    function select({ row }) {
        areaVisible.value = false;
        console.log(row, row.data);
        currentArea.value = row.data.areaName;
        currentId.value = row.data.id;
    sendData(currentId.value);
    }
  watchEffect(() => {
    if (message.value) {
      if (urole == 0) {
        getLockList(JSON.parse(message.value));
      } else {
        getLocks(JSON.parse(message.value));
      }
    }
  });
    function getLocks(res) {
            let { code, data, data2 } = res;
            let _sum = 0,
                _num_open = 0,
                _num_close = 0,
                _num_online = 0,
                _num_unLoad = 0,
                _num_offline = 0;
            let list = [];
            if (code && data) {
                // console.log('data2', data2, '=============');
                list = data2.allLinfs.map(v => ({
                    ...v,
                    stateStr: ['闭锁', '开锁'][v.lockState] || '--',
                }));
                _sum = data2.sumLinf;
                _num_open = data2.openNum;
                _num_close = data2.closeNum;
                _num_online = data2.onlineNum;
                _num_offline = data2.offLineNum;
                _num_unLoad = data2.unLoadNum;
            }
            lockList.value = list;
            sum.value = _sum;
            num_open.value = _num_open;
            num_close.value = _num_close;
            num_online.value = _num_online;
            num_offline.value = _num_offline;
            num_unLoad.value = _num_unLoad;
    }
    // 递归函数,用于展开或折叠所有行及其子行
    const toggleRowsExpansion = (rows, expand, ref) => {
        rows.forEach((row) => {
            ref.toggleRowExpansion(row, expand);
            if (row.children && row.children.length) {
                // 如果当前行有子行,则递归调用
                toggleRowsExpansion(row.children, expand, ref);
            }
        });
    };
    const expandAll = (expand) => {
        toggleRowsExpansion(tableData.value, expand, tableRef.value);
    };
    onMounted(() => {
        // 如果是管理员,获取授权用户列表  如果是普通用户 只能查出锁具列表
        if (urole == 0) {
            // getLockList();
      sendData(uname);
        } else {
            getAreas();
        }
    })
</script>
<template>
  <div class="p-monitor">
    <div class="title">
      实时监控
    </div>
    <!-- 区域 -->
    <div class="area-name" v-if="urole > 0" @click="areaVisible = true">{{ currentArea }}</div>
    <!-- info -->
    <div class="info">
      <!-- <div class="item"> -->
      <div class="label">锁具</div>
      <div class="value">{{ sum }}</div>
      <!-- </div> -->
      <!-- <div class="item"> -->
      <div class="label">在线</div>
      <div class="value">{{ num_online }}</div>
      <!-- </div> -->
      <!-- <div class="item"> -->
      <div class="label">离线</div>
      <div class="value">{{ num_offline }}</div>
      <div class="label">未安装</div>
      <div class="value">{{ num_unLoad }}</div>
      <!-- </div> -->
      <!-- <div class="item"> -->
      <div class="label">开启</div>
      <div class="value">{{ num_open }}</div>
      <!-- </div> -->
      <!-- <div class="item"> -->
      <div class="label">关闭</div>
      <div class="value">{{ num_close }}</div>
      <!-- </div> -->
    </div>
    <!-- 列表 -->
    <div class="list">
      <div class="scroller" v-if="lockList.length">
        <div class="lock-item" v-for="(lock, idx) in lockList" :key="'lock_' + idx">
          <div class="station">{{ lock.areaPath }}</div>
          <div :class="['icon', { online: lock.lockOnline }]">
            <el-icon><icon-lock class="icon-lock" /></el-icon>
          </div>
          <div :class="['state1', { online: lock.lockOnline }]">{{ lock.lockOnline ? '在线' : '离线' }}</div>
          <div class="lock-name">{{lock.lockName}}</div>
          <div class="lock-id">{{ lock.lockId }}</div>
          <div class="state">{{ lock.stateStr }}</div>
        </div>
      </div>
      <van-empty v-else image-size="10rem" description="没有管理任何锁具" />
    </div>
    <van-action-sheet v-model:show="areaVisible" title="选择区域">
      <template #description>
        <van-button type="primary" size="small" @click="expandAll(true)">全部展开</van-button>
        <van-button type="success" size="small" @click="expandAll(false)">全部折叠</van-button>
      </template>
      <div class="content">
        <el-table stripe ref="tableRef" :data="tableData" :show-header="false"
          :tree-props="{ children: 'children', checkStrictly: true }" row-key="id" default-expand-all>
          <!-- <el-table-column type="selection" width="55" /> -->
          <el-table-column prop="label" label="区域名称" />
          <el-table-column align="center" fixed="right" label="操作" width="80">
            <template #default="scope">
              <el-button type="primary" size="small" @click="select(scope)">选择</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </van-action-sheet>
  </div>
</template>
<style scoped lang="less">
.p-monitor {
  height: 100%;
  // background: #f2f2f2 linear-gradient(#81d3f8, #81d3f8) top center / 100% 20% no-repeat;
  // padding: 10px;
  display: flex;
  flex-direction: column;
  .title {
    height: 50px;
    line-height: 50px;
    text-align: center;
    background: #81d3f8;
    color: #027db4;
    font-size: 16px;
    font-weight: bold;
  }
  .area-name {
    background: #81d3f8;
    text-align: center;
    font-weight: bold;
    font-size: 18px;
  }
  .info {
    // display: flex;
    // justify-content: space-around;
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 4px;
    place-items: center;
    padding: 10px;
    background: #81d3f8;
    border-radius: 0 0 8px 8px;
    // .item {
    //   display: flex;
    //   align-items: center;
    //   font-size: 16px;
    // }
    .label {
      color: #090;
      justify-self: end;
      &::after {
        content: ':';
      }
    }
    .value {
      justify-self: start;
      // margin-left: 0.6em;
      color: #027db4;
      font-weight: bold;
    }
  }
  .list {
    flex: 1;
    position: relative;
    .scroller {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      overflow-y: auto;
      .lock-item {
        // border-radius: 6px;
        // height: 50px;
        // background: #fff;
        // display: flex;
        // align-items: center;
        // justify-content: space-between;
        // padding: 0 8px;
        background: #fff;
        padding: 10px 0;
        margin-bottom: 10px;
        display: grid;
        gap: 4px;
        grid-template-columns: 1fr 1fr 5fr 4fr;
        grid-auto-rows: 24px 1.3fr 1fr;
        &::before {
          content: '';
        }
        &~.lock-item {
          margin-top: 6px;
        }
        .station {
          // align-self: center;
          grid-column: 1 e('/') 4;
          grid-row: 1 e('/') 2;
          font-size: 12px;
          display: flex;
          align-items: center;
          background: #81d3f8;
          border-radius: 0 20px 20px 0;
          color: #333;
          padding-left: 0.4em;
        }
        .icon {
          grid-column: 1 e('/') 2;
          grid-row: 2 e('/') 4;
          font-size: 30px;
          color: #aaa;
          &.online {
            color: #0a0;
          }
        }
        .state1 {
          grid-area: 2 e('/') 2 e('/') 3 e('/') 3;
          font-size: 12px;
          color: #aaa;
          &.online {
            color: #0a0;
          }
          // color: #000;
          // font-weight: bold;
        }
        .lock-name {
          grid-area: 2 e('/') 3 e('/') 3 e('/') 4;
          font-size: 16px;
          color: #000;
          font-weight: bold;
        }
        .lock-id {
          grid-area: 3 e('/') 3 e('/') 4 e('/') 4;
          font-size: 12px;
          color: #999;
        }
        .state {
          font-weight: bold;
          color: #0f0;
        }
      }
    }
  }
  :deep(.van-action-sheet__description) {
    padding: 0;
    button+button {
      margin-left: 10px;
    }
  }
}
</style>
src/views/user/apis.js
New file
@@ -0,0 +1,44 @@
import axios from '@/assets/js/axios';
import uname from '@/assets/js/uname';
/**
 * 登出
 * @param params
 * @returns
 */
export function logout() {
  return axios({
    url: 'login/logout',
    method: 'GET'
  });
}
/**
 * 普通用户登录查看自己授权记录 锁的个数
 * @param params
 * @returns
 */
export function getAuthByUid() {
  return axios({
    url: 'app/getAuthByUid',
    method: 'GET',
    params: {
      uname: uname.value
    }
  });
}
/**
 * 区域管理员登录查看自己管理记录
 * @param params
 * @returns
 */
export function getAuthByUid2() {
  return axios({
    url: 'app/getInfByAreaManage',
    method: 'GET',
    params: {
      uname: uname.value
    }
  });
}
src/views/user/index.vue
New file
@@ -0,0 +1,223 @@
<script setup>
    import { ref, onMounted } from "vue";
    import { logout, getAuthByUid, getAuthByUid2 } from './apis';
    import { useUserStore } from "@/stores/user.js";
    import { useRouter } from "vue-router";
    const router = useRouter();
    const { urole } = useUserStore();
    const lockNum = ref(0);
    const areaNum = ref(0);
    const userNum = ref(0);
    function logoutUser() {
        localStorage.removeItem("uname");
        localStorage.removeItem("uid");
        localStorage.removeItem("urole");
        location.reload();
        logout();
    }
    function getLockList() {
        getAuthByUid().then((res) => {
            let { code, data, data2 } = res.data;
            let len = 0;
            if (code && data) {
                console.log(data2.length);
                len = data2.length;
            }
            lockNum.value = len;
        })
            .catch((err) => {
                console.log(err);
            });
    }
    function getLockListAdmin() {
        getAuthByUid2().then((res) => {
            let { code, data, data2 } = res.data;
            let _userNum = 0,
                _lockNum = 0,
                _areaNum = 0;
            if (code && data) {
                _userNum = data2.userNum;
                _lockNum = data2.lockNum;
                _areaNum = data2.areaNum;
            }
            lockNum.value = _lockNum;
            userNum.value = _userNum;
            areaNum.value = _areaNum;
        })
            .catch((err) => {
                console.log(err);
            });
    }
    function toMonitor() {
        router.push({ path: "/monitor" });
    }
    onMounted(() => {
        // 如果是管理员,获取授权用户列表  如果是普通用户 只能查出锁具列表
        if (urole == 0) {
            getLockList();
        } else {
            getLockListAdmin();
        }
    })
</script>
<template>
  <div class="p-user">
    <div class="title">我的</div>
    <div class="header">
      <div class="avatar">
        <van-icon name="contact" />
      </div>
      <div class="role">{{urole == 0 ? '普通用户' : '区域管理员'}}</div>
      <div class="info">
        <div class="item" v-if="urole > 0">
          <div class="ico house" @click="toMonitor"></div>
          <div class="label">管理区域</div>
          <div class="value">{{ areaNum }}</div>
        </div>
        <div class="item" @click="toMonitor">
          <div class="ico lock"></div>
          <div class="label">管理锁具</div>
          <div class="value">{{ lockNum }}</div>
        </div>
        <div class="item" v-if="urole > 0">
          <div class="ico user"></div>
          <div class="label">用户</div>
          <div class="value">{{ userNum }}</div>
        </div>
      </div>
    </div>
    <!-- <div class="main">
        <div class="title-level1">系统设置</div>
        <van-cell title="账号安全" is-link to="home" />
        <van-cell title="人脸认证" is-link to="home" />
        <van-cell title="系统设置" is-link to="home" />
        <van-cell title="帮助中心" is-link to="home" />
        <van-cell title="检查版本" is-link to="home" />
      </div> -->
    <div class="footer">
      <van-button type="primary" block @click="logoutUser">退出登录</van-button>
    </div>
  </div>
</template>
<style scoped lang="less">
.p-user {
  height: 100%;
  background: #f2f2f2 linear-gradient(#81d3f8, #81d3f8) top center / 100% 20% no-repeat;
  padding: 10px;
  display: flex;
  flex-direction: column;
  .title {
    height: 50px;
    line-height: 50px;
    text-align: center;
    color: #027db4;
    font-size: 16px;
    font-weight: bold;
  }
  .header {
    background-color: #fff;
    padding-bottom: 10px;
    border-radius: 6px;
    margin-top: 2em;
    .avatar {
      background: #81d3f8;
      border: 3px #fff solid;
      width: 4em;
      height: 4em;
      border-radius: 50%;
      margin: 0 auto -1.8em;
      transform: translate(0, -50%);
      display: flex;
      justify-content: center;
      align-items: center;
      .van-icon {
        font-size: 2em;
        color: #fff;
      }
    }
    .role {
      text-align: center;
      font-size: 14px;
      color: #999;
    }
    .info {
      margin-top: 10px;
      display: flex;
      justify-content: space-around;
      .item {
        display: flex;
        flex-direction: column;
        align-items: center;
        .ico {
          width: 40px;
          height: 40px;
          // background: #81d3f8;
          // border-radius: 50%;
          &.house {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%2381d3f8' d='M10.707 2.293a1 1 0 0 0-1.414 0l-7 7a1 1 0 0 0 1.414 1.414L4 10.414V17a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6.586l.293.293a1 1 0 0 0 1.414-1.414z'%3e%3c/path%3e%3c/svg%3e");
          }
          &.lock {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3cpath fill='%2381d3f8' d='M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9zm4.1 8.5l-.1.1V17c0 .6-.4 1-1 1s-1-.4-1-1v-1.4c-.6-.6-.7-1.5-.1-2.1s1.5-.7 2.1-.1c.6.5.7 1.5.1 2.1'%3e%3c/path%3e%3c/svg%3e");
          }
          &.user {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%2381d3f8' d='M9 6a3 3 0 1 1-6 0a3 3 0 0 1 6 0m8 0a3 3 0 1 1-6 0a3 3 0 0 1 6 0m-4.07 11q.07-.49.07-1a6.97 6.97 0 0 0-1.5-4.33A5 5 0 0 1 19 16v1zM6 11a5 5 0 0 1 5 5v1H1v-1a5 5 0 0 1 5-5'%3e%3c/path%3e%3c/svg%3e");
          }
        }
        .label {
          margin-top: 5px;
          font-size: 12px;
          color: #999;
        }
        .value {
          margin-top: 5px;
          font-weight: bold;
          font-size: 16px;
          color: #81d3f8;
        }
      }
    }
  }
  .main {
    margin-top: 10px;
    background-color: #fff;
    padding: 10px;
    border-radius: 6px;
  }
  .footer {
    margin-top: 10px;
    border-radius: 6px;
    background-color: #fff;
    padding: 10px;
  }
}
</style>