From c71e806d1966a83aad55880c8a456358182a82be Mon Sep 17 00:00:00 2001
From: lefengyang <lefengyang@tencent.com>
Date: Sun, 09 Aug 2026 01:36:45 +0800
Subject: [PATCH] uniapp
---
ledApp/static/logo.png | 0
ledApp/uni.promisify.adaptor.js | 13
ledApp/pages/addDevice/addDevice0.vue | 487 ++
ledApp/main.js | 30
ledApp/static/img/btn_1.png | 0
ledApp/pages/invite/invite.vue | 96
ledApp/pages/policy/policy.vue | 63
ledApp/pages/personal/personal.vue | 300 +
ledApp/static/img/ico_3.png | 0
ledApp/static/img/logo.png | 0
ledApp/static/img/ico_head.png | 0
ledApp/uni.scss | 76
ledApp/static/img/ico_m1.png | 0
ledApp/static/img/ico_1.png | 0
ledApp/pages/addTemplate/addTemplate copy 4.vue | 1056 +++++
ledApp/static/img/ico_m3a.png | 0
ledApp/pages/addTemplate/addTemplate copy.vue | 348 +
ledApp/static/img/ico_2.png | 0
ledApp/pages/addTemplate/addTemplate copy 3.vue | 1056 +++++
ledApp/pages/template/template.vue | 183
ledApp/api/api.js | 60
ledApp/static/img/ico_m3.png | 0
ledApp/pages/animation/animation copy.vue | 533 ++
ledApp/pages/addTemplate/addTemplate copy 2.vue | 548 ++
ledApp/pages/animation/animation.vue | 624 +++
ledApp/pages/my/my.vue | 176
ledApp/static/img/ico_m1a.png | 0
ledApp/pages.json | 122
ledApp/pages/addTemplate/addTemplate copy 1.vue | 548 ++
ledApp/static/img/bg_1.png | 0
ledApp/pages/index/index.vue | 1353 +++++++
ledApp/static/img/ico_m2.png | 0
ledApp/App.vue | 39
ledApp/manifest.json | 146
ledApp/pages/set/set.vue | 129
ledApp/static/img/bg_2.jpg | 0
ledApp/pages/login/login.vue | 248 +
ledApp/pages/feedback/feedback.vue | 111
ledApp/static/img/bg_2.png | 0
ledApp/static/img/vip_head.jpg | 0
ledApp/pages/addTemplate/addTemplate.vue | 1062 +++++
ledApp/util/request.js | 70
ledApp/pages/about/about.vue | 59
ledApp/pages/register/register.vue | 264 +
ledApp/static/img/ph_1.png | 0
ledApp/index.html | 20
ledApp/static/img/bg_4.png | 0
ledApp/static/img/bg_3.png | 0
ledApp/static/img/ico_m2a.png | 0
ledApp/pages/addDevice/addDevice.vue | 1622 ++++++++
50 files changed, 11,442 insertions(+), 0 deletions(-)
diff --git a/ledApp/App.vue b/ledApp/App.vue
new file mode 100644
index 0000000..8ec0899
--- /dev/null
+++ b/ledApp/App.vue
@@ -0,0 +1,39 @@
+<script>
+ export default {
+ onLaunch: function() {
+ console.log('App Launch')
+
+ if(uni.getStorageSync('token')){
+ console.log('已登陆------------')
+ // uni.switchTab({
+ // url: '/pages/index/index'
+ // });
+ }
+ else{
+ console.log('未登陆------------')
+ // uni.reLaunch({
+ // url: '/pages/login/login'
+ // });
+ }
+ uni.onBLECharacteristicValueChange(res => {
+ uni.$emit('BLECharacteristic', res)
+ })
+
+
+ },
+ onShow: function() {
+ console.log('App Show')
+ },
+ onHide: function() {
+ console.log('App Hide')
+ }
+ }
+</script>
+
+<style>
+ /*每个页面公共css */
+ page {
+ font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+ }
+</style>
diff --git a/ledApp/api/api.js b/ledApp/api/api.js
new file mode 100644
index 0000000..8a32d56
--- /dev/null
+++ b/ledApp/api/api.js
@@ -0,0 +1,60 @@
+// post请求,获取菜单
+export const postMenu = (params, config = {}) => uni.$uv.http.post('/ebapi/public_api/index', params, config)
+
+// get请求,获取菜单,注意:get请求的配置等,都在第二个参数中,详见前面解释
+export const getMenu = (data) => uni.$uv.http.get('/ebapi/public_api/index', data)
+
+// 登录
+export const login = (data) => uni.$uv.http.post("/login", data);
+
+// 退出登录
+export const logout = (data) => uni.$uv.http.post("/logout", data);
+
+// 注册
+export const register = (data) => uni.$uv.http.post("/mobile/register", data);
+
+// 注销账户
+export const unRegistry = () => uni.$uv.http.post("/unRegistry");
+
+// 获取用户信息
+export const userInfo = (data) => uni.$uv.http.get('/getInfo', data)
+
+// 修改编辑用户信息
+export const userProfile = (data) => uni.$uv.http.put("/system/user/profile", data);
+
+// 新建设备
+export const equipmentAdd = (data) => uni.$uv.http.post("/system/equipment", data);
+
+// 设备列表
+export const equipmentList = (data) => uni.$uv.http.get('/system/equipment/list', {params:data})
+
+// 删除列表
+export const equipmentDel = (id) => uni.$uv.http.delete("/system/equipment/"+id);
+
+
+// 模板列表
+export const templeteList = (data) => uni.$uv.http.get('/system/templete/list', {params:data})
+
+// 新增模板
+export const templeteAdd = (data) => uni.$uv.http.post("/system/templete", data);
+
+// 删除模版
+export const templeteDel = (id) => uni.$uv.http.delete("/system/templete/"+id);
+
+// 隐私协议/关于我们 1/2
+export const content = (id) => uni.$uv.http.get('/system/content/'+id)
+
+// 意见反馈
+export const feekback = (data) => uni.$uv.http.post("/system/opinion", data);
+
+// 客服电话
+export const configPhone = () => uni.$uv.http.get('/system/config/configKey/biz.customer.phone')
+
+// app下载
+export const appDownload = () => uni.$uv.http.get('/system/config/configKey/biz.appDownload')
+
+// 模版详情
+export const templeteInfo = (id) => uni.$uv.http.get("/system/templete/"+id);
+
+// 修改模板
+export const templeteEdit = (data) => uni.$uv.http.put("/system/templete", data);
diff --git a/ledApp/index.html b/ledApp/index.html
new file mode 100644
index 0000000..c3ff205
--- /dev/null
+++ b/ledApp/index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <script>
+ var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+ CSS.supports('top: constant(a)'))
+ document.write(
+ '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+ (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+ </script>
+ <title></title>
+ <!--preload-links-->
+ <!--app-context-->
+ </head>
+ <body>
+ <div id="app"><!--app-html--></div>
+ <script type="module" src="/main.js"></script>
+ </body>
+</html>
diff --git a/ledApp/main.js b/ledApp/main.js
new file mode 100644
index 0000000..d7a00b0
--- /dev/null
+++ b/ledApp/main.js
@@ -0,0 +1,30 @@
+import App from './App'
+import uvUI from '@/uni_modules/uv-ui-tools'
+import { Request } from '@/util/request'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+Vue.use(uvUI);
+App.mpType = 'app'
+const app = new Vue({
+ ...App
+})
+app.$mount()
+Request(app)
+// #endif
+
+// #ifdef VUE3
+import {
+ createSSRApp
+} from 'vue'
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uvUI);
+ Request(app)
+ return {
+ app
+ }
+}
+// #endif
\ No newline at end of file
diff --git a/ledApp/manifest.json b/ledApp/manifest.json
new file mode 100644
index 0000000..7863e26
--- /dev/null
+++ b/ledApp/manifest.json
@@ -0,0 +1,146 @@
+{
+ "name" : "ledApp",
+ "appid" : "__UNI__8C00436",
+ "description" : "",
+ "versionName" : "1.0.6",
+ "versionCode" : 106,
+ "transformPx" : false,
+ /* 5+App特有相关 */
+ "app-plus" : {
+ "usingComponents" : true,
+ "nvueStyleCompiler" : "uni-app",
+ "compilerVersion" : 3,
+ "splashscreen" : {
+ "alwaysShowBeforeRender" : true,
+ "waiting" : true,
+ "autoclose" : true,
+ "delay" : 0
+ },
+ "screenOrientation" : [ "portrait-primary", "landscape-primary" ],
+ /* 模块配置 */
+ "modules" : {
+ "Barcode" : {},
+ "Geolocation" : {},
+ "Bluetooth" : {},
+ "Camera" : {}
+ },
+ /* 应用发布信息 */
+ "distribute" : {
+ /* android打包配置 */
+ "android" : {
+ "permissions" : [
+ "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+ "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+ "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+ "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+ "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+ "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+ "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+ "<uses-feature android:name=\"android.hardware.camera\"/>",
+ "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />",
+ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",
+ "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />",
+ "<uses-permission android:name=\"android.permission.BLUETOOTH\" />",
+ "<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />",
+ "<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />",
+ "<uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\" />"
+ ],
+ "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
+ "minSdkVersion" : 21,
+ "targetSdkVersion" : 31
+ },
+ /* ios打包配置 */
+ "ios" : {
+ "privacyDescription" : {
+ "NSBluetoothAlwaysUsageDescription" : "本应用需要始终使用蓝牙功能",
+ "NSBluetoothPeripheralUsageDescription" : "需要使用蓝牙连接外设设备",
+ "NSLocationWhenInUseUsageDescription" : "蓝牙连接外设设备需要定位服务",
+ "NSLocationAlwaysUsageDescription" : "",
+ "NSLocationAlwaysAndWhenInUseUsageDescription" : "蓝牙连接外设设备需要定位服务"
+ },
+ "dSYMs" : false,
+ "idfa" : false
+ },
+ /* SDK配置 */
+ "sdkConfigs" : {
+ "geolocation" : {
+ "system" : {
+ "__platform__" : [ "ios", "android" ]
+ }
+ }
+ },
+ "icons" : {
+ "android" : {
+ "hdpi" : "unpackage/res/icons/72x72.png",
+ "xhdpi" : "unpackage/res/icons/96x96.png",
+ "xxhdpi" : "unpackage/res/icons/144x144.png",
+ "xxxhdpi" : "unpackage/res/icons/192x192.png"
+ },
+ "ios" : {
+ "appstore" : "unpackage/res/icons/1024x1024.png",
+ "ipad" : {
+ "app" : "unpackage/res/icons/76x76.png",
+ "app@2x" : "unpackage/res/icons/152x152.png",
+ "notification" : "unpackage/res/icons/20x20.png",
+ "notification@2x" : "unpackage/res/icons/40x40.png",
+ "proapp@2x" : "unpackage/res/icons/167x167.png",
+ "settings" : "unpackage/res/icons/29x29.png",
+ "settings@2x" : "unpackage/res/icons/58x58.png",
+ "spotlight" : "unpackage/res/icons/40x40.png",
+ "spotlight@2x" : "unpackage/res/icons/80x80.png"
+ },
+ "iphone" : {
+ "app@2x" : "unpackage/res/icons/120x120.png",
+ "app@3x" : "unpackage/res/icons/180x180.png",
+ "notification@2x" : "unpackage/res/icons/40x40.png",
+ "notification@3x" : "unpackage/res/icons/60x60.png",
+ "settings@2x" : "unpackage/res/icons/58x58.png",
+ "settings@3x" : "unpackage/res/icons/87x87.png",
+ "spotlight@2x" : "unpackage/res/icons/80x80.png",
+ "spotlight@3x" : "unpackage/res/icons/120x120.png"
+ }
+ }
+ }
+ }
+ },
+ /* 快应用特有相关 */
+ "quickapp" : {},
+ /* 小程序特有相关 */
+ "mp-weixin" : {
+ "appid" : "wx835fdaefed9491a4",
+ "setting" : {
+ "urlCheck" : false,
+ "minified" : true,
+ "postcss" : true,
+ "es6" : true,
+ "bluetooth" : true,
+ "request" : [ "scope.userLocation", "scope.userInfo" ]
+ },
+ "usingComponents" : true,
+ "permission" : {
+ "scope.userLocation" : {
+ "desc" : "你的位置信息将用于蓝牙设备扫描"
+ }
+ }
+ },
+ "mp-alipay" : {
+ "usingComponents" : true
+ },
+ "mp-baidu" : {
+ "usingComponents" : true
+ },
+ "mp-toutiao" : {
+ "usingComponents" : true
+ },
+ "uniStatistics" : {
+ "enable" : false
+ },
+ "vueVersion" : "3"
+}
diff --git a/ledApp/pages.json b/ledApp/pages.json
new file mode 100644
index 0000000..1be60ba
--- /dev/null
+++ b/ledApp/pages.json
@@ -0,0 +1,122 @@
+{
+ "pages": [{
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "设备"
+ }
+ },
+ {
+ "path": "pages/login/login",
+ "style": {
+ "navigationBarTitleText": "登陆"
+ }
+ },
+ {
+ "path": "pages/my/my",
+ "style": {
+ "navigationBarTitleText": "我的"
+ }
+ },
+ {
+ "path": "pages/template/template",
+ "style": {
+ "navigationBarTitleText": "模版"
+ }
+ },
+ {
+ "path": "pages/addDevice/addDevice",
+ "style": {
+ "navigationBarTitleText": "添加设备"
+ }
+ },
+ {
+ "path": "pages/invite/invite",
+ "style": {
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/about/about",
+ "style": {
+ "navigationBarTitleText": "关于我们"
+ }
+ },
+ {
+ "path": "pages/feedback/feedback",
+ "style": {
+ "navigationBarTitleText": "反馈问题"
+ }
+ },
+ {
+ "path": "pages/set/set",
+ "style": {
+ "navigationBarTitleText": "设置"
+ }
+ },
+ {
+ "path": "pages/policy/policy",
+ "style": {
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/addTemplate/addTemplate",
+ "style": {
+ "navigationBarTitleText": "添加模版"
+ }
+ },
+ {
+ "path": "pages/animation/animation",
+ "style": {
+ "navigationBarTitleText": "编辑动画",
+ "orientation": "landscape"
+ }
+ },
+ {
+ "path": "pages/register/register",
+ "style": {
+ "navigationBarTitleText": ""
+ }
+ },
+
+ {
+ "path": "pages/personal/personal",
+ "style": {
+ "navigationBarTitleText": ""
+ }
+ }
+ ],
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "LEDAPP",
+ "navigationBarBackgroundColor": "#F8F8F8",
+ "backgroundColor": "#F8F8F8",
+ "navigationStyle": "custom"
+ },
+ "tabBar": {
+ "color": "#757575",
+ "selectedColor": "#3086F3",
+ "borderStyle": "black",
+ "backgroundColor": "#ffffff",
+ "list": [{
+ "pagePath": "pages/index/index",
+ "iconPath": "/static/img/ico_m1.png",
+ "selectedIconPath": "/static/img/ico_m1a.png",
+ "text": "设备"
+ },
+ {
+ "pagePath": "pages/template/template",
+ "iconPath": "/static/img/ico_m2.png",
+ "selectedIconPath": "/static/img/ico_m2a.png",
+ "text": "模版"
+ },
+ {
+ "pagePath": "pages/my/my",
+ "iconPath": "/static/img/ico_m3.png",
+ "selectedIconPath": "/static/img/ico_m3a.png",
+ "text": "我的"
+ }
+ ]
+ },
+ "uniIdRouter": {}
+}
\ No newline at end of file
diff --git a/ledApp/pages/about/about.vue b/ledApp/pages/about/about.vue
new file mode 100644
index 0000000..be6b6e5
--- /dev/null
+++ b/ledApp/pages/about/about.vue
@@ -0,0 +1,59 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="关于我们" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="about">
+ <rich-text :nodes="aboutData"></rich-text>
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import {
+ content
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ aboutData: ""
+ }
+ },
+ onLoad() {
+ this.getContent()
+ },
+ methods: {
+ getContent() {
+ content(2).then(res => {
+ console.log(res.data)
+ this.aboutData = res.data.content.replace(/(?<=src=")\/prod-api/g, uni.$uv.http.config.baseURL)
+ })
+ }
+ }
+ }
+</script>
+<style>
+ /deep/ rich-text img {
+ max-width: 100%;
+ height: auto;
+ }
+</style>
+<style lang="scss" scoped>
+ .about {
+ margin-top: 360rpx;
+ padding: 30rpx;
+ font-weight: 400;
+ font-size: 27rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 46rpx;
+ text-align: justified;
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_4.png') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addDevice/addDevice.vue b/ledApp/pages/addDevice/addDevice.vue
new file mode 100644
index 0000000..f23b618
--- /dev/null
+++ b/ledApp/pages/addDevice/addDevice.vue
@@ -0,0 +1,1622 @@
+<template>
+ <view>
+ <uv-navbar title="添加设备" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="#3B74EE"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="page_bg" v-if="searchIng">
+ <view class="searching_ico">
+ <image src="/static/img/ico_1.png" mode="widthFix"></image>
+ </view>
+
+ <view class="searching_text" v-if="isBluetooth && isLocation">
+ <view class="name">
+ 搜索中
+ </view>
+ <view class="text">
+ 持续自动搜索中...
+ </view>
+ </view>
+
+ <view class="searching_text" v-if="!isBluetooth">
+ <view class="name">
+ 蓝牙未开启
+ </view>
+ <view class="text">
+ 请手动开启系统蓝牙
+ </view>
+ </view>
+
+ <view class="searching_text" v-if="!isLocation">
+ <view class="name">
+ 定位服务未开启
+ </view>
+ <view class="text">
+ 请手动开启系统定位服务
+ </view>
+ </view>
+
+ </view>
+
+ <view class="bluetooth" v-if="!searchIng">
+ <view class="bluetooth_title">
+ <image class="ico" src="/static/img/ico_2.png" mode="widthFix"></image>
+ <text>已找到设备</text>
+ </view>
+ <view class="bluetooth_list">
+ <view class="item" v-for="(item,index) in bluetoothList" :key="index">
+ <image class="ph" src="/static/img/ph_1.png" mode="aspectFill"></image>
+ <view class="info">
+ <view class="name">
+ {{item.name}}
+ </view>
+ <view class="btn" v-if="!isLink">
+ <uv-button type="primary" shape="circle" text="连接" size="small"
+ @click="createBLEConnection(item)"></uv-button>
+ </view>
+ <!-- <view class="btn" v-if="!isLink">
+ <uv-button type="primary" shape="circle" text="连接" size="small"
+ @click="createBLEConnection(item.deviceId)"></uv-button>
+ </view> -->
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="LIST" size="small"
+ @click="commandLIST()"></uv-button>
+ </view> -->
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="播放LED" size="small"
+ @click="this.$refs.popupLED.open();"></uv-button>
+ </view> -->
+ <view class="btn" v-if="isLink">
+ <uv-button type="primary" shape="circle" text="发送LED" size="small"
+ @click="this.$refs.popupTemplete.open();"></uv-button>
+ </view>
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="获取画板数据" size="small"
+ @click="getDrawData()"></uv-button>
+ </view> -->
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="删除全部" size="small"
+ @click="commandDALL()"></uv-button>
+ </view> -->
+ <!-- <view class="btn" v-if="isLink">
+ <uv-button type="primary" shape="circle" text="调节亮度" size="small"
+ @click="this.$refs.popupBRIG.open();"></uv-button>
+ </view> -->
+
+ </view>
+ </view>
+ </view>
+ </view>
+
+
+ <!-- 设置密码弹出框 -->
+ <uv-popup ref="popupPwd" @change="changePopupPwd" mode="center" round="30rpx" :closeable="false"
+ :closeOnClickOverlay="false">
+ <view class="popup_pwd">
+ <view class="title">请输入设备绑定的密码</view>
+ <view class="cont">
+ <uv-input v-model="password" type="number" maxlength="4" placeholder="请输入4位数密码" />
+ </view>
+ <view class="btns">
+ <uv-button type="primary" :disabled="password.length !== 4" @click="confirmPwd">确定</uv-button>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 设置名称出框 -->
+ <uv-popup ref="popupName" @change="changePopupName" mode="center" round="30rpx" :closeable="false"
+ :closeOnClickOverlay="false">
+ <view class="popup_pwd">
+ <view class="title">请输入设备名称</view>
+ <view class="cont">
+ <uv-input v-model="deviceName" placeholder="请输入设备名称" />
+ </view>
+ <view class="btns">
+ <uv-button type="primary" :disabled="deviceName == ''" @click="confirmName">确定</uv-button>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 播放设备中文件列表 -->
+ <uv-popup ref="popupLED" mode="center" round="30rpx">
+ <view class="popup_pwd">
+ <view class="title">发送LED</view>
+ <view class="cont cont_led">
+ <view class="led_item" v-for="(item,index) in fileNames" :key="index">
+ <view class="name">
+ {{item.fileName}}
+ </view>
+ <!-- <view class="name">
+ {{item.nameHex}}
+ </view> -->
+ <uv-button type="primary" @click="commandDELL(item.nameHex)">删除</uv-button>
+ <uv-button type="primary" @click="commandPLAY(item.nameHex)">播放</uv-button>
+ </view>
+ </view>
+ <view class="btns">
+ <uv-button type="primary" plain @click="this.$refs.popupLED.close();">关闭</uv-button>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 调节亮度 -->
+ <uv-popup ref="popupBRIG" mode="center" round="30rpx">
+ <view class="popup_pwd">
+ <view class="title">调节亮度</view>
+ <view class="cont">
+ <uv-slider v-model="brightness" :min="0" :max="9" :step="1" block-size="24" block-color="#2979ff"
+ @changing="changeBrightness"></uv-slider>
+ </view>
+ <view class="btns_two">
+ <view class="btn2">
+ <uv-button type="primary" plain @click="this.$refs.popupBRIG.close();">关闭</uv-button>
+ </view>
+ <view class="btn2">
+ <uv-button type="primary" @click="commandBRIG()">确定</uv-button>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 模版列表 -->
+ <uv-popup ref="popupTemplete" mode="center" round="30rpx">
+ <view class="popup_pwd">
+ <view class="title">发送LED</view>
+ <view class="cont cont_led">
+ <view class="led_item" v-for="(item,index) in templeteData" :key="index">
+ <view class="name">
+ {{item.name}}
+ </view>
+ <view class="name">
+ {{item.screen}}
+ </view>
+ <uv-button type="primary" shape="circle" size="small"
+ @click="commandSendTemplete(item)">播放</uv-button>
+ </view>
+ </view>
+ <view class="btns">
+ <uv-button type="primary" plain @click="this.$refs.popupTemplete.close();">关闭</uv-button>
+ </view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ import {
+ equipmentAdd,
+ templeteList
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ searchIng: true,
+ bluetoothList: [], //搜索到的蓝牙设备列表
+ deviceId: '', //设备id
+ serviceId: '', //服务uuid
+ characteristicId1: '', //特征uuid
+ characteristicId2: '',
+ characteristicId3: '',
+ showPwdPopup: false,
+ password: '', //设置的密码
+ checId: '', //智能壳的ID
+ batchNumber: '', //生产顺序号
+ fileNames: [], //文件名称列表
+ brightness: 3, //亮度
+ nameIndex: 1,
+ name: '',
+
+ ledName: '', // 名称
+ screen: '',
+ ledTotal: 3, // 总帧数
+ ledCurrent: 0, // 当前帧数
+ ledData: [],
+ isLink: false,
+ deviceName: '',
+ versions: '',
+ isBluetooth: true,
+ lastBluetooth: null,
+ isLocation: true,
+ platform: ''
+ // ledData: [
+ // '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ // '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ // '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF0000FF0000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
+ // ]
+ }
+ },
+ onLoad() {
+ console.log('onLoad-----')
+
+ },
+ onShow() {
+ const systemInfo = uni.getSystemInfoSync();
+ if (systemInfo.platform === 'ios') {
+ // iOS 设备
+ this.platform = 'ios'
+ } else if (systemInfo.platform === 'android') {
+ // Android 设备
+ this.platform = 'android'
+ }
+ console.log('onShow-----')
+ this.openBluetooth()
+ this.getTempleteList()
+ //this.checkBluetoothEnabled()
+ },
+ onHide() {
+ console.log('onHide----aaa-')
+ uni.$off('BLECharacteristic')
+ this.closeBLEConnection()
+ this.closeBluetooth()
+ this.stopBluetoothDevices()
+ },
+ onUnload() {
+ uni.$off('BLECharacteristic')
+ console.log('onUnload-----aaa')
+ this.closeBLEConnection()
+ this.closeBluetooth()
+ this.stopBluetoothDevices()
+ },
+ methods: {
+ // 获取模版列表
+ getTempleteList() {
+ templeteList({
+ userId: uni.getStorageSync('userId'),
+ pageNum: 1,
+ pageSize: 10
+ }).then(res => {
+ //console.log('模版列表-------', res.rows)
+ this.templeteData = res.rows
+ })
+ },
+ // 添加设备
+ postEquipmentAdd() {
+
+ //console.log('this.checId---', this.checId)
+ equipmentAdd({
+ userId: uni.getStorageSync('userId'),
+ code: this.platform,
+ password: this.password,
+ name: this.deviceName, //设备名称
+ checId: this.checId, //智能壳的ID
+ deviceId: this.deviceId, //设备id
+ serviceId: this.serviceId, //服务id
+ characteristicId1: this.characteristicId1, //特征id
+ characteristicId2: this.characteristicId2,
+ characteristicId3: this.characteristicId3,
+ versions: this.versions
+ }).then(res => {
+ console.log(res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if (res.code == 200) {
+ this.closeBLEConnection()
+ setTimeout(() => {
+ uni.switchTab({
+ url: '/pages/index/index'
+ });
+ }, 300);
+ }
+ })
+ },
+ changeBrightness(value) {
+ //console.log('changeBrightness----', value)
+ //this.brightness = value
+ },
+ // 设置名称
+ confirmName() {
+ this.$refs.popupName.close();
+ this.commandQVER()
+ uni.showLoading({
+ title: '添加中...',
+ mask: true
+ });
+ //this.postEquipmentAdd()
+ },
+ changePopupName(e) {
+ console.log(e)
+ },
+ changePopupPwd(e) {
+ console.log(e.show)
+ if (e.show) {
+ this.password = ''
+ } else {
+ //this.closeBLEConnection()
+ }
+ },
+ // 初始化蓝牙
+ openBluetooth() {
+ uni.openBluetoothAdapter({
+ success: (res) => {
+ console.log('蓝牙初始化成功');
+ // uni.onBluetoothAdapterStateChange(function (res) {
+ // console.log('监听蓝牙适配器状态变化事件------', res)
+ // })
+ this.isBluetooth = true
+ this.startBluetoothDevices()
+ },
+ fail: (err) => {
+ //console.log('蓝牙初始化失败----', err);
+ if (err.code == 10001) {
+ console.log('蓝牙开关未开启----', err);
+ this.isBluetooth = false
+
+ this.lastBluetooth = false
+ uni.onBluetoothAdapterStateChange(res => {
+ console.log('蓝牙状态变化:', res.available)
+
+ // 仅当状态从 false 变成 true 时执行一次
+ if (this.lastBluetooth == false && res.available == true) {
+ console.log('蓝牙开启,执行 openBluetooth()')
+ this.openBluetooth()
+ }
+ this.lastBluetooth = res.available
+ this.isBluetooth = res.available
+ })
+ }
+ },
+ complete: () => {
+
+ }
+ });
+ },
+ // 关闭蓝牙
+ closeBluetooth() {
+ uni.closeBluetoothAdapter({
+ success: (res) => {
+ console.log(res)
+ }
+ })
+ },
+ //开始搜寻附近的蓝牙外围设备
+ startBluetoothDevices() {
+ const that = this
+ uni.startBluetoothDevicesDiscovery({
+ success: (res) => {
+ console.log('开始扫描设备', res);
+ that.onBluetoothDeviceFound();
+ },
+ fail: (err) => {
+ console.log('扫描失败', err);
+ if(err.code == 10016){
+ //that.isLocation = false
+ const timerLocation = setInterval(function() {
+ const systemSetting = uni.getSystemSetting()
+ that.isLocation = systemSetting.locationEnabled
+ console.log("执行一次",this.isLocation,systemSetting.locationEnabled);
+ if(that.isLocation){
+ clearInterval(timerLocation);
+ that.openBluetooth()
+ }
+ }, 500);
+ }
+ }
+ });
+ },
+ // 停止搜寻附近的蓝牙外围设备
+ stopBluetoothDevices() {
+ uni.stopBluetoothDevicesDiscovery({
+ success: (res) => {
+ console.log(res)
+ },
+ fail: (err) => {
+ console.log(err)
+ }
+ });
+ },
+ // 监听寻找到新设备的事件
+ onBluetoothDeviceFound() {
+ uni.onBluetoothDeviceFound(res => {
+ //console.log('找到新设备', res);
+ res.devices.forEach((device) => {
+ if (device.name.includes('CH9140')) {
+ this.bluetoothList.push(device);
+ console.log('找到新设备----', this.bluetoothList);
+ this.stopBluetoothDevices()
+ setTimeout(() => {
+ this.searchIng = false
+ }, 500);
+ }
+ });
+ });
+ },
+ // 连接低功耗蓝牙设备
+ createBLEConnection(item) {
+ console.log('连接de蓝牙设备--------', item)
+ const that = this
+ uni.showLoading({
+ title: '连接中...',
+ mask: true
+ });
+ that.name = item.name
+ that.deviceId = item.deviceId
+ uni.createBLEConnection({
+ deviceId: that.deviceId,
+ success: (res) => {
+ console.log('连接低功耗蓝牙设备', res);
+ setTimeout(() => {
+ //that.setBLEMTU()
+ that.getServices();
+ }, 500);
+ },
+ fail: (err) => {
+ console.log('连接失败', err);
+ this.closeBLEConnection()
+ uni.hideLoading();
+ uni.showToast({
+ title: '连接失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ // 断开与低功耗蓝牙设备的连接
+ closeBLEConnection() {
+ uni.closeBLEConnection({
+ deviceId: this.deviceId,
+ success(res) {
+ console.log(res)
+ }
+ })
+ },
+ setBLEMTU() {
+ uni.setBLEMTU({
+ deviceId: this.deviceId,
+ mtu: 80, // 最大值一般 512,蓝牙 4.2 通常支持 247
+ success: (res) => {
+ console.log('设置MTU成功:', res);
+ },
+ fail: (err) => {
+ console.error('设置MTU失败:', err);
+ }
+ });
+ },
+ // 获取蓝牙设备所有服务
+ getServices() {
+ console.log(this.deviceId)
+ uni.getBLEDeviceServices({
+ deviceId: this.deviceId,
+ success: (res) => {
+ console.log('服务列表成功', res);
+ this.serviceId = res.services.find(item => item.uuid.toLowerCase().includes('fff0'))?.uuid;
+ this.getCharacteristics();
+ },
+ fail: (err) => {
+ console.error('服务列表失败:', err);
+ uni.hideLoading();
+ uni.showToast({
+ title: '服务列表失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ // 获取蓝牙设备某个服务中所有特征值(characteristic)
+ getCharacteristics() {
+ uni.getBLEDeviceCharacteristics({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ success: (res) => {
+ console.log('特征列表', res);
+ this.characteristicId1 = res.characteristics[0].uuid
+ this.characteristicId2 = res.characteristics[1].uuid
+ this.characteristicId3 = res.characteristics[2].uuid
+ this.startNotify()
+ setTimeout(() => {
+ this.setBaud()
+ }, 500);
+ }
+ });
+ },
+ // 设置波特率
+ setBaud() {
+ var that = this
+ var hexStr = "06000900009600000801009F"
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId3,
+ value: this.hexStringToArrayBuffer(hexStr),
+ writeType: 'write',
+ success: (res) => {
+ console.log('设置波特率成功----', res);
+ that.setBLEMTU()
+ that.commandCHEC()
+ },
+ fail: (err) => {
+ console.log('设置波特率失败----', err);
+ }
+ });
+ },
+ // 开始监听
+ startNotify() {
+ const that = this
+ uni.notifyBLECharacteristicValueChange({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId1,
+ state: true,
+ success: () => {
+ console.log('开始监听------');
+ uni.$off('BLECharacteristic');
+ uni.$on('BLECharacteristic',function(res){
+ console.log('BLECharacteristic---------------cc', res)
+ const resData = that.ab2hex(res.value);
+ const result = that.removeStartBytes(resData)
+ const resName = that.getCommandType(result);
+ console.log("回调返回数据-------add", resData);
+ console.log('result-----------add', result)
+ console.log('resName----------add', resName)
+ uni.hideLoading();
+ if (resName == 'CHEC') {
+ that.parseBLECHECResponse(result)
+ } else if (resName == 'BIND') {
+ that.$refs.popupName.open();
+ } else if (resName == 'LIST') {
+ that.parseFileList(result)
+ } else if (resName == 'BRIG') {
+
+ } else if (resName == 'PLAY') {
+
+ } else if (resName == 'INST') {
+ // 添加动画帧
+ that.ledCurrent++
+ if (that.ledCurrent < that.ledTotal) {
+ uni.showLoading({
+ title: '发送中' + that.ledCurrent,
+ mask: true
+ });
+ that.commandINST(that.ledData[that.ledCurrent]);
+ } else if (that.ledCurrent == that.ledTotal) {
+ uni.showLoading({
+ title: '发送结束帧',
+ mask: true
+ });
+ that.commandEnd()
+ }
+
+ } else if (resName == 'DALL') {
+
+ } else if (resName == 'QVER') {
+ // 查询版本信息
+ that.versions = parseInt(result.slice(-8, -6), 16);
+ that.postEquipmentAdd()
+ } else {
+ that.isLink == false
+ uni.showToast({
+ title: '返回数据错误',
+ duration: 2000,
+ icon: 'none'
+ });
+ // 关闭蓝牙设备
+ that.closeBLEConnection()
+ }
+ })
+ //uni.onBLECharacteristicValueChange(res => {
+ // const resData = that.ab2hex(res.value);
+ // const result = that.removeStartBytes(resData)
+ // const resName = that.getCommandType(result);
+ // console.log("回调返回数据-------add", resData);
+ // console.log('result-----------add', result)
+ // console.log('resName----------add', resName)
+ // uni.hideLoading();
+ // if (resName == 'CHEC') {
+ // that.parseBLECHECResponse(result)
+ // } else if (resName == 'BIND') {
+ // that.$refs.popupName.open();
+ // } else if (resName == 'LIST') {
+ // that.parseFileList(result)
+ // } else if (resName == 'BRIG') {
+
+ // } else if (resName == 'PLAY') {
+
+ // } else if (resName == 'INST') {
+ // // 添加动画帧
+ // that.ledCurrent++
+ // if (that.ledCurrent < that.ledTotal) {
+ // uni.showLoading({
+ // title: '发送中' + that.ledCurrent,
+ // mask: true
+ // });
+ // that.commandINST(that.ledData[that.ledCurrent]);
+ // } else if (that.ledCurrent == that.ledTotal) {
+ // uni.showLoading({
+ // title: '发送结束帧',
+ // mask: true
+ // });
+ // that.commandEnd()
+ // }
+
+ // } else if (resName == 'DALL') {
+
+ // } else if (resName == 'QVER') {
+ // // 查询版本信息
+ // that.versions = parseInt(result.slice(-8, -6), 16);
+ // that.postEquipmentAdd()
+ // } else {
+ // that.isLink == false
+ // uni.showToast({
+ // title: '返回数据错误',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // // 关闭蓝牙设备
+ // that.closeBLEConnection()
+ // }
+ //});
+ }
+ });
+ },
+ // 判断并去除10个0x55启动字节(20个hex字符)
+ removeStartBytes(hexStr) {
+ // 10个0x55 = 20个hex字符
+ const startBytes = '55'.repeat(10); // '55555555555555555555'
+ if (hexStr.startsWith(startBytes)) {
+ return hexStr.slice(20); // 去掉前20个字符
+ }
+ return hexStr;
+ },
+ // 返回指令类型 判断
+ getCommandType(hexString) {
+ // 判断是否有 返回标志 单字节。0(0x30),工作无异常;1(0x31)工作有异常
+ // 判断是否包含返回标志 30 或 31
+ // 查找30或31的位置
+ let returnFlag = '';
+ let cmd = '';
+ let packageType = hexString.slice(2, 4);
+
+ if (packageType == '41') {
+ returnFlag = hexString.slice(12, 14);
+ const before8Chars = hexString.slice(4, 12);
+ cmd = before8Chars.match(/.{2}/g)
+ .map(h => String.fromCharCode(parseInt(h, 16)))
+ .join('');
+ return cmd;
+ } else if (packageType == '43') {
+ returnFlag = hexString.slice(28, 30);
+ const before8Chars = hexString.slice(20, 28);
+ cmd = before8Chars.match(/.{2}/g)
+ .map(h => String.fromCharCode(parseInt(h, 16)))
+ .join('');
+ return cmd;
+ }
+ console.log('returnFlag------', returnFlag)
+ },
+ // 查询智能壳ID
+ // 构建指令数据
+ commandCHEC() {
+ //555555555555555555557e41434845430095977e
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x43, 0x48, 0x45, 0x43]; // "CHEC"的ASCII码
+ const reserved = 0x00; // 保留字节
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ reserved
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('CHEC data----AAAA--------', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ // 加密绑定号 发送绑定号
+ commandBIND() {
+ console.log('checId---', this.checId)
+ // 59 5C 37 39 71 4D 56 65 5A 37 12 18 07 06 2A 18 36 1E 1E 06
+ // 7e 41 42 49 4e 44 6d 71 41 45 95 4e 61 78 9e 39 49 12 15 13 71 42 94 50 50 17 7c c9 7e
+ //帧头+0x41+ BIND +加密绑定号+CRC校验码+帧尾
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x42, 0x49, 0x4E, 0x44]; // "BIND"的ASCII码
+ const reserved = this.replaceScreenSizeInID(this.checId, this.password); // 密码
+ console.log('reserved----111', reserved)
+ const reservedId = this.encryptSmartShellID(reserved)
+ console.log('reservedid--222', reservedId)
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ ...this.hexStringToArray(reservedId)
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('BIND data----BBBB--------', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ //关屏指令(BLAC)
+ commandBLAC() {
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x42, 0x49, 0x4E, 0x44]; // "BLAC"的ASCII码
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('data----', data)
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ // 查询版本信息
+ commandQVER() {
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x51, 0x56, 0x45, 0x52]; // "QVER"的ASCII码
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('QVER----', data)
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ //查询智能壳已存信息序列(LIST)
+ commandLIST() {
+ //1.手机发指令:启动字节+帧头+包头段(0x43+简易绑定号+屏大小)+LIST+信息属性+CRC校验码+帧尾。
+ //2.智能壳指令:启动字节+帧头+包头段+LIST+返回标志+加密地址+文件数+[信息名1+信息名2+...+信息名n]+CRC校验码+帧尾。
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x4C, 0x49, 0x53, 0x54]; // "LIST"的ASCII码
+ const infoAttribute = 0x00; // 信息属性: 0=一般信息
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('LIST data----', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ //指定节目显示(PLAY)
+ commandPLAY(nameHex) {
+ console.log('nameHex----', nameHex, typeof nameHex)
+ //1.手机发指令:启动字节+帧头+0x41+ PLAY +信息属性+文件名+CRC校验码+帧尾。
+ //2.智能壳指令:启动字节+帧头+0x41+ PLAY +返回标志+CRC校验码+帧尾
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x50, 0x4C, 0x41, 0x59]; // "PLAY"的ASCII码
+ const infoAttribute = 0x00; // 信息属性: 0=一般信息
+ //const fileName = [0x42, 0x48] //文件名称
+ const fileName = this.hexStringToArray(nameHex)
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ infoAttribute,
+ ...fileName
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('PLAY data----', data)
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ // 调节亮度
+ commandBRIG() {
+ console.log('brightness----', this.brightness)
+ //1.手机发指令:启动字节+帧头+0x41+ BRIG +亮度+CRC校验码+帧尾。
+ //2.智能壳回指令:启动字节+帧头+0x41+ BRIG +返回标志+CRC校验码+帧尾。
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x42, 0x52, 0x49, 0x47]; // "BRIG"的ASCII码
+
+ //亮度:单字节,0,1,2,3。。。ASIC码
+ const brightness = this.stringToASCIIByteArray(this.brightness.toString())
+ //const brightness = 0x32
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ brightness
+ ];
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('BRIG data----', data)
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+
+ },
+ commandSendTemplete(item) {
+ console.log('--------------', item)
+ uni.showLoading({
+ title: '发送中...',
+ mask: true
+ });
+ const name = 'A' + this.nameIndex++
+ this.ledName = name
+ this.screen = item.screen
+ this.ledTotal = item.fpsTotal
+ this.ledCurrent = 0
+ this.ledData = JSON.parse(item.fpsData)
+ this.commandINST(this.ledData[this.ledCurrent]);
+ },
+ // 获取画板数据
+ getDrawData() {
+ uni.showLoading({
+ title: '发送中...',
+ mask: true
+ });
+ const name = 'A' + this.nameIndex++
+ this.ledName = name
+ console.log('name--------', name)
+ // uni.getStorage({
+ // key: 'pixelData',
+ // success: (res) => {
+ // this.commandINST(res.data)
+ // }
+ // });
+ this.commandINST(this.ledData[this.ledCurrent]);
+ },
+ //添加信息指令(INST)
+ commandINST(hexString) {
+ // 1.手机发指令:启动+帧头+包头段(0x43+简易绑定号+屏大小)+INST+信息属性+文件名+总帧数+当前帧*+块描述+块数据+CRC校验码+帧尾
+ //注意:当前帧如果是0xff,则表示是结束帧,后面四个字节,第一个字节(表示速度),第二个字节(播放次数,0表示一直播放),后面二个字节保留;其他数字表示表示当前是第几帧。
+ //2.智能壳指令:启动+帧头+包头段+INST+返回标志+加密地址+CRC校验码+帧尾
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x49, 0x4E, 0x53, 0x54]; // "INST"的ASCII码
+ const infoAttribute = 0x02; // 信息属性: 0=一般信息
+
+ const fileName = this.stringToASCIIByteArray(this.ledName) //文件名称
+ //console.log('fileName----', this.ledName)
+ // 总帧数
+ const totalFrame = this.hexStringToArray(this.ledTotal.toString(16).padStart(2, '0')
+ .toUpperCase()) //[0x01]; //this.hexStringToArray('01')
+ //console.log('totalFrame----', totalFrame)
+ // 当前帧
+ const currentFrame = this.hexStringToArray(this.ledCurrent.toString(16).padStart(2, '0')
+ .toUpperCase()) //0x00
+
+ // 块描述
+ const tblockDesc = 0x00
+
+ // 块数据
+ const blockDescription = this.hexStringToArray(hexString)
+ //const blockString = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF000000000000000000000000000000000000000000000000000000000000FF0000000000000000000000000000000000000000000000FF0000000000000000FF0000FF00000000000000000000000000000000FF0000FF000000000000000000FF0000000000FF0000FF00FF0000FF0000FF000000000000000000FF00FF0000FF000000000000000000FF00000000000000000000FF000000FF0000FF0000FF0000FF0000000000FF0000000000000000000000FF0000000000000000000000FF0000FF0000000000000000FF0000FF0000000000000000000000000000FF00FF000000000000000000000000FF00FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF0000000000000000FF0000FF000000000000000000000000FF0000000000FF0000000000000000000000000000000000000000FF0000000000000000000000000000000000FF0000000000000000000000000000000000FF00000000000000000000000000FF0000FF000000FF0000FF0000000000000000000000FF00000000000000FF0000FF000000FF00FF0000FF000000000000FF0000000000000000000000000000000000000000000000FF0000000000FF0000000000000000000000FF0000000000000000000000000000000000000000FF0000000000000000FF0000000000000000FF0000000000000000FF00000000000000FF0000FF0000FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF000000000000000000000000000000000000FF0000FF0000000000000000FF0000000000000000FF0000000000000000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ //const blockDescription = this.hexStringToArray(blockString)
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute,
+ ...fileName,
+ ...totalFrame,
+ ...currentFrame,
+ tblockDesc,
+ ...blockDescription
+ ];
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log(this.ledCurrent, '----INST data----', this.hexArrayToHexString(data))
+ //const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ //this.writeBLECharacteristicValue(buffer)
+ this.sendBLEDataInChunks(
+ this.hexArrayToHexString(data)
+ )
+ }, 1000);
+ },
+ // 发送结束针
+ commandEnd() {
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x49, 0x4E, 0x53, 0x54]; // "INST"的ASCII码
+ const infoAttribute = 0x02; // 信息属性: 0=一般信息
+
+ const fileName = this.stringToASCIIByteArray(this.ledName) //文件名称
+ //console.log('fileName----', this.ledName)
+ // 总帧数
+ const totalFrame = this.hexStringToArray(this.ledTotal.toString(16).padStart(2, '0')
+ .toUpperCase()) //[0x01]; //this.hexStringToArray('01')
+ //console.log('totalFrame----', totalFrame)
+
+ //结束幕:
+ const ffdata = this.hexStringToArray('ff10000000')
+ const endData = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute,
+ ...fileName,
+ ...totalFrame,
+ ...ffdata
+ ]
+ // 计算CRC校验码(从包头段开始计算)
+ const endcrc = this.calcCRC16(endData.slice(11));
+
+ // 添加CRC和帧尾
+ endData.push((endcrc >> 8) & 0xFF); // CRC高8位
+ endData.push(endcrc & 0xFF); // CRC低8位
+ endData.push(frameHeader); // 帧尾
+ console.log('INST endData----', this.hexArrayToHexString(endData))
+
+ const endbuffer = new Uint8Array(endData).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(endbuffer)
+ }, 1000);
+ },
+ async sendBLEDataInChunks(hexStr) {
+ const maxBytes = 60
+ const interval = 50
+ const chunks = this.splitHexData(hexStr, maxBytes)
+
+ console.log(`📦 总共需要发送 ${chunks.length} 包,每包 ${maxBytes} 字节,间隔 ${interval}ms`)
+
+ for (let i = 0; i < chunks.length; i++) {
+ const buffer = this.hexToBuffer(chunks[i])
+ try {
+ await new Promise((resolve, reject) => {
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId2,
+ value: buffer,
+ success: (res) => {
+ console.log(`✅ 第 ${i + 1} 包发送成功`, res)
+ resolve()
+ },
+ fail: (err) => {
+ console.error(`❌ 第 ${i + 1} 包发送失败`, err)
+ reject(err)
+ }
+ })
+ })
+ } catch (err) {
+ console.error(`🚫 发送中断,第 ${i + 1} 包发送失败`)
+ break
+ }
+ // 延迟下一包发送
+ await new Promise(r => setTimeout(r, interval))
+ }
+ console.log("🎉 数据发送完成")
+ // setTimeout(() => {
+ // uni.showLoading({
+ // title: '发送中...',
+ // mask: true
+ // });
+ // this.commandEnd()
+ // }, 500)
+ },
+ // 删除单个
+ commandDELL(nameHex) {
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x44, 0x45, 0x4C, 0x4C]; // "DELL"的ASCII码
+ const infoAttribute = 0x00; // 信息属性: 0=一般信息
+ //const fileName = [0x42, 0x48] //文件名称
+ const fileName = this.hexStringToArray(nameHex)
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute,
+ ...fileName
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('DELL data----', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+ // 删除全部
+ commandDALL() {
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x44, 0x41, 0x4C, 0x4C]; // "DALL"的ASCII码
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('DALL data----', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 1000);
+ },
+
+ // 生成简易绑定号
+ generateSimpleBindId() {
+ if (!this.checId || !this.password) {
+ //console.error("生成简易绑定号失败:checId或password不可用");
+ uni.hideLoading();
+ uni.showToast({
+ title: '请先连接设备',
+ icon: 'none',
+ mask: true
+ });
+ return;
+ }
+ // 从checId中提取生产顺序号(最后4字节,即8个字符)
+ const productionNumber = this.checId.slice(-8); // 生产顺序号
+ // 生产顺序号+密码
+ const simpleBindData = productionNumber + this.password;
+ // 用加密数字加密
+ const encryptedSimpleBind = this.encryptSmartShellID(simpleBindData);
+ // 取前6字节作为简易绑定号
+ const simpleBindId = this.hexStringToArray(encryptedSimpleBind.slice(0, 12));
+ //console.log('简易绑定号生成----', simpleBindId);
+ return simpleBindId;
+ },
+
+ //向低功耗蓝牙设备特征值中写入二进制数据
+ writeBLECharacteristicValue(buffer) {
+ const that = this
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId2,
+ value: buffer,
+ writeType: 'write',
+ success: (res) => {
+ console.log('指令发送成功2----', res);
+ },
+ fail: (err) => {
+ console.log('指令发送失败2----', err);
+ uni.hideLoading();
+ uni.showToast({
+ title: '指令发送失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ //向低功耗蓝牙设备特征值中写入二进制数据2222222
+ writeBLEValue(buffer) {
+ const that = this
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId3,
+ value: buffer,
+ writeType: 'write',
+ success: (res) => {
+ console.log('指令发送成功----3', res);
+
+ },
+ fail: (err) => {
+ console.log('指令发送失败----3', err);
+ }
+ });
+ },
+ // 将十六进制字符串转为 ArrayBuffer
+ hexToBuffer(hexStr) {
+ const typedArray = new Uint8Array(hexStr.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
+ return typedArray.buffer
+ },
+
+ // 将大十六进制数据切分为每段 maxBytes 大小(单位:字节)
+ splitHexData(hexStr, maxBytes = 100) {
+ const result = []
+ for (let i = 0; i < hexStr.length; i += maxBytes * 2) {
+ result.push(hexStr.slice(i, i + maxBytes * 2))
+ }
+ return result
+ },
+
+ // 获取文件名称文件数量
+ parseFileList(hexString) {
+ // 文件数占1字节(2个十六进制字符
+ const fileCountHex = hexString.slice(32, 34);
+ const fileCount = parseInt(fileCountHex, 16);
+ console.log('文件数:', fileCount);
+
+ // 获取文件名称列表
+ const fileNames = [];
+ let nameStartIndex = 34; // 文件数后开始
+
+ for (let i = 0; i < fileCount; i++) {
+ // 每个信息名占2个字节(4个十六进制字符)
+ const nameHex = hexString.slice(nameStartIndex, nameStartIndex + 4);
+ const nameBytes = nameHex.match(/.{2}/g);
+ if (nameBytes && nameBytes.length === 2) {
+ // 将两个字节转换为ASCII字符
+ const char1 = String.fromCharCode(parseInt(nameBytes[0], 16));
+ const char2 = String.fromCharCode(parseInt(nameBytes[1], 16));
+ const fileName = char1 + char2;
+ fileNames.push({
+ fileName: fileName,
+ nameHex: nameHex
+ });
+ }
+
+ nameStartIndex += 4; // 移动到下一个文件名
+ }
+ console.log('文件名称列表:', fileNames);
+ this.fileNames = fileNames
+ },
+ // 指令ASCII码转换
+ stringToASCIIByteArray(str) {
+ return str.split('').map(char => char.charCodeAt(0));
+ },
+ //16进制数组 转 16进制字符串
+ hexArrayToHexString(hexArray) {
+ return hexArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
+ },
+ // 16进制字符串转16进制数组
+ hexStringToArray(hexStr) {
+ if (!hexStr) return [];
+ return hexStr.match(/.{2}/g).map(byte => parseInt(byte, 16));
+ },
+ // 16进制数据转二进数据
+ hexStringToArrayBuffer(hexStr) {
+ const bytes = new Uint8Array(hexStr.match(/.{2}/g).map(byte => parseInt(byte, 16)));
+ return bytes.buffer;
+ },
+ // 二进数据转16进制数据
+ ab2hex(buffer) {
+ const hexArr = Array.prototype.map.call(
+ new Uint8Array(buffer),
+ function(bit) {
+ return ('00' + bit.toString(16)).slice(-2)
+ }
+ )
+ return hexArr.join('')
+ },
+ calcCRC16(data) {
+ // 创建查表
+ const table = this.buildCRC16Table();
+ let crc = 0;
+ // 计算CRC
+ for (let i = 0; i < data.length; i++) {
+ crc = ((crc << 8) & 0xFF00) ^ table[((crc >> 8) ^ data[i]) & 0xFF];
+ }
+ return crc & 0xFFFF;
+ },
+ buildCRC16Table() {
+ const table = new Array(256);
+ const poly = 0x1021; // CRC-CCITT多项式
+ for (let i = 0; i < 256; i++) {
+ let nData = i << 8;
+ let nAccum = 0;
+ for (let j = 0; j < 8; j++) {
+ if ((nData ^ nAccum) & 0x8000) {
+ nAccum = (nAccum << 1) ^ poly;
+ } else {
+ nAccum <<= 1;
+ }
+ nData <<= 1;
+ }
+ table[i] = nAccum & 0xFFFF;
+ }
+ return table;
+ },
+
+ // 设置密码
+ confirmPwd() {
+ console.log("✅ 用户密码:", this.password);
+ this.$refs.popupPwd.close();
+ this.commandBIND()
+ uni.showLoading({
+ title: '绑定中...',
+ mask: true
+ });
+ },
+ // 密码转16进制数组
+ passwordToHexArray(pwd) {
+ if (!/^\d{4}$/.test(pwd)) throw new Error('密码必须是4位数字');
+ return [
+ parseInt(pwd.slice(0, 2), 10),
+ parseInt(pwd.slice(2, 4), 10)
+ ];
+ },
+ // 密码置换掉ID号中"屏的大小",加密下发
+ replaceScreenSizeInID(originalHexString, password4Digit) {
+ // 密码4个数字,每二个合起来一个字节,如3472变成 0x34 0x72
+ const high = password4Digit.slice(0, 2); // "34"
+ const low = password4Digit.slice(2, 4); // "72"
+
+ // 替换屏幕大小字段(第 10、11 个字节,即第20-23个字符)
+ const before = originalHexString.slice(0, 20);
+ const after = originalHexString.slice(24);
+
+ return before + high + low + after;
+ },
+ // 提取智能壳加密ID
+ parseBLECHECResponse(hexStr) {
+ const encryptedID = hexStr.slice(14, -6);
+ console.log('加密id-------', typeof encryptedID, encryptedID);
+ if(!encryptedID){
+ // uni.showToast({
+ // title: '请设置手电筒模式并在手电筒闪烁时点击确认',
+ // icon: 'none',
+ // mask: true,
+ // duration: 2000
+ // });
+ this.closeBLEConnection()
+ //this.openBluetooth()
+ uni.showModal({
+ title: '提示',
+ content: '请将设备设置手电筒模式并在手电筒闪烁时点击确认',
+ showCancel: false,
+ confirmText: '关闭',
+ success: function (res) {
+ if (res.confirm) {
+ console.log('用户点击确定');
+ }
+ }
+ });
+ return
+ }
+ const decryptId = this.decryptSmartShellID(encryptedID)
+ console.log('解密id-------', typeof decryptId, decryptId);
+ this.checId = decryptId
+ this.$refs.popupPwd.open();
+ },
+ // 解密函数
+ decryptSmartShellID(encryptedHex) {
+ const key = '2021101236011119680215001413714294505017';
+ const decrypted = [];
+ for (let i = 0; i < 20; i++) {
+ const byteHex = encryptedHex.substr(i * 2, 2);
+ const encryptedByte = parseInt(byteHex, 16);
+ const keyHex = key.substr(i * 2, 2)
+ const keyByte = parseInt(keyHex, 16);
+ const decryptedByte = (encryptedByte - keyByte);
+ decrypted.push(decryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+ return decrypted.join('');
+ },
+ // 加密函数
+ encryptSmartShellID(encryptedHex) {
+ const key = '2021101236011119680215001413714294505017';
+ const decrypted = [];
+ for (let i = 0; i < 20; i++) {
+ const byteHex = encryptedHex.substr(i * 2, 2);
+ const encryptedByte = parseInt(byteHex, 16);
+ const keyHex = key.substr(i * 2, 2)
+ const keyByte = parseInt(keyHex, 16);
+ const decryptedByte = (encryptedByte + keyByte);
+ decrypted.push(decryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+ return decrypted.join('');
+ },
+
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .btns_two {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 40rpx;
+ margin-top: 50rpx;
+
+ .btn2 {
+ width: 47%;
+ }
+ }
+
+ .cont_led {
+ max-height: 500rpx;
+ overflow-y: auto;
+ }
+
+ .popup_pwd {
+ width: 660rpx;
+ box-sizing: border-box;
+ padding: 0 50rpx 50rpx;
+
+ .led_item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10rpx 0;
+
+ .name {
+ font-size: 30rpx;
+ color: #000000;
+ line-height: 50rpx;
+ width: 100%;
+ box-sizing: border-box;
+ padding-right: 40rpx;
+ }
+
+ .btn {
+ flex-shrink: 0;
+ width: 120rpx;
+ }
+ }
+
+ .title {
+ text-align: center;
+ font-weight: 500;
+ font-size: 32rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 42rpx;
+ padding: 50rpx 0 40rpx;
+ }
+
+ .btns {
+ margin-top: 40rpx;
+ }
+ }
+
+ .bluetooth {
+ padding: 20rpx 30rpx 0;
+
+ .bluetooth_list {
+ .item {
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.2), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 20rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 22rpx;
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ margin-top: 24rpx;
+
+ .ph {
+ display: block;
+ flex-shrink: 0;
+ width: 230rpx;
+ height: 170rpx;
+ border-radius: 20rpx;
+ margin-right: 24rpx;
+ }
+
+ .info {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .name {
+ font-weight: 500;
+ font-size: 27rpx;
+ color: #000000;
+ line-height: 50rpx;
+ }
+
+ .btn {
+ width: 200rpx;
+ margin-top: 20rpx;
+ }
+ }
+ }
+ }
+
+ .bluetooth_title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 20rpx;
+
+ text {
+ display: block;
+ width: 100%;
+ font-weight: 500;
+ font-size: 34rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 38rpx;
+ }
+
+ .ico {
+ flex-shrink: 0;
+ width: 40rpx;
+ margin-right: 10rpx;
+ }
+ }
+ }
+
+ .searching_text {
+ text-align: center;
+ background: url('/static/img/bg_3.png') no-repeat center top;
+ background-size: 100% auto;
+ box-sizing: border-box;
+ padding-top: 140rpx;
+ margin-top: -100rpx;
+ height: 300rpx;
+
+ .name {
+ font-weight: 500;
+ font-size: 36rpx;
+ color: #000000;
+ line-height: 34rpx;
+ }
+
+ .text {
+ font-size: 26rpx;
+ color: rgba(0, 0, 0, 0.5);
+ line-height: 34rpx;
+ margin-top: 30rpx;
+ }
+ }
+
+ .searching_ico {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-top: 100rpx;
+ }
+
+ .page_bg {
+ width: 100%;
+ background: linear-gradient(3deg, #72E5FF 0%, #3B74EE 98%, #3B74EE 100%);
+ }
+
+ .page_bg1 {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addDevice/addDevice0.vue b/ledApp/pages/addDevice/addDevice0.vue
new file mode 100644
index 0000000..65f8b60
--- /dev/null
+++ b/ledApp/pages/addDevice/addDevice0.vue
@@ -0,0 +1,487 @@
+<template>
+ <view>
+ <uv-navbar title="添加设备" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="#3B74EE"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="page_bg">
+ <view class="searching_ico" @click="getBluetoothDevices">
+ <image src="/static/img/ico_1.png" mode="widthFix"></image>
+ </view>
+
+ <view class="searching_text">
+ <view class="name">
+ 搜索中
+ </view>
+ <view class="text">
+ 持续自动搜索中...
+ </view>
+ </view>
+ </view>
+
+ <view class="bluetooth" v-if="bluetoothList.length != 0">
+ <view class="bluetooth_title">
+ <image class="ico" src="/static/img/ico_2.png" mode="widthFix"></image>
+ <text>已找到设备</text>
+ </view>
+ <view class="bluetooth_list">
+ <view class="item" v-for="(item,index) in bluetoothList" :key="index">
+ <image class="ph" src="/static/img/ph_1.png" mode="aspectFill"></image>
+ <view class="info">
+ <view class="name">
+ {{item.device.name}}
+ </view>
+ <view class="btn">
+ <uv-button type="primary" shape="circle" text="连接" size="small"
+ @click="createBLEConnection(item.device.address)"></uv-button>
+
+ </view>
+
+ </view>
+ </view>
+ </view>
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import {
+ BleLib
+ } from '@/uni_modules/android-ble'
+ var lib = new BleLib()
+
+ export default {
+ data() {
+ return {
+ searchIng: true,
+ bluetoothList: [],
+ deviceId: '', //设备id
+ serviceId: '', //服务uuid
+ characteristicId: '', //特征uuid
+ characteristicId1: '',
+ characteristicId2: '',
+ characteristicId3: '',
+ bluetooth: false
+ }
+ },
+ onLoad() {
+
+ },
+ onShow() {
+ this.bluetooth = lib.isEnabled();
+ if (this.bluetooth) {
+ console.log('蓝牙开启', this.bluetooth)
+ this.getBluetoothDevices()
+ } else {
+ console.log('蓝牙关闭', this.bluetooth)
+ }
+ },
+ onUnload() {
+ //lib.close();
+ },
+ methods: {
+ // 蓝牙扫描
+ getBluetoothDevices() {
+ console.log('蓝牙扫描----')
+ const that = this
+ var isconnect = lib.isConnected();
+ console.log('蓝牙是否连接----',isconnect)
+ if(isconnect){
+ console.log('蓝牙是否连接----',isconnect)
+ var device = lib.getConnectMac();
+ console.log('device----',device)
+ }
+ lib.startScanBleDevice(15000, function(res) {
+ if (res.data.device.name === 'CH9140BLE2U') {
+ that.bluetoothList.push(res.data);
+ console.log(res.data)
+ }
+ })
+ },
+ // 连接蓝牙
+ createBLEConnection(address) {
+ console.log('address-----',address)
+ const that = this
+ const isconnect = lib.isConnected();
+ console.log('蓝牙是否连接----',isconnect)
+ // if(isconnect){
+ // const device = lib.getConnectMac();
+ // console.log('获取已连接设备id----',device)
+ // that.commandCHEC()
+ // }
+ // else{
+ lib.connect(address, false, function(res) {
+ console.log('连接', res)
+ // type==0 表示连接成功
+ if (res.type == 0) {
+ // 扫描蓝牙与特征值
+ lib.scanServices(function(resSevice) {
+ console.log('特征值--------', resSevice)
+ that.characteristicId1 = resSevice.data[2].characteristics[0].uuid;
+ that.characteristicId2 = resSevice.data[2].characteristics[1].uuid;
+ that.characteristicId3 = resSevice.data[2].characteristics[2].uuid;
+ that.onNotityReadBleData()
+ setTimeout(() => {
+ console.log("延迟执行这段代码");
+ that.setBaud()
+ //that.commandCHEC()
+ }, 2000);
+ });
+ // lib.setMtu(512, function(resMtu) {
+ // console.log('设置mtu(仅安卓)', resMtu)
+ // })
+
+
+ } else {
+ if(res.message == 'bluetooth is connected'){
+ //that.commandCHEC()
+ }
+ else{
+ uni.showToast({
+ title: '蓝牙连接失败',
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ }
+ })
+ // }
+ },
+ // 开启服务消息通知
+ onNotityReadBleData() {
+ const that = this
+ console.log(that.characteristicId1)
+ lib.onNotityReadBleData(lib.getSericUUID(),
+ that.characteristicId1,
+ true,
+ function(res) {
+ console.log('开启服务消息通知', res.data)
+
+ uni.$uv.debounce(that.extractEncryptedIDWithCheck(res.data), 2000)
+ }
+ )
+ },
+ // 构建指令数据
+ commandCHEC() {
+ //555555555555555555557e41434845430095977e
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x43, 0x48, 0x45, 0x43]; // "CHEC"的ASCII码
+ const reserved = 0x00; // 保留字节
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ reserved
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+
+ setTimeout(() => {
+ this.writeDataToBle(data)
+ }, 2000);
+ },
+ // 设置波特率
+ setBaud() {
+ var that = this
+ var d = "06000900009600000801009F"
+ lib.writeStringDataToBle(
+ lib.getSericUUID(),
+ that.characteristicId2,
+ d,
+ function(res) {
+ console.log('设置波特率', res)
+ that.commandCHEC()
+ }
+ )
+ },
+
+ // 发送写入指令
+ writeDataToBle(b) {
+ var that = this
+ lib.writeDataToBle(
+ lib.getSericUUID(),
+ that.characteristicId2,
+ b,
+ function(res) {
+ console.log('发送16进制数组', res)
+
+ }
+ )
+ },
+ writeStringDataToBle() {
+ var that = this
+ var d = "555555555555555555557e41434845430095977e"
+ lib.writeStringDataToBle(
+ lib.getSericUUID(),
+ that.characteristicId2,
+ d,
+ function(res) {
+ console.log('发送16进制字符数据', res)
+ }
+ )
+ },
+ // 16进制数据转二位数据
+ hexStringToArrayBuffer(hexStr) {
+ const bytes = new Uint8Array(hexStr.match(/.{2}/g).map(byte => parseInt(byte, 16)));
+ return bytes.buffer;
+ },
+ // 二位数据转16进制数据
+ ab2hex(buffer) {
+ const hexArr = Array.prototype.map.call(
+ new Uint8Array(buffer),
+ function(bit) {
+ return ('00' + bit.toString(16)).slice(-2)
+ }
+ )
+ return hexArr.join('')
+ },
+ calcCRC16(data) {
+ // 创建查表
+ const table = this.buildCRC16Table();
+ let crc = 0;
+
+ // 计算CRC
+ for (let i = 0; i < data.length; i++) {
+ crc = ((crc << 8) & 0xFF00) ^ table[((crc >> 8) ^ data[i]) & 0xFF];
+ }
+
+ return crc & 0xFFFF;
+ },
+ buildCRC16Table() {
+ const table = new Array(256);
+ const poly = 0x1021; // CRC-CCITT多项式
+
+ for (let i = 0; i < 256; i++) {
+ let nData = i << 8;
+ let nAccum = 0;
+
+ for (let j = 0; j < 8; j++) {
+ if ((nData ^ nAccum) & 0x8000) {
+ nAccum = (nAccum << 1) ^ poly;
+ } else {
+ nAccum <<= 1;
+ }
+ nData <<= 1;
+ }
+ table[i] = nAccum & 0xFFFF;
+ }
+ return table;
+ },
+ // 判断返回数据是否正确 并提取加密id
+ extractEncryptedIDWithCheck(buffer) {
+ const hexArr = Array.prototype.map.call(
+ new Uint8Array(buffer),
+ function(bit) {
+ return ('00' + bit.toString(16)).slice(-2)
+ }
+ )
+ const responseHex = hexArr.join('').slice(20)
+ console.log('responseHex---',responseHex)
+ // 1. 提取返回标志位置(第11字节,即下标:30~31)
+ const returnFlagHex = responseHex.substring(12, 14).toUpperCase();
+ console.log('returnFlagHex---',returnFlagHex)
+ if (returnFlagHex === "30") {
+ // 正常,提取加密ID(从下标 32 开始,取 40 位
+ const encryptedID = responseHex.substring(14, 55).toUpperCase();
+ console.log('encryptedID---',encryptedID)
+ uni.showToast({
+ title: encryptedID,
+ duration: 2000,
+ icon: 'none'
+ });
+ } else if (returnFlagHex === "31") {
+ // 异常,无加密ID
+ uni.showToast({
+ title: '智能壳工作异常,返回标志0x31',
+ duration: 2000,
+ icon: 'none'
+ });
+ lib.close();
+ } else {
+ // 未知标志
+ uni.showToast({
+ title: '智能壳工作异常,未知返回标志',
+ duration: 2000,
+ icon: 'none'
+ });
+ lib.close();
+ }
+ },
+ // 解密函数
+ decryptSmartShellID(encryptedHex) {
+ if (encryptedHex.length !== 40) {
+ console.error('加密ID长度必须为40个十六进制字符(20字节)');
+ return null;
+ }
+
+ const key = [
+ 0x14, 0x15, 0x0A, 0x0C, 0x24, 0x01, 0x0B, 0x13, 0x44, 0x02,
+ 0x0F, 0x00, 0x0E, 0x0D, 0x47, 0x2A, 0x5E, 0x32, 0x32, 0x11
+ ];
+
+ const decrypted = [];
+
+ for (let i = 0; i < 20; i++) {
+ const byteHex = encryptedHex.substr(i * 2, 2);
+ const encryptedByte = parseInt(byteHex, 16);
+ const decryptedByte = (encryptedByte - key[i] + 256) % 256;
+ decrypted.push(decryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+
+ return decrypted.join(' ');
+ },
+ // 加密函数
+ encryptSmartShellID(originalHex) {
+ if (originalHex.length !== 40) {
+ console.error('原始ID长度必须为40位十六进制字符串(20字节)');
+ return null;
+ }
+
+ // 加密密钥(20 字节)
+ const key = [
+ 0x14, 0x15, 0x0A, 0x0C, 0x24, 0x01, 0x0B, 0x13, 0x44, 0x02,
+ 0x0F, 0x00, 0x0E, 0x0D, 0x47, 0x2A, 0x5E, 0x32, 0x32, 0x11
+ ];
+
+ const encrypted = [];
+
+ for (let i = 0; i < 20; i++) {
+ const byteHex = originalHex.substr(i * 2, 2);
+ const originalByte = parseInt(byteHex, 16);
+ const encryptedByte = (originalByte + key[i]) % 256; // 忽略进位
+ encrypted.push(encryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+
+ return encrypted.join('');
+ }
+
+
+
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .bluetooth {
+ padding: 0 30rpx;
+
+ .bluetooth_list {
+ .item {
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 20rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 22rpx;
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ margin-top: 24rpx;
+
+ .ph {
+ display: block;
+ flex-shrink: 0;
+ width: 230rpx;
+ height: 170rpx;
+ border-radius: 20rpx;
+ margin-right: 24rpx;
+ }
+
+ .info {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .name {
+ font-weight: 500;
+ font-size: 27rpx;
+ color: #000000;
+ line-height: 50rpx;
+ }
+
+ .btn {
+ width: 160rpx;
+ margin-top: 10rpx;
+ }
+ }
+ }
+ }
+
+ .bluetooth_title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 20rpx;
+
+ text {
+ display: block;
+ width: 100%;
+ font-weight: 500;
+ font-size: 34rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 38rpx;
+ }
+
+ .ico {
+ flex-shrink: 0;
+ width: 40rpx;
+ margin-right: 10rpx;
+ }
+ }
+ }
+
+ .searching_text {
+ text-align: center;
+ background: url('/static/img/bg_3.png') no-repeat center top;
+ background-size: 100% auto;
+ box-sizing: border-box;
+ padding-top: 140rpx;
+ margin-top: -100rpx;
+ height: 300rpx;
+
+ .name {
+ font-weight: 500;
+ font-size: 36rpx;
+ color: #000000;
+ line-height: 34rpx;
+ }
+
+ .text {
+ font-size: 26rpx;
+ color: rgba(0, 0, 0, 0.5);
+ line-height: 34rpx;
+ margin-top: 30rpx;
+ }
+ }
+
+ .searching_ico {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-top: 100rpx;
+ }
+
+ .page_bg {
+ width: 100%;
+ background: linear-gradient(3deg, #72E5FF 0%, #3B74EE 98%, #3B74EE 100%);
+ }
+
+ .page_bg1 {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addTemplate/addTemplate copy 1.vue b/ledApp/pages/addTemplate/addTemplate copy 1.vue
new file mode 100644
index 0000000..d90c1a9
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate copy 1.vue
@@ -0,0 +1,548 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="新增模版" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 封面图片
+ </view>
+ <view class="item item_pic">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ :previewFullImage="false" @afterRead="afterRead" @delete="deletePic">
+ </uv-upload>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column" @change="changeSpec">
+ <uv-radio class="spec_radio" v-for="(item, index) in specList" :key="index" :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode">
+ <uv-radio class="spec_radio" v-for="(item, index) in modeList" :key="index" :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="clickGoAnim(index)" shape="circle" size="small"
+ color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+
+ </view>
+
+
+ <!-- <view class="temp_btn" @click="clickGoAnim">
+ 编辑动画
+ </view> -->
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle"
+ @click="clickTempleteAdd()"></uv-button>
+ <uv-button text="测试" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle"
+ style="margin-top: 30rpx;" @click="clickTempleteAdd2()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+
+ </view>
+</template>
+
+<script>
+ import {
+ templeteAdd
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1
+ }
+ },
+ onLoad() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ },
+ onShow() {
+ console.log('------onShow')
+ //const fpsParams = uni.getStorageSync('fpsParams');
+ // if (fpsParams) {
+ // console.log('收到回传参数:', fpsParams);
+ // this.fpsData[fpsParams.fpsIndex] = fpsParams.data;
+ // uni.removeStorageSync('fpsParams');
+ // }
+ const that = this
+ uni.getStorage({
+ key: 'fpsParams',
+ success: (res) => {
+ console.log('获取成功:', res.data);
+ that.fpsData[res.data.fpsIndex] = res.data.data;
+ },
+ fail: (err) => {
+ console.error('获取失败:', err);
+ }
+ });
+ },
+ methods: {
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 24) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+ clickTempleteAdd2() {
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+
+ templeteAdd({
+ name: '',
+ userId: uni.getStorageSync('userId'),
+ screen: '', //屏幕
+ fpsTotal: '', //总帧数
+ speed: '', //速度
+ pic: '',
+ fpsData: ''
+ }).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ uni.navigateBack({
+ delta: 1
+ });
+ })
+
+ },
+ clickTempleteAdd() {
+ if (!this.tempName) {
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fileList1.length == 0) {
+ uni.showToast({
+ title: '请上传封面图片',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fpsData.length == 0) {
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ //console.log('fpsData------------', this.fpsData)
+ // 循环校验每一帧是否完整
+ // let missingIndex = -1;
+ // for (let i = 0; i < this.total; i++) {
+ // if (this.fpsData[i] === null || this.fpsData[i] === undefined) {
+ // missingIndex = i;
+ // break;
+ // }
+ // }
+ // if (missingIndex !== -1) {
+ // uni.showToast({
+ // title: '请编辑第' + (missingIndex + 1) + '帧',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // return;
+ // }
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+ // 动画方式
+ if (this.modeVal == 0) { //一帧动画
+ let frames = this.generateFrames(this.fpsData[0], 24);
+ const parsedData = frames.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = 24
+ } else if (this.modeVal == 1) {
+ const parsedData = this.fpsData.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = this.total
+ }
+
+ // setTimeout(() => {
+
+ // }, 1000)
+ const addData = {
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData)
+ }
+ console.log('addData-----', addData)
+ templeteAdd(addData).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+
+ },
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ // this.fpsData = []
+ // for (let i = 0; i < this.total; i++) {
+ // this.fpsData.push(null);
+ // }
+ this.adjustFpsData()
+ console.log('fpsData------------', this.fpsData)
+ },
+ changeMode(e) {
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ clickGoAnim(index) {
+ uni.navigateTo({
+ url: '/pages/animation/animation?fps=' + index
+ })
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data).fileName)
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .footer_none {
+ height: 200rpx;
+ }
+
+ .footer_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ }
+
+ .temp_btn {
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient(142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ }
+
+ .spec_radio {
+ margin-top: 30rpx;
+
+ &:first-child {
+ margin-top: 10rpx;
+ }
+ }
+
+ .temp {
+ padding: 0 30rpx;
+
+ .temp_item1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+
+ }
+
+ .item {}
+ }
+
+ .temp_item {
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+ }
+
+ .item {
+ margin-top: 20rpx;
+
+ &.item_pic {
+ margin-top: 25rpx;
+ }
+ }
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addTemplate/addTemplate copy 2.vue b/ledApp/pages/addTemplate/addTemplate copy 2.vue
new file mode 100644
index 0000000..d90c1a9
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate copy 2.vue
@@ -0,0 +1,548 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="新增模版" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 封面图片
+ </view>
+ <view class="item item_pic">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ :previewFullImage="false" @afterRead="afterRead" @delete="deletePic">
+ </uv-upload>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column" @change="changeSpec">
+ <uv-radio class="spec_radio" v-for="(item, index) in specList" :key="index" :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode">
+ <uv-radio class="spec_radio" v-for="(item, index) in modeList" :key="index" :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="clickGoAnim(index)" shape="circle" size="small"
+ color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+
+ </view>
+
+
+ <!-- <view class="temp_btn" @click="clickGoAnim">
+ 编辑动画
+ </view> -->
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle"
+ @click="clickTempleteAdd()"></uv-button>
+ <uv-button text="测试" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle"
+ style="margin-top: 30rpx;" @click="clickTempleteAdd2()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+
+ </view>
+</template>
+
+<script>
+ import {
+ templeteAdd
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1
+ }
+ },
+ onLoad() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ },
+ onShow() {
+ console.log('------onShow')
+ //const fpsParams = uni.getStorageSync('fpsParams');
+ // if (fpsParams) {
+ // console.log('收到回传参数:', fpsParams);
+ // this.fpsData[fpsParams.fpsIndex] = fpsParams.data;
+ // uni.removeStorageSync('fpsParams');
+ // }
+ const that = this
+ uni.getStorage({
+ key: 'fpsParams',
+ success: (res) => {
+ console.log('获取成功:', res.data);
+ that.fpsData[res.data.fpsIndex] = res.data.data;
+ },
+ fail: (err) => {
+ console.error('获取失败:', err);
+ }
+ });
+ },
+ methods: {
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 24) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+ clickTempleteAdd2() {
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+
+ templeteAdd({
+ name: '',
+ userId: uni.getStorageSync('userId'),
+ screen: '', //屏幕
+ fpsTotal: '', //总帧数
+ speed: '', //速度
+ pic: '',
+ fpsData: ''
+ }).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ uni.navigateBack({
+ delta: 1
+ });
+ })
+
+ },
+ clickTempleteAdd() {
+ if (!this.tempName) {
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fileList1.length == 0) {
+ uni.showToast({
+ title: '请上传封面图片',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fpsData.length == 0) {
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ //console.log('fpsData------------', this.fpsData)
+ // 循环校验每一帧是否完整
+ // let missingIndex = -1;
+ // for (let i = 0; i < this.total; i++) {
+ // if (this.fpsData[i] === null || this.fpsData[i] === undefined) {
+ // missingIndex = i;
+ // break;
+ // }
+ // }
+ // if (missingIndex !== -1) {
+ // uni.showToast({
+ // title: '请编辑第' + (missingIndex + 1) + '帧',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // return;
+ // }
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+ // 动画方式
+ if (this.modeVal == 0) { //一帧动画
+ let frames = this.generateFrames(this.fpsData[0], 24);
+ const parsedData = frames.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = 24
+ } else if (this.modeVal == 1) {
+ const parsedData = this.fpsData.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = this.total
+ }
+
+ // setTimeout(() => {
+
+ // }, 1000)
+ const addData = {
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData)
+ }
+ console.log('addData-----', addData)
+ templeteAdd(addData).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+
+ },
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ // this.fpsData = []
+ // for (let i = 0; i < this.total; i++) {
+ // this.fpsData.push(null);
+ // }
+ this.adjustFpsData()
+ console.log('fpsData------------', this.fpsData)
+ },
+ changeMode(e) {
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ clickGoAnim(index) {
+ uni.navigateTo({
+ url: '/pages/animation/animation?fps=' + index
+ })
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data).fileName)
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .footer_none {
+ height: 200rpx;
+ }
+
+ .footer_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ }
+
+ .temp_btn {
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient(142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ }
+
+ .spec_radio {
+ margin-top: 30rpx;
+
+ &:first-child {
+ margin-top: 10rpx;
+ }
+ }
+
+ .temp {
+ padding: 0 30rpx;
+
+ .temp_item1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+
+ }
+
+ .item {}
+ }
+
+ .temp_item {
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+ }
+
+ .item {
+ margin-top: 20rpx;
+
+ &.item_pic {
+ margin-top: 25rpx;
+ }
+ }
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addTemplate/addTemplate copy 3.vue b/ledApp/pages/addTemplate/addTemplate copy 3.vue
new file mode 100644
index 0000000..dc6ca6d
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate copy 3.vue
@@ -0,0 +1,1056 @@
+<template>
+ <view class="page_bg">
+
+ <uv-navbar :title="titleName" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 封面图片
+ </view>
+ <view class="item item_pic">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ :previewFullImage="false" @afterRead="afterRead" @delete="deletePic">
+ <!-- <image v-if="fileList1.length != 0"
+ :src="baseURL + fileList1[0]?.url"
+ mode="aspectFill"
+ style="width: 200rpx;height: 200rpx;"
+ ></image>
+ <view class="item_pic_btn" v-else>
+ <uv-icon name="camera-fill" color="#ccc" size="60rpx"></uv-icon>
+ </view> -->
+ </uv-upload>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column" @change="changeSpec">
+ <uv-radio class="spec_radio" v-for="(item, index) in specList" :key="index" :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode">
+ <uv-radio class="spec_radio" v-for="(item, index) in modeList" :key="index" :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="popupAnimationOpen(index)" shape="circle" size="small" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+ </view>
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" @click="clickTempleteAdd()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+ <!-- 动画弹出框 -->
+ <uv-popup ref="popupAnimation" mode="center" @change="changeAnimation" :safeAreaInsetTop="false" :safeAreaInsetBottom="false" :zoom="false" duration="0">
+ <view class="animation_pixel">
+ <view class="back_btn" @click="popupAnimationClose">
+ <uv-icon name="arrow-leftward" color="#333" size="30"></uv-icon>
+ </view>
+ <!-- 动画按钮 -->
+ <view class="controls">
+ <uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
+ <uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
+ <uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
+ <uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
+ <uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
+ </view>
+
+ <!-- 文字生成图形 -->
+ <canvas canvas-id="myCanvas" id="myCanvas" class="canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+
+ <view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
+ @touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd"
+ :style="{
+ gridTemplateRows: `repeat(${height}, 1fr)`,
+ gridTemplateColumns: `repeat(${width}, 1fr)`
+ }">
+ <view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
+ <view class="pixel_cell_item" :style="{ backgroundColor: cell }"></view>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 分辨率 -->
+ <uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
+
+ <!-- 颜色 -->
+ <uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
+ <view class="color_list">
+ <view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }" :key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ import {
+ templeteAdd,
+ templeteInfo,
+ templeteEdit
+ } from '@/api/api.js'
+ let ctx = null
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1,
+
+
+
+ // 模版动画
+ pixelData: [],
+ canvasData: [],
+ canvasWidth: 240,
+ canvasHeight: 120,
+ width: 24,
+ height: 12,
+ color: '#FF0000',
+ gridRect: null, // 存储像素网格的位置信息
+ lastCell: null, // 防止重复绘制
+ columnsRatio: [
+ ['12*24', '16*32', '24*48', '32*64', '40*80']
+ ],
+ // columnsRatio: [
+ // ['12*24']
+ // ],
+ colorList: [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFFF00',
+ '#FFA500',
+ '#800080',
+ '#00FFFF',
+ '#FFC0CB'
+ ],
+ fpsIndex: 0,
+ fpsList: [],
+ tId: '',
+ tempInfo:{},
+ titleName: '新增模版'
+
+ }
+ },
+ onLoad(opt) {
+ this.baseURL = uni.$uv.http.config.baseURL
+ if(opt.id){
+ this.tId = opt.id
+ this.getTempleteInfo()
+ this.titleName = '编辑模版'
+ }
+ else{
+ this.titleName = '新增模版'
+ }
+ },
+ onShow() {
+
+ },
+ onHide() {
+ this.$refs.popupAnimation.close();
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ onUnload() {
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ methods: {
+ // 获取编辑详情
+ getTempleteInfo(){
+ templeteInfo(this.tId).then(res=>{
+ console.log('获取编辑详情-------',res)
+ if(res.code == 200){
+ this.tempInfo = res.data
+ this.tempName = this.tempInfo.name
+ this.fileList1.push({
+ url: this.baseURL + this.tempInfo.pic
+ })
+
+ this.specVal = this.tempInfo.screen
+ this.modeVal = this.tempInfo.mode
+
+ this.total = this.tempInfo.cycle
+ this.fpsTotal = this.tempInfo.fpsTotal
+ this.speed = this.tempInfo.speed
+
+ this.fpsList = JSON.parse(this.tempInfo.fpsList)
+
+ }
+ else{
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ })
+ },
+ // 添加模版提交接口
+ clickTempleteAdd() {
+
+ if (!this.tempName) {
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fileList1.length == 0) {
+ uni.showToast({
+ title: '请上传封面图片',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ if (this.fpsList.length == 0) {
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ } else {
+ for (let i = 0; i < this.fpsList.length; i++) {
+ if (this.fpsList[i] === null) {
+ uni.showToast({
+ title: `请编辑第 ${i+1} 个动画帧`,
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ }
+ }
+ console.log('this.fpsList=========',this.fpsList)
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+
+
+ // 遍历二维数组
+ this.fpsData = this.fpsList.map(inner =>
+ inner.map(item => {
+ if (item === "") {
+ return "000000"; // 空的改成000000
+ } else if (item.includes("#")) {
+ return item.replace("#", ""); // 去掉#
+ } else {
+ return item; // 保持原样
+ }
+ })
+ );
+
+ console.log('this.fpsData=========',this.fpsData)
+
+ // 动画方式
+ if (this.modeVal == 0) { //一帧动画
+ let frames = this.generateFrames(this.fpsData[0], 24);
+ const parsedData = frames.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = 24
+ } else if (this.modeVal == 1) {
+ console.log('modeVal--------1')
+ const parsedData = this.fpsData.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ console.log('modeVal--------1',parsedData)
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = this.total
+ }
+ console.log('animationData--------', this.animationData)
+
+ // 判断this.fileList1[0].url里是否有 this.baseURL
+ if (this.fileList1[0].url.includes(this.baseURL)) {
+ this.fileList1[0].url = this.fileList1[0].url.replace(this.baseURL, "");
+ }
+
+ console.log('url----',this.fileList1[0].url);
+
+ // 判断如果有id就编辑
+ if(this.tId){
+ const putData = {
+ id: this.tId,
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('putData-----', putData)
+ templeteEdit(putData).then(res => {
+ console.log('编辑动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+ else{
+ const addData = {
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('addData-----', addData)
+ templeteAdd(addData).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+
+ },
+ // 保存动画
+ clickSaveData() {
+
+ this.fpsList[this.fpsIndex] = this.pixelData;
+
+
+
+
+ this.$refs.popupAnimation.close();
+ },
+ changeColor() {
+ this.$refs.pickerColor.open();
+ },
+ clickColor(item) {
+ console.log('confirmColor---', item);
+ this.color = item;
+ this.$refs.pickerColor.close();
+ },
+ changeRatio() {
+ this.$refs.pickerRatio.open();
+ },
+ // 分辨率
+ confirmRatio(e) {
+ console.log('confirmRatio---', e.value[0]);
+ const ratio = e.value[0].split('*');
+ console.log('ratio---', ratio[0]);
+ this.height = Number(ratio[0]);
+ this.width = Number(ratio[1]);
+ this.canvasWidth = this.width * 10;
+ this.canvasHeight = this.height * 10;
+ // 重新初始化一维数组
+ this.pixelData = new Array(this.height * this.width).fill('')
+ console.log('pixelData---', this.pixelData);
+ },
+ // 预先获取像素网格的位置信息
+ getGridRect(cb) {
+ if (this.gridRect) {
+ cb(this.gridRect)
+ } else {
+ uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
+ this.gridRect = rect
+ cb(rect)
+ }).exec()
+ }
+ },
+ // 计算触摸点对应的格子
+ getCellByTouch(touch, cb) {
+ this.getGridRect(rect => {
+ if (!rect) return
+ const x = touch.clientX - rect.left
+ const y = touch.clientY - rect.top
+ const cellWidth = rect.width / this.width
+ const cellHeight = rect.height / this.height
+ const colIndex = Math.floor(x / cellWidth)
+ const rowIndex = Math.floor(y / cellHeight)
+ if (
+ colIndex >= 0 && colIndex < this.width &&
+ rowIndex >= 0 && rowIndex < this.height
+ ) {
+ cb({
+ rowIndex,
+ colIndex
+ })
+ }
+ })
+ },
+ handleTouchStart(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ })
+ },
+ handleTouchMove(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ // 只在格子变化时才触发
+ if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
+ cell.colIndex) {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ }
+ })
+ },
+ handleTouchEnd() {
+ this.lastCell = null
+ },
+ handleClick(rowIndex, colIndex) {
+ console.log(rowIndex, colIndex)
+ // 计算一维数组的索引
+ const index = rowIndex * this.width + colIndex
+ // 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
+ const currentColor = this.pixelData[index]
+ const newColor = currentColor ? '' : this.color
+ this.$set(this.pixelData, index, newColor)
+ },
+ clear() {
+ ctx && ctx.clearRect(0, 0, 1000, 1000)
+ // 将所有像素点设置为空字符串
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 模版动画弹出框
+ changeAnimation(e){
+ //console.log('打开关闭模版动画弹窗触发---',e)
+ if(e.show){
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('landscape-primary')
+ }
+ }
+ else{
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ }
+ },
+ // 打开模版动画
+ popupAnimationOpen(index){
+ this.$refs.popupAnimation.open();
+ this.fpsIndex = index
+
+ if(this.fpsList[this.fpsIndex] == null){
+ console.log('第一次编辑-----null')
+ this.pixelData = new Array(this.height * this.width).fill('')
+ }else{
+ console.log('已编辑过-----')
+ this.pixelData = this.fpsList[this.fpsIndex]
+ }
+ },
+ // 关闭模版动画
+ popupAnimationClose(){
+ this.$refs.popupAnimation.close();
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 24) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ this.adjustFpsData()
+ },
+ changeMode(e) {
+ this.fpsList = []
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsList.length > this.total) {
+ this.fpsList = this.fpsList.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsList.length < this.total) {
+ const missing = this.total - this.fpsList.length;
+ this.fpsList = this.fpsList.concat(new Array(missing).fill(null));
+ }
+
+ // 如果 fpsList 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsList 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data))
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ },
+ // 生成点阵数据
+ generateDotMatrix(char) {
+ return new Promise((resolve, reject) => {
+
+ // 使用 uni-app 的 canvas context
+ ctx = uni.createCanvasContext('myCanvas', this);
+
+ // 设置字体样式
+ ctx.setFontSize(this.canvasHeight * 0.9);
+ ctx.setFillStyle('#000')
+ ctx.setTextAlign('center')
+ ctx.setTextBaseline('middle')
+ // 绘制文字
+ // 计算文字位置
+ // 水平居中
+ const x = this.canvasWidth / 2;
+ // 垂直居中,稍微向上偏移一点
+ const y = this.canvasHeight / 2;
+
+ // 绘制文字
+ ctx.fillText(char, x, y);
+
+ // 绘制到画布
+ ctx.draw(false, () => {
+ // 获取画布数据
+ uni.canvasGetImageData({
+ canvasId: 'myCanvas',
+ x: 0,
+ y: 0,
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ success: (res) => {
+ console.log(res)
+ const imageData = res.data;
+ // 生成一维数组
+ const pixelArray = new Array(this.height * this.width).fill('')
+
+ // 计算每个格子的像素大小
+ const cellWidth = this.canvasWidth / this.width;
+ const cellHeight = this.canvasHeight / this.height;
+
+ // 生成一维数组
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ let blackPixels = 0;
+ let totalPixels = 0;
+
+ // 对每个格子进行采样
+ for (let py = 0; py < cellHeight; py++) {
+ for (let px = 0; px < cellWidth; px++) {
+ const pixelY = Math.floor(y * cellHeight + py);
+ const pixelX = Math.floor(x * cellWidth + px);
+
+ const idx = (pixelY * this.canvasWidth + pixelX) * 4;
+ const r = imageData[idx];
+ const g = imageData[idx + 1];
+ const b = imageData[idx + 2];
+ const a = imageData[idx + 3];
+
+ // 计算亮度
+ const brightness = 0.3 * r + 0.59 * g + 0.11 * b;
+
+ // 如果亮度小于阈值且透明度大于阈值,则认为是黑色
+ if (brightness < 128 && a > 128) {
+ blackPixels++;
+ }
+ totalPixels++;
+ }
+ }
+
+ // 如果黑色像素占比超过阈值,则设置为当前选择的颜色
+ const isBlack = blackPixels / totalPixels > 0.3;
+ const arrayIndex = y * this.width + x;
+ pixelArray[arrayIndex] = isBlack ? this.color : ''; // 改为颜色值或空字符串
+ }
+ }
+
+ resolve(pixelArray);
+ },
+ fail: (err) => {
+ reject(err);
+ }
+ });
+ });
+ });
+ },
+
+ // 修改添加文字方法
+ async addText() {
+ uni.showModal({
+ title: '添加文字',
+ content: '',
+ editable: true,
+ placeholderText: '请输入文字',
+ success: async (res) => {
+ if (res.confirm) {
+ const text = res.content;
+ if (text.length > 0) {
+ try {
+ // 生成点阵数据
+ const dotMatrix = await this.generateDotMatrix(text);
+ console.log('点阵数据:', dotMatrix);
+ this.pixelData = dotMatrix
+ } catch (error) {
+ console.error('生成点阵失败:', error);
+ uni.showToast({
+ title: '生成点阵失败',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: '请输入文字',
+ icon: 'none'
+ });
+ }
+ }
+ }
+ });
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ page {
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ }
+ .item_pic_btn{
+ width: 200rpx;
+ height: 200rpx;
+ background: #eee;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 15rpx;
+ }
+ .color_list {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 30rpx;
+ box-sizing: border-box;
+ padding: 50rpx;
+
+ .color_item {
+ width: 30rpx;
+ height: 30rpx;
+ border-radius: 5rpx;
+ }
+ }
+
+ .controls {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+ display: flex;
+ gap: 20px;
+ z-index: 9;
+ }
+ .animation_pixel{
+ width: 100vw;
+ height: 100vh;
+ position: relative;
+ .back_btn{
+ position: absolute;
+ left: 30px;
+ top: 20px;
+ z-index: 9999;
+ }
+ .canvas {
+ margin: 100rpx auto 0;
+ background: #ccc;
+ position: absolute;
+ z-index: 1;
+ left: -9999px;
+ top: -9999px;
+ }
+ .pixel_grid {
+ position: fixed;
+ left: 0;
+ top: var(--status-bar-height);
+ right: 0;
+ bottom: 0;
+ display: grid;
+ gap: 1px;
+ box-sizing: border-box;
+ background: #eee;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel_cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ background: #fff;
+
+ .pixel_cell_item {
+ width: 50%;
+ height: 50%;
+ aspect-ratio: 1/1;
+ font-size: 10rpx;
+ }
+ }
+ }
+
+ }
+ // -------------
+ .footer_none {
+ height: 200rpx;
+ }
+
+ .footer_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ }
+ .footer_btn2 {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
+ }
+
+ .temp_btn {
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient(142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ }
+
+ .spec_radio {
+ margin-top: 30rpx;
+
+ &:first-child {
+ margin-top: 10rpx;
+ }
+ }
+
+ .temp {
+ padding: 0 30rpx;
+
+ .temp_item1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+
+ }
+
+ .item {}
+ }
+
+ .temp_item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-direction: column;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+ }
+
+ .item {
+ margin-top: 20rpx;
+ width: 100%;
+ &.item_pic {
+ margin-top: 25rpx;
+ }
+ }
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addTemplate/addTemplate copy 4.vue b/ledApp/pages/addTemplate/addTemplate copy 4.vue
new file mode 100644
index 0000000..dc6ca6d
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate copy 4.vue
@@ -0,0 +1,1056 @@
+<template>
+ <view class="page_bg">
+
+ <uv-navbar :title="titleName" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 封面图片
+ </view>
+ <view class="item item_pic">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ :previewFullImage="false" @afterRead="afterRead" @delete="deletePic">
+ <!-- <image v-if="fileList1.length != 0"
+ :src="baseURL + fileList1[0]?.url"
+ mode="aspectFill"
+ style="width: 200rpx;height: 200rpx;"
+ ></image>
+ <view class="item_pic_btn" v-else>
+ <uv-icon name="camera-fill" color="#ccc" size="60rpx"></uv-icon>
+ </view> -->
+ </uv-upload>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column" @change="changeSpec">
+ <uv-radio class="spec_radio" v-for="(item, index) in specList" :key="index" :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode">
+ <uv-radio class="spec_radio" v-for="(item, index) in modeList" :key="index" :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="popupAnimationOpen(index)" shape="circle" size="small" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+ </view>
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" @click="clickTempleteAdd()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+ <!-- 动画弹出框 -->
+ <uv-popup ref="popupAnimation" mode="center" @change="changeAnimation" :safeAreaInsetTop="false" :safeAreaInsetBottom="false" :zoom="false" duration="0">
+ <view class="animation_pixel">
+ <view class="back_btn" @click="popupAnimationClose">
+ <uv-icon name="arrow-leftward" color="#333" size="30"></uv-icon>
+ </view>
+ <!-- 动画按钮 -->
+ <view class="controls">
+ <uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
+ <uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
+ <uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
+ <uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
+ <uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
+ </view>
+
+ <!-- 文字生成图形 -->
+ <canvas canvas-id="myCanvas" id="myCanvas" class="canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+
+ <view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
+ @touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd"
+ :style="{
+ gridTemplateRows: `repeat(${height}, 1fr)`,
+ gridTemplateColumns: `repeat(${width}, 1fr)`
+ }">
+ <view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
+ <view class="pixel_cell_item" :style="{ backgroundColor: cell }"></view>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 分辨率 -->
+ <uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
+
+ <!-- 颜色 -->
+ <uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
+ <view class="color_list">
+ <view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }" :key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ import {
+ templeteAdd,
+ templeteInfo,
+ templeteEdit
+ } from '@/api/api.js'
+ let ctx = null
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1,
+
+
+
+ // 模版动画
+ pixelData: [],
+ canvasData: [],
+ canvasWidth: 240,
+ canvasHeight: 120,
+ width: 24,
+ height: 12,
+ color: '#FF0000',
+ gridRect: null, // 存储像素网格的位置信息
+ lastCell: null, // 防止重复绘制
+ columnsRatio: [
+ ['12*24', '16*32', '24*48', '32*64', '40*80']
+ ],
+ // columnsRatio: [
+ // ['12*24']
+ // ],
+ colorList: [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFFF00',
+ '#FFA500',
+ '#800080',
+ '#00FFFF',
+ '#FFC0CB'
+ ],
+ fpsIndex: 0,
+ fpsList: [],
+ tId: '',
+ tempInfo:{},
+ titleName: '新增模版'
+
+ }
+ },
+ onLoad(opt) {
+ this.baseURL = uni.$uv.http.config.baseURL
+ if(opt.id){
+ this.tId = opt.id
+ this.getTempleteInfo()
+ this.titleName = '编辑模版'
+ }
+ else{
+ this.titleName = '新增模版'
+ }
+ },
+ onShow() {
+
+ },
+ onHide() {
+ this.$refs.popupAnimation.close();
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ onUnload() {
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ methods: {
+ // 获取编辑详情
+ getTempleteInfo(){
+ templeteInfo(this.tId).then(res=>{
+ console.log('获取编辑详情-------',res)
+ if(res.code == 200){
+ this.tempInfo = res.data
+ this.tempName = this.tempInfo.name
+ this.fileList1.push({
+ url: this.baseURL + this.tempInfo.pic
+ })
+
+ this.specVal = this.tempInfo.screen
+ this.modeVal = this.tempInfo.mode
+
+ this.total = this.tempInfo.cycle
+ this.fpsTotal = this.tempInfo.fpsTotal
+ this.speed = this.tempInfo.speed
+
+ this.fpsList = JSON.parse(this.tempInfo.fpsList)
+
+ }
+ else{
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ })
+ },
+ // 添加模版提交接口
+ clickTempleteAdd() {
+
+ if (!this.tempName) {
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fileList1.length == 0) {
+ uni.showToast({
+ title: '请上传封面图片',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ if (this.fpsList.length == 0) {
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ } else {
+ for (let i = 0; i < this.fpsList.length; i++) {
+ if (this.fpsList[i] === null) {
+ uni.showToast({
+ title: `请编辑第 ${i+1} 个动画帧`,
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ }
+ }
+ console.log('this.fpsList=========',this.fpsList)
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+
+
+ // 遍历二维数组
+ this.fpsData = this.fpsList.map(inner =>
+ inner.map(item => {
+ if (item === "") {
+ return "000000"; // 空的改成000000
+ } else if (item.includes("#")) {
+ return item.replace("#", ""); // 去掉#
+ } else {
+ return item; // 保持原样
+ }
+ })
+ );
+
+ console.log('this.fpsData=========',this.fpsData)
+
+ // 动画方式
+ if (this.modeVal == 0) { //一帧动画
+ let frames = this.generateFrames(this.fpsData[0], 24);
+ const parsedData = frames.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = 24
+ } else if (this.modeVal == 1) {
+ console.log('modeVal--------1')
+ const parsedData = this.fpsData.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ console.log('modeVal--------1',parsedData)
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = this.total
+ }
+ console.log('animationData--------', this.animationData)
+
+ // 判断this.fileList1[0].url里是否有 this.baseURL
+ if (this.fileList1[0].url.includes(this.baseURL)) {
+ this.fileList1[0].url = this.fileList1[0].url.replace(this.baseURL, "");
+ }
+
+ console.log('url----',this.fileList1[0].url);
+
+ // 判断如果有id就编辑
+ if(this.tId){
+ const putData = {
+ id: this.tId,
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('putData-----', putData)
+ templeteEdit(putData).then(res => {
+ console.log('编辑动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+ else{
+ const addData = {
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('addData-----', addData)
+ templeteAdd(addData).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+
+ },
+ // 保存动画
+ clickSaveData() {
+
+ this.fpsList[this.fpsIndex] = this.pixelData;
+
+
+
+
+ this.$refs.popupAnimation.close();
+ },
+ changeColor() {
+ this.$refs.pickerColor.open();
+ },
+ clickColor(item) {
+ console.log('confirmColor---', item);
+ this.color = item;
+ this.$refs.pickerColor.close();
+ },
+ changeRatio() {
+ this.$refs.pickerRatio.open();
+ },
+ // 分辨率
+ confirmRatio(e) {
+ console.log('confirmRatio---', e.value[0]);
+ const ratio = e.value[0].split('*');
+ console.log('ratio---', ratio[0]);
+ this.height = Number(ratio[0]);
+ this.width = Number(ratio[1]);
+ this.canvasWidth = this.width * 10;
+ this.canvasHeight = this.height * 10;
+ // 重新初始化一维数组
+ this.pixelData = new Array(this.height * this.width).fill('')
+ console.log('pixelData---', this.pixelData);
+ },
+ // 预先获取像素网格的位置信息
+ getGridRect(cb) {
+ if (this.gridRect) {
+ cb(this.gridRect)
+ } else {
+ uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
+ this.gridRect = rect
+ cb(rect)
+ }).exec()
+ }
+ },
+ // 计算触摸点对应的格子
+ getCellByTouch(touch, cb) {
+ this.getGridRect(rect => {
+ if (!rect) return
+ const x = touch.clientX - rect.left
+ const y = touch.clientY - rect.top
+ const cellWidth = rect.width / this.width
+ const cellHeight = rect.height / this.height
+ const colIndex = Math.floor(x / cellWidth)
+ const rowIndex = Math.floor(y / cellHeight)
+ if (
+ colIndex >= 0 && colIndex < this.width &&
+ rowIndex >= 0 && rowIndex < this.height
+ ) {
+ cb({
+ rowIndex,
+ colIndex
+ })
+ }
+ })
+ },
+ handleTouchStart(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ })
+ },
+ handleTouchMove(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ // 只在格子变化时才触发
+ if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
+ cell.colIndex) {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ }
+ })
+ },
+ handleTouchEnd() {
+ this.lastCell = null
+ },
+ handleClick(rowIndex, colIndex) {
+ console.log(rowIndex, colIndex)
+ // 计算一维数组的索引
+ const index = rowIndex * this.width + colIndex
+ // 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
+ const currentColor = this.pixelData[index]
+ const newColor = currentColor ? '' : this.color
+ this.$set(this.pixelData, index, newColor)
+ },
+ clear() {
+ ctx && ctx.clearRect(0, 0, 1000, 1000)
+ // 将所有像素点设置为空字符串
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 模版动画弹出框
+ changeAnimation(e){
+ //console.log('打开关闭模版动画弹窗触发---',e)
+ if(e.show){
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('landscape-primary')
+ }
+ }
+ else{
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ }
+ },
+ // 打开模版动画
+ popupAnimationOpen(index){
+ this.$refs.popupAnimation.open();
+ this.fpsIndex = index
+
+ if(this.fpsList[this.fpsIndex] == null){
+ console.log('第一次编辑-----null')
+ this.pixelData = new Array(this.height * this.width).fill('')
+ }else{
+ console.log('已编辑过-----')
+ this.pixelData = this.fpsList[this.fpsIndex]
+ }
+ },
+ // 关闭模版动画
+ popupAnimationClose(){
+ this.$refs.popupAnimation.close();
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 24) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ this.adjustFpsData()
+ },
+ changeMode(e) {
+ this.fpsList = []
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsList.length > this.total) {
+ this.fpsList = this.fpsList.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsList.length < this.total) {
+ const missing = this.total - this.fpsList.length;
+ this.fpsList = this.fpsList.concat(new Array(missing).fill(null));
+ }
+
+ // 如果 fpsList 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsList 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data))
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ },
+ // 生成点阵数据
+ generateDotMatrix(char) {
+ return new Promise((resolve, reject) => {
+
+ // 使用 uni-app 的 canvas context
+ ctx = uni.createCanvasContext('myCanvas', this);
+
+ // 设置字体样式
+ ctx.setFontSize(this.canvasHeight * 0.9);
+ ctx.setFillStyle('#000')
+ ctx.setTextAlign('center')
+ ctx.setTextBaseline('middle')
+ // 绘制文字
+ // 计算文字位置
+ // 水平居中
+ const x = this.canvasWidth / 2;
+ // 垂直居中,稍微向上偏移一点
+ const y = this.canvasHeight / 2;
+
+ // 绘制文字
+ ctx.fillText(char, x, y);
+
+ // 绘制到画布
+ ctx.draw(false, () => {
+ // 获取画布数据
+ uni.canvasGetImageData({
+ canvasId: 'myCanvas',
+ x: 0,
+ y: 0,
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ success: (res) => {
+ console.log(res)
+ const imageData = res.data;
+ // 生成一维数组
+ const pixelArray = new Array(this.height * this.width).fill('')
+
+ // 计算每个格子的像素大小
+ const cellWidth = this.canvasWidth / this.width;
+ const cellHeight = this.canvasHeight / this.height;
+
+ // 生成一维数组
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ let blackPixels = 0;
+ let totalPixels = 0;
+
+ // 对每个格子进行采样
+ for (let py = 0; py < cellHeight; py++) {
+ for (let px = 0; px < cellWidth; px++) {
+ const pixelY = Math.floor(y * cellHeight + py);
+ const pixelX = Math.floor(x * cellWidth + px);
+
+ const idx = (pixelY * this.canvasWidth + pixelX) * 4;
+ const r = imageData[idx];
+ const g = imageData[idx + 1];
+ const b = imageData[idx + 2];
+ const a = imageData[idx + 3];
+
+ // 计算亮度
+ const brightness = 0.3 * r + 0.59 * g + 0.11 * b;
+
+ // 如果亮度小于阈值且透明度大于阈值,则认为是黑色
+ if (brightness < 128 && a > 128) {
+ blackPixels++;
+ }
+ totalPixels++;
+ }
+ }
+
+ // 如果黑色像素占比超过阈值,则设置为当前选择的颜色
+ const isBlack = blackPixels / totalPixels > 0.3;
+ const arrayIndex = y * this.width + x;
+ pixelArray[arrayIndex] = isBlack ? this.color : ''; // 改为颜色值或空字符串
+ }
+ }
+
+ resolve(pixelArray);
+ },
+ fail: (err) => {
+ reject(err);
+ }
+ });
+ });
+ });
+ },
+
+ // 修改添加文字方法
+ async addText() {
+ uni.showModal({
+ title: '添加文字',
+ content: '',
+ editable: true,
+ placeholderText: '请输入文字',
+ success: async (res) => {
+ if (res.confirm) {
+ const text = res.content;
+ if (text.length > 0) {
+ try {
+ // 生成点阵数据
+ const dotMatrix = await this.generateDotMatrix(text);
+ console.log('点阵数据:', dotMatrix);
+ this.pixelData = dotMatrix
+ } catch (error) {
+ console.error('生成点阵失败:', error);
+ uni.showToast({
+ title: '生成点阵失败',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: '请输入文字',
+ icon: 'none'
+ });
+ }
+ }
+ }
+ });
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ page {
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ }
+ .item_pic_btn{
+ width: 200rpx;
+ height: 200rpx;
+ background: #eee;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 15rpx;
+ }
+ .color_list {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 30rpx;
+ box-sizing: border-box;
+ padding: 50rpx;
+
+ .color_item {
+ width: 30rpx;
+ height: 30rpx;
+ border-radius: 5rpx;
+ }
+ }
+
+ .controls {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+ display: flex;
+ gap: 20px;
+ z-index: 9;
+ }
+ .animation_pixel{
+ width: 100vw;
+ height: 100vh;
+ position: relative;
+ .back_btn{
+ position: absolute;
+ left: 30px;
+ top: 20px;
+ z-index: 9999;
+ }
+ .canvas {
+ margin: 100rpx auto 0;
+ background: #ccc;
+ position: absolute;
+ z-index: 1;
+ left: -9999px;
+ top: -9999px;
+ }
+ .pixel_grid {
+ position: fixed;
+ left: 0;
+ top: var(--status-bar-height);
+ right: 0;
+ bottom: 0;
+ display: grid;
+ gap: 1px;
+ box-sizing: border-box;
+ background: #eee;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel_cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ background: #fff;
+
+ .pixel_cell_item {
+ width: 50%;
+ height: 50%;
+ aspect-ratio: 1/1;
+ font-size: 10rpx;
+ }
+ }
+ }
+
+ }
+ // -------------
+ .footer_none {
+ height: 200rpx;
+ }
+
+ .footer_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ }
+ .footer_btn2 {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
+ }
+
+ .temp_btn {
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient(142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ }
+
+ .spec_radio {
+ margin-top: 30rpx;
+
+ &:first-child {
+ margin-top: 10rpx;
+ }
+ }
+
+ .temp {
+ padding: 0 30rpx;
+
+ .temp_item1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+
+ }
+
+ .item {}
+ }
+
+ .temp_item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-direction: column;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+ }
+
+ .item {
+ margin-top: 20rpx;
+ width: 100%;
+ &.item_pic {
+ margin-top: 25rpx;
+ }
+ }
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/addTemplate/addTemplate copy.vue b/ledApp/pages/addTemplate/addTemplate copy.vue
new file mode 100644
index 0000000..5778ff6
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate copy.vue
@@ -0,0 +1,348 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="新增模版" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column">
+ <uv-radio
+ class="spec_radio"
+ v-for="(item, index) in specList"
+ :key="index"
+ :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode">
+ <uv-radio
+ class="spec_radio"
+ v-for="(item, index) in modeList"
+ :key="index"
+ :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="clickGoAnim(item)" shape="circle" size="small" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+
+ </view>
+
+
+ <!-- <view class="temp_btn" @click="clickGoAnim">
+ 编辑动画
+ </view> -->
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" @click="clickTempleteAdd()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+ </view>
+</template>
+
+<script>
+ import { templeteAdd } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ tempName: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ modeList: [
+ {
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2,3,4,5,6,7,8,9]
+ ],
+ total: 1,
+ columnsSpeed: [
+ [10,20,30,40,50,60,70,80,90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1,
+ fpsIds: []
+ }
+ },
+ onLoad() {
+
+ },
+ onShow() {
+ console.log('------onShow')
+ const fpsParams = uni.getStorageSync('fpsParams');
+ // if (fpsParams) {
+ // console.log('收到回传参数:', fpsParams);
+ // this.fpsIndex = fpsParams.fpsIndex+1
+ // this.fpsData[fpsParams.fpsIndex] = fpsParams.data;
+ // this.fpsIds.push(fpsParams.fpsIndex)
+ // console.log(this.fpsIndex, this.fpsData);
+ // uni.removeStorageSync('fpsParams'); // 清理掉
+ // }
+ if (fpsParams) {
+ console.log('收到回传参数:', fpsParams);
+ // 用 splice 代替下标赋值
+ this.fpsIndex = fpsParams.fpsIndex+1
+ this.fpsData.splice(fpsParams.fpsIndex, 1, fpsParams.data);
+
+ if (!this.fpsIds.includes(fpsParams.fpsIndex + 1)) {
+ this.fpsIds.push(fpsParams.fpsIndex + 1);
+ }
+ console.log(this.fpsIndex, this.fpsData);
+ uni.removeStorageSync('fpsParams'); // 清理掉
+ }
+ },
+ methods: {
+ clickTempleteAdd(){
+ if(!this.tempName){
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(this.fpsIds.length == 0){
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ let totalArr = new Array(this.total).fill(0).map((_, i) => i + 1);
+ let diffBA = totalArr.filter(v => !this.fpsIds.includes(v));
+ console.log(totalArr, diffBA)
+ if(diffBA.length != 0){
+ uni.showToast({
+ title: '请编辑第'+ Math.min(...diffBA) +'帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ templeteAdd({
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ fpsTotal: this.total, //总帧数
+ speed: this.speed, //速度
+
+ fpsData: JSON.stringify(this.fpsData)
+ }).then(res=>{
+ console.log('添加动画模版',res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if(res.code == 200){
+ uni.navigateBack({
+ delta: 1
+ });
+ }
+ })
+ },
+ clickSpeed(){
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal(){
+ if(this.total != 1){
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ },
+ changeMode(e){
+ if(e == 0){
+ this.total = 1
+ }
+ else{
+ this.total = 2
+ }
+ this.fpsIndex = 1
+ this.fpsData = []
+ },
+ clickGoAnim(index){
+ console.log(this.fpsIndex, index)
+ // if(this.fpsIndex > index){
+ // uni.showToast({
+ // title: '请先编辑第'+this.fpsIndex+'帧',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // return
+ // }
+ uni.navigateTo({
+ url: '/pages/animation/animation?fps='+index
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.footer_none{
+ height: 200rpx;
+}
+.footer_btn{
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding:30rpx 40rpx;
+}
+.temp_btn{
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+}
+.spec_radio{
+ margin-top: 30rpx;
+ &:first-child{
+ margin-top: 10rpx;
+ }
+}
+.temp{
+ padding:0 30rpx;
+ .temp_item1{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient( 180deg, #FFFFFF 0%, rgba(255,255,255,0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67,147,248,0.1), -8rpx -8rpx 23rpx 0rpx rgba(255,255,255,0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding:30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+ .title{
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 40rpx;
+
+ }
+ .item{
+
+ }
+ }
+ .temp_item{
+ background: linear-gradient( 180deg, #FFFFFF 0%, rgba(255,255,255,0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67,147,248,0.1), -8rpx -8rpx 23rpx 0rpx rgba(255,255,255,0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding:30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+ .title{
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 40rpx;
+ }
+ .item{
+ margin-top: 15rpx;
+ }
+ }
+}
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/pages/addTemplate/addTemplate.vue b/ledApp/pages/addTemplate/addTemplate.vue
new file mode 100644
index 0000000..0a208de
--- /dev/null
+++ b/ledApp/pages/addTemplate/addTemplate.vue
@@ -0,0 +1,1062 @@
+<template>
+ <view class="page_bg">
+
+ <uv-navbar :title="titleName" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp">
+ <view class="temp_item">
+ <view class="title">
+ 模版名称
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入名称" border="none" v-model="tempName" fontSize="27rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 封面图片
+ </view>
+ <view class="item item_pic">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ :previewFullImage="false" @afterRead="afterRead" @delete="deletePic">
+ <!-- <image v-if="fileList1.length != 0"
+ :src="baseURL + fileList1[0]?.url"
+ mode="aspectFill"
+ style="width: 200rpx;height: 200rpx;"
+ ></image>
+ <view class="item_pic_btn" v-else>
+ <uv-icon name="camera-fill" color="#ccc" size="60rpx"></uv-icon>
+ </view> -->
+ </uv-upload>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 屏幕规格
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="specVal" placement="column" @change="changeSpec" labelSize="26rpx">
+ <uv-radio class="spec_radio" v-for="(item, index) in specList" :key="index" :label="item.name"
+ :name="item.name">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画方式
+ </view>
+ <view class="item">
+ <uv-radio-group v-model="modeVal" placement="column" @change="changeMode" labelSize="26rpx">
+ <uv-radio class="spec_radio" v-for="(item, index) in modeList" :key="index" :label="item.name"
+ :name="item.val">
+ </uv-radio>
+ </uv-radio-group>
+ </view>
+ </view>
+
+ <view class="temp_item">
+ <view class="title">
+ 动画帧数
+ </view>
+ <view class="item" @click="clickTotal">
+ <uv-input placeholder="请输入" readonly border="none" v-model="total" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <view class="temp_item">
+ <view class="title">
+ 动画速度
+ </view>
+ <view class="item" @click="clickSpeed">
+ <uv-input placeholder="请输入" readonly border="none" v-model="speed" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx"
+ customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view>
+ <!-- <view class="temp_item" v-if="modeVal == 1">
+ <view class="title">
+ 循环次数
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入" readonly border="none" v-model="cycle" suffixIcon="arrow-right"
+ suffixIconStyle="color: #bbb" fontSize="26rpx" customStyle="background: #ECEFF6;padding:18rpx 20rpx;border-radius: 10rpx;"></uv-input>
+ </view>
+ </view> -->
+ <view class="temp_item1" v-for="(item,index) in total" :key="index">
+ <view class="title">
+ 动画第{{item}}帧
+ </view>
+ <view class="item">
+ <uv-button text="编辑" @click="popupAnimationOpen(index)" shape="circle" size="small" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)"></uv-button>
+ </view>
+ </view>
+ </view>
+
+ <view class="footer_none"></view>
+ <view class="footer_btn">
+ <uv-button text="保存" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" @click="clickTempleteAdd()"></uv-button>
+ </view>
+
+ <!-- 动画帧数 -->
+ <uv-picker ref="pickerTotal" :columns="columnsTotal" @confirm="confirmTotal"></uv-picker>
+
+ <!-- 动画速度 -->
+ <uv-picker ref="pickerSpeed" :columns="columnsSpeed" @confirm="confirmSpeed"></uv-picker>
+
+ <!-- 动画弹出框 -->
+ <uv-popup ref="popupAnimation" mode="center" @change="changeAnimation" :safeAreaInsetTop="false" :safeAreaInsetBottom="false" :zoom="false" duration="0">
+ <view class="animation_pixel">
+ <view class="back_btn" @click="popupAnimationClose">
+ <uv-icon name="arrow-leftward" color="#333" size="30"></uv-icon>
+ </view>
+ <!-- 动画按钮 -->
+ <view class="controls">
+ <uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
+ <uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
+ <uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
+ <uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
+ <uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
+ </view>
+
+ <!-- 文字生成图形 -->
+ <canvas canvas-id="myCanvas" id="myCanvas" class="canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+
+ <view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
+ @touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd"
+ :style="{
+ gridTemplateRows: `repeat(${height}, 1fr)`,
+ gridTemplateColumns: `repeat(${width}, 1fr)`
+ }">
+ <view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
+ <view class="pixel_cell_item" :style="{ backgroundColor: cell }"></view>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 分辨率 -->
+ <uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
+
+ <!-- 颜色 -->
+ <uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
+ <view class="color_list">
+ <view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }" :key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ import {
+ templeteAdd,
+ templeteInfo,
+ templeteEdit
+ } from '@/api/api.js'
+ let ctx = null
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1,
+
+
+
+ // 模版动画
+ pixelData: [],
+ canvasData: [],
+ canvasWidth: 240,
+ canvasHeight: 120,
+ width: 24,
+ height: 12,
+ color: '#FF0000',
+ gridRect: null, // 存储像素网格的位置信息
+ lastCell: null, // 防止重复绘制
+ columnsRatio: [
+ ['12*24', '16*32', '24*48', '32*64', '40*80']
+ ],
+ // columnsRatio: [
+ // ['12*24']
+ // ],
+ colorList: [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFFF00',
+ '#FFA500',
+ '#800080',
+ '#00FFFF',
+ '#FFC0CB'
+ ],
+ fpsIndex: 0,
+ fpsList: [],
+ tId: '',
+ tempInfo:{},
+ titleName: '新增模版'
+
+ }
+ },
+ onLoad(opt) {
+ this.baseURL = uni.$uv.http.config.baseURL
+ if(opt.id){
+ this.tId = opt.id
+ this.getTempleteInfo()
+ this.titleName = '编辑模版'
+ }
+ else{
+ this.titleName = '新增模版'
+ }
+ },
+ onShow() {
+
+ },
+ onHide() {
+ this.$refs.popupAnimation.close();
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ onUnload() {
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ methods: {
+ // 跳转到编辑动画页面
+ goToAnimaiton(index){
+ this.fpsIndex = index
+ uni.navigateTo({
+ url: '/pages/animation/animation?index='+index
+ })
+ },
+ // 获取编辑详情
+ getTempleteInfo(){
+ templeteInfo(this.tId).then(res=>{
+ console.log('获取编辑详情-------',res)
+ if(res.code == 200){
+ this.tempInfo = res.data
+ this.tempName = this.tempInfo.name
+ this.fileList1.push({
+ url: this.baseURL + this.tempInfo.pic
+ })
+
+ this.specVal = this.tempInfo.screen
+ this.modeVal = this.tempInfo.mode
+
+ this.total = this.tempInfo.cycle
+ this.fpsTotal = this.tempInfo.fpsTotal
+ this.speed = this.tempInfo.speed
+
+ this.fpsList = JSON.parse(this.tempInfo.fpsList)
+
+ }
+ else{
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ })
+ },
+ // 添加模版提交接口
+ clickTempleteAdd() {
+
+ if (!this.tempName) {
+ uni.showToast({
+ title: '请输入模版名称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (this.fileList1.length == 0) {
+ uni.showToast({
+ title: '请上传封面图片',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ if (this.fpsList.length == 0) {
+ uni.showToast({
+ title: '请编辑动画帧',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ } else {
+ for (let i = 0; i < this.fpsList.length; i++) {
+ if (this.fpsList[i] === null) {
+ uni.showToast({
+ title: `请编辑第 ${i+1} 个动画帧`,
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ }
+ }
+ console.log('this.fpsList=========',this.fpsList)
+
+ uni.showLoading({
+ title: '添加中',
+ mask: true
+ });
+
+
+ // 遍历二维数组
+ this.fpsData = this.fpsList.map(inner =>
+ inner.map(item => {
+ if (item === "") {
+ return "000000"; // 空的改成000000
+ } else if (item.includes("#")) {
+ return item.replace("#", ""); // 去掉#
+ } else {
+ return item; // 保持原样
+ }
+ })
+ );
+
+ console.log('this.fpsData=========',this.fpsData)
+
+ // 动画方式
+ if (this.modeVal == 0) { //一帧动画
+ let frames = this.generateFrames(this.fpsData[0], 24);
+ const parsedData = frames.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = 24
+ } else if (this.modeVal == 1) {
+ console.log('modeVal--------1')
+ const parsedData = this.fpsData.map(item => {
+ return this.toSnakeData(item, this.specWidth)
+ });
+ console.log('modeVal--------1',parsedData)
+ this.animationData = parsedData.map(item => {
+ return this.mergeRowsToString(item)
+ });
+ this.fpsTotal = this.total
+ }
+ console.log('animationData--------', this.animationData)
+
+ // 判断this.fileList1[0].url里是否有 this.baseURL
+ if (this.fileList1[0].url.includes(this.baseURL)) {
+ this.fileList1[0].url = this.fileList1[0].url.replace(this.baseURL, "");
+ }
+
+ console.log('url----',this.fileList1[0].url);
+
+ // 判断如果有id就编辑
+ if(this.tId){
+ const putData = {
+ id: this.tId,
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('putData-----', putData)
+ templeteEdit(putData).then(res => {
+ console.log('编辑动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+ else{
+ const addData = {
+ name: this.tempName,
+ userId: uni.getStorageSync('userId'),
+ screen: this.specVal, //屏幕
+ mode: this.modeVal,
+ cycle: this.total,
+ fpsTotal: this.fpsTotal, //总帧数
+ speed: this.speed, //速度
+ pic: this.fileList1[0].url,
+ fpsData: JSON.stringify(this.animationData),
+ fpsList: JSON.stringify(this.fpsList)
+ }
+ console.log('addData-----', addData)
+ templeteAdd(addData).then(res => {
+ console.log('添加动画模版', res)
+ uni.hideLoading();
+ if (res.code == 200) {
+ uni.navigateBack({
+ delta: 1
+ });
+ } else {
+ uni.showToast({
+ title: res.msg,
+ duration: 1500,
+ icon: 'none',
+ mask: true
+ });
+ }
+ })
+ }
+
+ },
+ // 保存动画
+ clickSaveData() {
+ this.fpsList[this.fpsIndex] = this.pixelData;
+ this.$refs.popupAnimation.close();
+ },
+ changeColor() {
+ this.$refs.pickerColor.open();
+ },
+ clickColor(item) {
+ console.log('confirmColor---', item);
+ this.color = item;
+ this.$refs.pickerColor.close();
+ },
+ changeRatio() {
+ this.$refs.pickerRatio.open();
+ },
+ // 分辨率
+ confirmRatio(e) {
+ console.log('confirmRatio---', e.value[0]);
+ const ratio = e.value[0].split('*');
+ console.log('ratio---', ratio[0]);
+ this.height = Number(ratio[0]);
+ this.width = Number(ratio[1]);
+ this.canvasWidth = this.width * 10;
+ this.canvasHeight = this.height * 10;
+ // 重新初始化一维数组
+ this.pixelData = new Array(this.height * this.width).fill('')
+ console.log('pixelData---', this.pixelData);
+ },
+ // 预先获取像素网格的位置信息
+ getGridRect(cb) {
+ if (this.gridRect) {
+ cb(this.gridRect)
+ } else {
+ uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
+ this.gridRect = rect
+ cb(rect)
+ }).exec()
+ }
+ },
+ // 计算触摸点对应的格子
+ getCellByTouch(touch, cb) {
+ this.getGridRect(rect => {
+ if (!rect) return
+ const x = touch.clientX - rect.left
+ const y = touch.clientY - rect.top
+ const cellWidth = rect.width / this.width
+ const cellHeight = rect.height / this.height
+ const colIndex = Math.floor(x / cellWidth)
+ const rowIndex = Math.floor(y / cellHeight)
+ if (
+ colIndex >= 0 && colIndex < this.width &&
+ rowIndex >= 0 && rowIndex < this.height
+ ) {
+ cb({
+ rowIndex,
+ colIndex
+ })
+ }
+ })
+ },
+ handleTouchStart(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ })
+ },
+ handleTouchMove(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ // 只在格子变化时才触发
+ if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
+ cell.colIndex) {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ }
+ })
+ },
+ handleTouchEnd() {
+ this.lastCell = null
+ },
+ handleClick(rowIndex, colIndex) {
+ console.log(rowIndex, colIndex)
+ // 计算一维数组的索引
+ const index = rowIndex * this.width + colIndex
+ // 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
+ const currentColor = this.pixelData[index]
+ const newColor = currentColor ? '' : this.color
+ this.$set(this.pixelData, index, newColor)
+ },
+ clear() {
+ ctx && ctx.clearRect(0, 0, 1000, 1000)
+ // 将所有像素点设置为空字符串
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 模版动画弹出框
+ changeAnimation(e){
+ //console.log('打开关闭模版动画弹窗触发---',e)
+ // ios设备不支持弹出框横屏切换
+ if(e.show){
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('landscape-primary')
+ }
+ }
+ else{
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait-primary')
+ }
+ }
+ },
+ // 打开模版动画
+ popupAnimationOpen(index){
+ this.$refs.popupAnimation.open();
+ this.fpsIndex = index
+
+ if(this.fpsList[this.fpsIndex] == null){
+ console.log('第一次编辑-----null')
+ this.pixelData = new Array(this.height * this.width).fill('')
+ }else{
+ console.log('已编辑过-----')
+ this.pixelData = this.fpsList[this.fpsIndex]
+ }
+ },
+ // 关闭模版动画
+ popupAnimationClose(){
+ this.$refs.popupAnimation.close();
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 24) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ this.adjustFpsData()
+ },
+ changeMode(e) {
+ this.fpsList = []
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsList.length > this.total) {
+ this.fpsList = this.fpsList.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsList.length < this.total) {
+ const missing = this.total - this.fpsList.length;
+ this.fpsList = this.fpsList.concat(new Array(missing).fill(null));
+ }
+
+ // 如果 fpsList 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsList 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data))
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ },
+ // 生成点阵数据
+ generateDotMatrix(char) {
+ return new Promise((resolve, reject) => {
+
+ // 使用 uni-app 的 canvas context
+ ctx = uni.createCanvasContext('myCanvas', this);
+
+ // 设置字体样式
+ ctx.setFontSize(this.canvasHeight * 0.9);
+ ctx.setFillStyle('#000')
+ ctx.setTextAlign('center')
+ ctx.setTextBaseline('middle')
+ // 绘制文字
+ // 计算文字位置
+ // 水平居中
+ const x = this.canvasWidth / 2;
+ // 垂直居中,稍微向上偏移一点
+ const y = this.canvasHeight / 2;
+
+ // 绘制文字
+ ctx.fillText(char, x, y);
+
+ // 绘制到画布
+ ctx.draw(false, () => {
+ // 获取画布数据
+ uni.canvasGetImageData({
+ canvasId: 'myCanvas',
+ x: 0,
+ y: 0,
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ success: (res) => {
+ console.log(res)
+ const imageData = res.data;
+ // 生成一维数组
+ const pixelArray = new Array(this.height * this.width).fill('')
+
+ // 计算每个格子的像素大小
+ const cellWidth = this.canvasWidth / this.width;
+ const cellHeight = this.canvasHeight / this.height;
+
+ // 生成一维数组
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ let blackPixels = 0;
+ let totalPixels = 0;
+
+ // 对每个格子进行采样
+ for (let py = 0; py < cellHeight; py++) {
+ for (let px = 0; px < cellWidth; px++) {
+ const pixelY = Math.floor(y * cellHeight + py);
+ const pixelX = Math.floor(x * cellWidth + px);
+
+ const idx = (pixelY * this.canvasWidth + pixelX) * 4;
+ const r = imageData[idx];
+ const g = imageData[idx + 1];
+ const b = imageData[idx + 2];
+ const a = imageData[idx + 3];
+
+ // 计算亮度
+ const brightness = 0.3 * r + 0.59 * g + 0.11 * b;
+
+ // 如果亮度小于阈值且透明度大于阈值,则认为是黑色
+ if (brightness < 128 && a > 128) {
+ blackPixels++;
+ }
+ totalPixels++;
+ }
+ }
+
+ // 如果黑色像素占比超过阈值,则设置为当前选择的颜色
+ const isBlack = blackPixels / totalPixels > 0.3;
+ const arrayIndex = y * this.width + x;
+ pixelArray[arrayIndex] = isBlack ? this.color : ''; // 改为颜色值或空字符串
+ }
+ }
+
+ resolve(pixelArray);
+ },
+ fail: (err) => {
+ reject(err);
+ }
+ });
+ });
+ });
+ },
+
+ // 修改添加文字方法
+ async addText() {
+ uni.showModal({
+ title: '添加文字',
+ content: '',
+ editable: true,
+ placeholderText: '请输入文字',
+ success: async (res) => {
+ if (res.confirm) {
+ const text = res.content;
+ if (text.length > 0) {
+ try {
+ // 生成点阵数据
+ const dotMatrix = await this.generateDotMatrix(text);
+ console.log('点阵数据:', dotMatrix);
+ this.pixelData = dotMatrix
+ } catch (error) {
+ console.error('生成点阵失败:', error);
+ uni.showToast({
+ title: '生成点阵失败',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: '请输入文字',
+ icon: 'none'
+ });
+ }
+ }
+ }
+ });
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ page {
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ }
+ .item_pic_btn{
+ width: 200rpx;
+ height: 200rpx;
+ background: #eee;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 15rpx;
+ }
+ .color_list {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 30rpx;
+ box-sizing: border-box;
+ padding: 50rpx;
+
+ .color_item {
+ width: 30rpx;
+ height: 30rpx;
+ border-radius: 5rpx;
+ }
+ }
+
+ .controls {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+ display: flex;
+ gap: 20px;
+ z-index: 9;
+ }
+ .animation_pixel{
+ width: 100vw;
+ height: 100vh;
+ position: relative;
+ .back_btn{
+ position: absolute;
+ left: 30px;
+ top: 20px;
+ z-index: 9999;
+ }
+ .canvas {
+ margin: 100rpx auto 0;
+ background: #ccc;
+ position: absolute;
+ z-index: 1;
+ left: -9999px;
+ top: -9999px;
+ }
+ .pixel_grid {
+ position: fixed;
+ left: 0;
+ top: var(--status-bar-height);
+ right: 0;
+ bottom: 0;
+ display: grid;
+ gap: 1px;
+ box-sizing: border-box;
+ background: #eee;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel_cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ background: #fff;
+
+ .pixel_cell_item {
+ width: 50%;
+ height: 50%;
+ aspect-ratio: 1/1;
+ font-size: 10rpx;
+ }
+ }
+ }
+
+ }
+ // -------------
+ .footer_none {
+ height: 200rpx;
+ }
+
+ .footer_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ }
+ .footer_btn2 {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 30rpx 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
+ }
+
+ .temp_btn {
+ position: fixed;
+ left: 30rpx;
+ bottom: 60rpx;
+ right: 30rpx;
+ box-sizing: border-box;
+ height: 96rpx;
+ background: linear-gradient(142deg, #6FD2FF 0%, #268DFF 100%);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ }
+
+ .spec_radio {
+ margin-top: 30rpx;
+
+ &:first-child {
+ margin-top: 10rpx;
+ }
+ }
+
+ .temp {
+ padding: 0 30rpx;
+
+ .temp_item1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+
+ }
+
+ .item {}
+ }
+
+ .temp_item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-direction: column;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 30rpx;
+ box-sizing: border-box;
+ margin-top: 24rpx;
+
+ .title {
+ flex-shrink: 0;
+ font-size: 30rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 40rpx;
+ }
+
+ .item {
+ margin-top: 20rpx;
+ width: 100%;
+ font-size: 30rpx;
+ display: flex;
+ flex-direction: column;
+ &.item_pic {
+ margin-top: 25rpx;
+ }
+ }
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/animation/animation copy.vue b/ledApp/pages/animation/animation copy.vue
new file mode 100644
index 0000000..15ccbff
--- /dev/null
+++ b/ledApp/pages/animation/animation copy.vue
@@ -0,0 +1,533 @@
+<template>
+ <view>
+ <!-- <uv-navbar title="动画编辑" :autoBack="true" bgColor="rgba(0,0,0,0)" :placeholder="false"></uv-navbar> -->
+ <view class="back_btn" @click="clickBack">
+ <uv-icon name="arrow-leftward" color="#333" size="26rpx"></uv-icon>
+ </view>
+ <view class="controls">
+ <uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
+ <uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
+
+ <uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
+ <uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
+
+ <uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
+ </view>
+
+ <!-- 文字生成图形 -->
+ <canvas canvas-id="myCanvas" id="myCanvas" class="canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+
+ <!-- 修改像素网格显示 -->
+ <!-- <view class="pixel-grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove">
+ <view class="pixel-row" v-for="(row, rowIndex) in Math.ceil(pixelData.length / width)" :key="rowIndex">
+ <view class="pixel-cell" v-for="(item, colIndex) in width" :key="colIndex">
+ <view v-if="pixelData[rowIndex * width + colIndex]" class="pixel-cell-item" :style="{ backgroundColor: pixelData[rowIndex * width + colIndex] }">
+ {{rowIndex,colIndex}}
+ </view>
+ </view>
+ </view>
+ </view> -->
+
+ <view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
+ @touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd"
+ :style="{
+ gridTemplateRows: `repeat(${height}, 1fr)`,
+ gridTemplateColumns: `repeat(${width}, 1fr)`
+ }">
+ <view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
+ <view class="pixel_cell_item" :style="{ backgroundColor: cell }">
+
+ </view>
+ </view>
+ </view>
+
+ <!-- 分辨率 -->
+ <uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
+
+ <!-- 颜色 -->
+ <uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
+ <view class="color_list">
+ <view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }"
+ :key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
+ </view>
+ </uv-popup>
+
+ <!-- <uv-pick-color ref="pickerColor" @confirm="confirm"></uv-pick-color> -->
+
+ </view>
+</template>
+
+<script>
+ let ctx = null
+
+ export default {
+ data() {
+ return {
+ pixelData: [],
+ canvasData: [],
+ canvasWidth: 240,
+ canvasHeight: 120,
+ width: 24,
+ height: 12,
+ color: '#FF0000',
+ gridRect: null, // 存储像素网格的位置信息
+ lastCell: null, // 防止重复绘制
+ columnsRatio: [
+ ['12*24', '16*32', '24*48', '32*64', '40*80']
+ ],
+ // columnsRatio: [
+ // ['12*24']
+ // ],
+ colorList: [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFFF00',
+ '#FFA500',
+ '#800080',
+ '#00FFFF',
+ '#FFC0CB'
+ ],
+ fpsIndex: 0
+ }
+ },
+ onLoad(opt) {
+ // App端强制横屏
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('landscape-primary')
+ }
+ if (opt.fps) {
+ this.fpsIndex = opt.fps
+ }
+
+ },
+ onShow() {
+ // 改为一维数组初始化
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ onHide() {
+ console.log('离开页面恢复竖屏------onHide')
+ // 离开页面恢复竖屏
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ onUnload() {
+ console.log('离开页面恢复竖屏------onUnload')
+ // 离开页面恢复竖屏
+ if (typeof plus !== 'undefined') {
+ plus.screen.lockOrientation('portrait')
+ }
+ },
+ methods: {
+ clickBack() {
+ uni.navigateBack({
+ delta: 1
+ });
+ },
+ clickSaveData() {
+ const parsed = this.pixelData.map(item => {
+ if (item) {
+ return item.replace("#", "");
+ } else {
+ return "000000";
+ }
+ });
+ console.log('parsed-------------', parsed.join(''))
+ //const snakeRows = this.toSnakeData(parsed, this.width)
+ //console.log('snakeRows-------------', snakeRows)
+ //const snakeString = this.mergeRowsToString(snakeRows)
+ //console.log('snakeString-------------', snakeString)
+
+ // uni.setStorageSync('fpsParams', {
+ // shouldRefresh: true,
+ // fpsIndex: Number(this.fpsIndex),
+ // //data: snakeString
+ // data: parsed
+ // });
+
+ uni.setStorage({
+ key: 'fpsParams',
+ data: {
+ shouldRefresh: true,
+ fpsIndex: Number(this.fpsIndex),
+ data: parsed
+ },
+ success: () => {
+ console.log('存储成功');
+ uni.navigateBack({
+ delta: 1,
+ });
+ },
+ fail: (err) => {
+ console.error('存储失败:', err);
+ }
+ });
+
+
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ getData() {
+ uni.switchTab({
+ url: '/pages/template/template',
+ success() {
+ console.log('跳转成功');
+ },
+ fail(err) {
+ console.error('跳转失败:', err);
+ }
+ });
+ },
+ changeColor() {
+ this.$refs.pickerColor.open();
+ },
+ clickColor(item) {
+ console.log('confirmColor---', item);
+ this.color = item;
+ this.$refs.pickerColor.close();
+ },
+ changeRatio() {
+ this.$refs.pickerRatio.open();
+ },
+ // 分辨率
+ confirmRatio(e) {
+ console.log('confirmRatio---', e.value[0]);
+ const ratio = e.value[0].split('*');
+ console.log('ratio---', ratio[0]);
+ this.height = Number(ratio[0]);
+ this.width = Number(ratio[1]);
+ this.canvasWidth = this.width * 10;
+ this.canvasHeight = this.height * 10;
+ // 重新初始化一维数组
+ this.pixelData = new Array(this.height * this.width).fill('')
+ console.log('pixelData---', this.pixelData);
+ },
+ // 预先获取像素网格的位置信息
+ getGridRect(cb) {
+ if (this.gridRect) {
+ cb(this.gridRect)
+ } else {
+ uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
+ this.gridRect = rect
+ cb(rect)
+ }).exec()
+ }
+ },
+ // 计算触摸点对应的格子
+ getCellByTouch(touch, cb) {
+ this.getGridRect(rect => {
+ if (!rect) return
+ const x = touch.clientX - rect.left
+ const y = touch.clientY - rect.top
+ const cellWidth = rect.width / this.width
+ const cellHeight = rect.height / this.height
+ const colIndex = Math.floor(x / cellWidth)
+ const rowIndex = Math.floor(y / cellHeight)
+ if (
+ colIndex >= 0 && colIndex < this.width &&
+ rowIndex >= 0 && rowIndex < this.height
+ ) {
+ cb({
+ rowIndex,
+ colIndex
+ })
+ }
+ })
+ },
+ handleTouchStart(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ })
+ },
+ handleTouchMove(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ // 只在格子变化时才触发
+ if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
+ cell.colIndex) {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ }
+ })
+ },
+ handleTouchEnd() {
+ this.lastCell = null
+ },
+ handleClick(rowIndex, colIndex) {
+ console.log(rowIndex, colIndex)
+ // 计算一维数组的索引
+ const index = rowIndex * this.width + colIndex
+ // 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
+ const currentColor = this.pixelData[index]
+ const newColor = currentColor ? '' : this.color
+ this.$set(this.pixelData, index, newColor)
+ },
+ clear() {
+ ctx && ctx.clearRect(0, 0, 1000, 1000)
+ // 将所有像素点设置为空字符串
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 生成点阵数据
+ generateDotMatrix(char) {
+ return new Promise((resolve, reject) => {
+
+ // 使用 uni-app 的 canvas context
+ ctx = uni.createCanvasContext('myCanvas', this);
+
+ // 设置字体样式
+ ctx.setFontSize(this.canvasHeight * 0.9);
+ ctx.setFillStyle('#000')
+ ctx.setTextAlign('center')
+ ctx.setTextBaseline('middle')
+ // 绘制文字
+ // 计算文字位置
+ // 水平居中
+ const x = this.canvasWidth / 2;
+ // 垂直居中,稍微向上偏移一点
+ const y = this.canvasHeight / 2;
+
+ // 绘制文字
+ ctx.fillText(char, x, y);
+
+ // 绘制到画布
+ ctx.draw(false, () => {
+ // 获取画布数据
+ uni.canvasGetImageData({
+ canvasId: 'myCanvas',
+ x: 0,
+ y: 0,
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ success: (res) => {
+ console.log(res)
+ const imageData = res.data;
+ // 生成一维数组
+ const pixelArray = new Array(this.height * this.width).fill('')
+
+ // 计算每个格子的像素大小
+ const cellWidth = this.canvasWidth / this.width;
+ const cellHeight = this.canvasHeight / this.height;
+
+ // 生成一维数组
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ let blackPixels = 0;
+ let totalPixels = 0;
+
+ // 对每个格子进行采样
+ for (let py = 0; py < cellHeight; py++) {
+ for (let px = 0; px < cellWidth; px++) {
+ const pixelY = Math.floor(y * cellHeight + py);
+ const pixelX = Math.floor(x * cellWidth + px);
+
+ const idx = (pixelY * this.canvasWidth +
+ pixelX) * 4;
+ const r = imageData[idx];
+ const g = imageData[idx + 1];
+ const b = imageData[idx + 2];
+ const a = imageData[idx + 3];
+
+ // 计算亮度
+ const brightness = 0.3 * r + 0.59 * g + 0.11 *
+ b;
+
+ // 如果亮度小于阈值且透明度大于阈值,则认为是黑色
+ if (brightness < 128 && a > 128) {
+ blackPixels++;
+ }
+ totalPixels++;
+ }
+ }
+
+ // 如果黑色像素占比超过阈值,则设置为当前选择的颜色
+ const isBlack = blackPixels / totalPixels > 0.3;
+ const arrayIndex = y * this.width + x;
+ pixelArray[arrayIndex] = isBlack ? this.color :
+ ''; // 改为颜色值或空字符串
+ }
+ }
+
+ resolve(pixelArray);
+ },
+ fail: (err) => {
+ reject(err);
+ }
+ });
+ });
+ });
+ },
+
+ // 修改添加文字方法
+ async addText() {
+ uni.showModal({
+ title: '添加文字',
+ content: '',
+ editable: true,
+ placeholderText: '请输入文字',
+ success: async (res) => {
+ if (res.confirm) {
+ const text = res.content;
+ if (text.length > 0) {
+ try {
+ // 生成点阵数据
+ const dotMatrix = await this.generateDotMatrix(text);
+ console.log('点阵数据:', dotMatrix);
+ this.pixelData = dotMatrix
+ } catch (error) {
+ console.error('生成点阵失败:', error);
+ uni.showToast({
+ title: '生成点阵失败',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: '请输入文字',
+ icon: 'none'
+ });
+ }
+ }
+ }
+ });
+ },
+
+
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .back_btn {
+ position: fixed;
+ left: 10rpx;
+ top: 26rpx;
+ z-index: 9999;
+ }
+
+ .pixel_grid {
+ position: fixed;
+ left: 0;
+ top: var(--status-bar-height);
+ right: 0;
+ bottom: 0;
+ display: grid;
+ gap: 1px;
+ box-sizing: border-box;
+ background: #eee;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel_cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ background: #fff;
+
+ .pixel_cell_item {
+ width: 50%;
+ height: 50%;
+ aspect-ratio: 1/1;
+ font-size: 10rpx;
+ }
+ }
+ }
+
+ .color_list {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 30rpx;
+ box-sizing: border-box;
+ padding: 50rpx;
+
+ .color_item {
+ width: 30rpx;
+ height: 30rpx;
+ border-radius: 5rpx;
+ }
+ }
+
+ .controls {
+ position: absolute;
+ bottom: 15rpx;
+ left: 20rpx;
+ display: flex;
+ gap: 20rpx;
+ z-index: 9;
+ }
+
+ .canvas {
+ margin: 100rpx auto 0;
+ background: #ccc;
+ position: absolute;
+ z-index: 1;
+ left: -9999px;
+ top: -9999px;
+ }
+
+ .pixel-grid {
+ position: fixed;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ z-index: 8;
+ background: #fff;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel-row {
+ display: flex;
+ flex: 1;
+
+ .pixel-cell {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1rpx solid #eee;
+ box-sizing: border-box;
+
+ .pixel-cell-item {
+ width: 50%;
+ aspect-ratio: 1/1;
+ color: #333 !important;
+ ;
+ font-size: 10rpx;
+ }
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/animation/animation.vue b/ledApp/pages/animation/animation.vue
new file mode 100644
index 0000000..280e105
--- /dev/null
+++ b/ledApp/pages/animation/animation.vue
@@ -0,0 +1,624 @@
+<template>
+ <view>
+ <view class="animation_pixel">
+ <view class="back_btn" @click="popupAnimationClose">
+ <uv-icon name="arrow-leftward" color="#333" size="30"></uv-icon>
+ </view>
+ <!-- 动画按钮 -->
+ <view class="controls">
+ <uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
+ <uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
+ <uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
+ <uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
+ <uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
+ </view>
+
+ <!-- 文字生成图形 -->
+ <canvas canvas-id="myCanvas" id="myCanvas" class="canvas"
+ :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+
+ <view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
+ @touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd" :style="{
+ gridTemplateRows: `repeat(${height}, 1fr)`,
+ gridTemplateColumns: `repeat(${width}, 1fr)`
+ }">
+ <view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
+ <view class="pixel_cell_item" :style="{ backgroundColor: cell }"></view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 分辨率 -->
+ <uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
+
+ <!-- 颜色 -->
+ <uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
+ <view class="color_list">
+ <view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }"
+ :key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ let ctx = null
+ export default {
+ data() {
+ return {
+ tempName: '',
+ pic: '',
+ specList: [{
+ name: '12*24'
+ }, {
+ name: '16*24'
+ }, {
+ name: '18*24'
+ }],
+ specVal: '12*24',
+ specWidth: 24,
+ specHeight: 12,
+ modeList: [{
+ name: '单个动画帧',
+ val: 0
+ },
+ {
+ name: '动画帧切换',
+ val: 1
+ }
+ ],
+ modeVal: 0,
+ columnsTotal: [
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30
+ ]
+ ],
+ total: 1, //动画总帧数
+ columnsSpeed: [
+ [10, 20, 30, 40, 50, 60, 70, 80, 90]
+ ],
+ speed: 10,
+ cycle: 1,
+ fpsData: [],
+ fpsIndex: 1, //第几帧
+ fpsIds: [],
+
+ baseURL: '',
+ fileList1: [],
+ animationData: [],
+ fpsTotal: 1,
+
+
+
+ // 模版动画
+ pixelData: [],
+ canvasData: [],
+ canvasWidth: 240,
+ canvasHeight: 120,
+ width: 24,
+ height: 12,
+ color: '#FF0000',
+ gridRect: null, // 存储像素网格的位置信息
+ lastCell: null, // 防止重复绘制
+ columnsRatio: [
+ ['12*24', '16*32', '24*48', '32*64', '40*80']
+ ],
+ // columnsRatio: [
+ // ['12*24']
+ // ],
+ colorList: [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFFF00',
+ '#FFA500',
+ '#800080',
+ '#00FFFF',
+ '#FFC0CB'
+ ],
+ fpsIndex: 0,
+ fpsList: [],
+ tId: '',
+ tempInfo: {},
+ titleName: '新增模版',
+
+
+ }
+ },
+ onLoad(opt) {
+ this.baseURL = uni.$uv.http.config.baseURL
+ if (opt.index) {
+ this.fpsIndex = opt.index
+ console.log('fpsIndex--', this.fpsIndex)
+ }
+ },
+ onShow() {
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ onHide() {
+
+ },
+ onUnload() {
+
+ },
+ methods: {
+ // 保存动画
+ clickSaveData() {
+ this.fpsList[this.fpsIndex] = this.pixelData;
+ this.$refs.popupAnimation.close();
+ },
+ changeColor() {
+ this.$refs.pickerColor.open();
+ },
+ clickColor(item) {
+ console.log('confirmColor---', item);
+ this.color = item;
+ this.$refs.pickerColor.close();
+ },
+ changeRatio() {
+ this.$refs.pickerRatio.open();
+ },
+ // 分辨率
+ confirmRatio(e) {
+ console.log('confirmRatio---', e.value[0]);
+ const ratio = e.value[0].split('*');
+ console.log('ratio---', ratio[0]);
+ this.height = Number(ratio[0]);
+ this.width = Number(ratio[1]);
+ this.canvasWidth = this.width * 10;
+ this.canvasHeight = this.height * 10;
+ // 重新初始化一维数组
+ this.pixelData = new Array(this.height * this.width).fill('')
+ console.log('pixelData---', this.pixelData);
+ },
+ // 预先获取像素网格的位置信息
+ getGridRect(cb) {
+ if (this.gridRect) {
+ cb(this.gridRect)
+ } else {
+ uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
+ this.gridRect = rect
+ cb(rect)
+ }).exec()
+ }
+ },
+ // 计算触摸点对应的格子
+ getCellByTouch(touch, cb) {
+ this.getGridRect(rect => {
+ if (!rect) return
+ const x = touch.clientX - rect.left
+ const y = touch.clientY - rect.top
+ const cellWidth = rect.width / this.width
+ const cellHeight = rect.height / this.height
+ const colIndex = Math.floor(x / cellWidth)
+ const rowIndex = Math.floor(y / cellHeight)
+ if (
+ colIndex >= 0 && colIndex < this.width &&
+ rowIndex >= 0 && rowIndex < this.height
+ ) {
+ cb({
+ rowIndex,
+ colIndex
+ })
+ }
+ })
+ },
+ handleTouchStart(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ })
+ },
+ handleTouchMove(e) {
+ // 阻止默认行为
+ e.preventDefault && e.preventDefault();
+
+ if (!e.touches.length) return
+ this.getCellByTouch(e.touches[0], cell => {
+ // 只在格子变化时才触发
+ if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
+ cell.colIndex) {
+ this.lastCell = cell
+ this.handleClick(cell.rowIndex, cell.colIndex)
+ }
+ })
+ },
+ handleTouchEnd() {
+ this.lastCell = null
+ },
+ handleClick(rowIndex, colIndex) {
+ console.log(rowIndex, colIndex)
+ // 计算一维数组的索引
+ const index = rowIndex * this.width + colIndex
+ // 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
+ const currentColor = this.pixelData[index]
+ const newColor = currentColor ? '' : this.color
+ this.$set(this.pixelData, index, newColor)
+ },
+ clear() {
+ ctx && ctx.clearRect(0, 0, 1000, 1000)
+ // 将所有像素点设置为空字符串
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 模版动画弹出框
+ changeAnimation(e) {
+ //console.log('打开关闭模版动画弹窗触发---',e)
+ // ios设备不支持弹出框横屏切换
+ // if(e.show){
+ // if (typeof plus !== 'undefined') {
+ // plus.screen.lockOrientation('landscape-primary')
+ // }
+ // }
+ // else{
+ // if (typeof plus !== 'undefined') {
+ // plus.screen.lockOrientation('portrait')
+ // }
+ // }
+ },
+ // 打开模版动画
+ popupAnimationOpen(index) {
+ this.fpsIndex = index
+ if (this.fpsList[this.fpsIndex] == null) {
+ console.log('第一次编辑-----null')
+ this.pixelData = new Array(this.height * this.width).fill('')
+ } else {
+ console.log('已编辑过-----')
+ this.pixelData = this.fpsList[this.fpsIndex]
+ }
+ },
+ // 关闭模版动画
+ popupAnimationClose() {
+ this.$refs.popupAnimation.close();
+ this.pixelData = new Array(this.height * this.width).fill('')
+ },
+ // 屏幕规格
+ changeSpec(e) {
+ const ratio = e.split('*');
+ this.specHeight = Number(ratio[0]);
+ this.specWidth = Number(ratio[1]);
+ },
+ // 偶数行数据反转
+ toSnakeData(pixelData, cols = 24) {
+ const rows = []
+ for (let i = 0; i < pixelData.length; i += cols) {
+ let row = pixelData.slice(i, i + cols)
+ if ((rows.length + 1) % 2 === 0) {
+ row.reverse() // 偶数行反转
+ }
+ rows.push(row)
+ }
+ return rows
+ },
+ // 二维数组转字符串
+ mergeRowsToString(rows) {
+ const rowStrings = rows.map(r => r.join('')) // 每行合并
+ return rowStrings.join('') // 所有行合并
+ },
+ // 生成一帧动画其他帧数据
+ generateFrames(data, frameCount = 12) {
+ const cols = this.specWidth; // 每行 24 个
+ const rows = this.specHeight; // 共 12 行
+ const frames = [];
+ // 拆分成 12 行
+ let grid = [];
+ for (let r = 0; r < rows; r++) {
+ grid.push(data.slice(r * cols, (r + 1) * cols));
+ }
+ // 生成帧
+ for (let f = 0; f < frameCount; f++) {
+ let frame = [];
+ // 每行左移 f 次
+ for (let r = 0; r < rows; r++) {
+ let row = grid[r].slice(); // 拷贝一行
+ let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
+ frame = frame.concat(shifted);
+ }
+ frames.push(frame);
+ }
+ return frames;
+ },
+
+ clickSpeed() {
+ this.$refs.pickerSpeed.open();
+ },
+ confirmSpeed(e) {
+ console.log('confirmSpeed', e.value[0]);
+ this.speed = e.value[0]
+ },
+ clickTotal() {
+ if (this.total != 1) {
+ this.$refs.pickerTotal.open();
+ }
+ },
+ confirmTotal(e) {
+ console.log('confirmTotal', e.value[0]);
+ this.total = e.value[0]
+ this.adjustFpsData()
+ },
+ changeMode(e) {
+ this.fpsList = []
+ this.fpsData = []
+ if (e == 0) {
+ this.total = 1
+ } else {
+ this.total = 2
+ }
+ this.adjustFpsData()
+ },
+ adjustFpsData() {
+ // 如果 fpsData 太长 → 截取
+ if (this.fpsList.length > this.total) {
+ this.fpsList = this.fpsList.slice(0, this.total);
+ }
+ // 如果 fpsData 太短 → 补齐 null
+ else if (this.fpsList.length < this.total) {
+ const missing = this.total - this.fpsList.length;
+ this.fpsList = this.fpsList.concat(new Array(missing).fill(null));
+ }
+
+ // 如果 fpsList 太长 → 截取
+ if (this.fpsData.length > this.total) {
+ this.fpsData = this.fpsData.slice(0, this.total);
+ }
+ // 如果 fpsList 太短 → 补齐 null
+ else if (this.fpsData.length < this.total) {
+ const missing = this.total - this.fpsData.length;
+ this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
+ }
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---', event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----', JSON.parse(res.data))
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ },
+ // 生成点阵数据
+ generateDotMatrix(char) {
+ return new Promise((resolve, reject) => {
+
+ // 使用 uni-app 的 canvas context
+ ctx = uni.createCanvasContext('myCanvas', this);
+
+ // 设置字体样式
+ ctx.setFontSize(this.canvasHeight * 0.9);
+ ctx.setFillStyle('#000')
+ ctx.setTextAlign('center')
+ ctx.setTextBaseline('middle')
+ // 绘制文字
+ // 计算文字位置
+ // 水平居中
+ const x = this.canvasWidth / 2;
+ // 垂直居中,稍微向上偏移一点
+ const y = this.canvasHeight / 2;
+
+ // 绘制文字
+ ctx.fillText(char, x, y);
+
+ // 绘制到画布
+ ctx.draw(false, () => {
+ // 获取画布数据
+ uni.canvasGetImageData({
+ canvasId: 'myCanvas',
+ x: 0,
+ y: 0,
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ success: (res) => {
+ console.log(res)
+ const imageData = res.data;
+ // 生成一维数组
+ const pixelArray = new Array(this.height * this.width).fill('')
+
+ // 计算每个格子的像素大小
+ const cellWidth = this.canvasWidth / this.width;
+ const cellHeight = this.canvasHeight / this.height;
+
+ // 生成一维数组
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ let blackPixels = 0;
+ let totalPixels = 0;
+
+ // 对每个格子进行采样
+ for (let py = 0; py < cellHeight; py++) {
+ for (let px = 0; px < cellWidth; px++) {
+ const pixelY = Math.floor(y * cellHeight + py);
+ const pixelX = Math.floor(x * cellWidth + px);
+
+ const idx = (pixelY * this.canvasWidth +
+ pixelX) * 4;
+ const r = imageData[idx];
+ const g = imageData[idx + 1];
+ const b = imageData[idx + 2];
+ const a = imageData[idx + 3];
+
+ // 计算亮度
+ const brightness = 0.3 * r + 0.59 * g + 0.11 *
+ b;
+
+ // 如果亮度小于阈值且透明度大于阈值,则认为是黑色
+ if (brightness < 128 && a > 128) {
+ blackPixels++;
+ }
+ totalPixels++;
+ }
+ }
+
+ // 如果黑色像素占比超过阈值,则设置为当前选择的颜色
+ const isBlack = blackPixels / totalPixels > 0.3;
+ const arrayIndex = y * this.width + x;
+ pixelArray[arrayIndex] = isBlack ? this.color :
+ ''; // 改为颜色值或空字符串
+ }
+ }
+
+ resolve(pixelArray);
+ },
+ fail: (err) => {
+ reject(err);
+ }
+ });
+ });
+ });
+ },
+
+ // 修改添加文字方法
+ async addText() {
+ uni.showModal({
+ title: '添加文字',
+ content: '',
+ editable: true,
+ placeholderText: '请输入文字',
+ success: async (res) => {
+ if (res.confirm) {
+ const text = res.content;
+ if (text.length > 0) {
+ try {
+ // 生成点阵数据
+ const dotMatrix = await this.generateDotMatrix(text);
+ console.log('点阵数据:', dotMatrix);
+ this.pixelData = dotMatrix
+ } catch (error) {
+ console.error('生成点阵失败:', error);
+ uni.showToast({
+ title: '生成点阵失败',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: '请输入文字',
+ icon: 'none'
+ });
+ }
+ }
+ }
+ });
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.color_list {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 30rpx;
+ box-sizing: border-box;
+ padding: 50rpx;
+
+ .color_item {
+ width: 30rpx;
+ height: 30rpx;
+ border-radius: 5rpx;
+ }
+}
+
+.controls {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+ display: flex;
+ gap: 20px;
+ z-index: 9;
+}
+
+.animation_pixel {
+ width: 100vw;
+ height: 100vh;
+ position: relative;
+
+ .back_btn {
+ position: absolute;
+ left: 30px;
+ top: 20px;
+ z-index: 9999;
+ }
+
+ .canvas {
+ margin: 100rpx auto 0;
+ background: #ccc;
+ position: absolute;
+ z-index: 1;
+ left: -9999px;
+ top: -9999px;
+ }
+
+ .pixel_grid {
+ position: fixed;
+ left: 0;
+ top: var(--status-bar-height);
+ right: 0;
+ bottom: 0;
+ display: grid;
+ gap: 1px;
+ box-sizing: border-box;
+ background: #eee;
+ touch-action: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+
+ .pixel_cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ background: #fff;
+
+ .pixel_cell_item {
+ width: 50%;
+ height: 50%;
+ aspect-ratio: 1/1;
+ font-size: 10rpx;
+ }
+ }
+ }
+
+}
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/feedback/feedback.vue b/ledApp/pages/feedback/feedback.vue
new file mode 100644
index 0000000..ab2cf71
--- /dev/null
+++ b/ledApp/pages/feedback/feedback.vue
@@ -0,0 +1,111 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="意见反馈" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="feedback">
+ <uv-textarea v-model="content" placeholder="请输入反馈内容" height="400rpx" customStyle="background:#ECEFF6" textStyle="font-size:27rpx"></uv-textarea>
+ </view>
+
+ <view class="feedback_btn" @click="clickFeedback">
+ 提交反馈
+ </view>
+
+ <view class="foot_tips">
+ 客服电话:{{phone}}
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import { feekback,configPhone } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ content: '',
+ userId: '',
+ phone: ''
+ }
+ },
+ onLoad() {
+ this.userId = uni.getStorageSync('userId');
+ this.getConfigPhone()
+ },
+ methods: {
+ getConfigPhone(){
+ configPhone().then(res=>{
+ console.log(res.msg)
+ this.phone = res.msg
+ })
+ },
+ clickFeedback(){
+ if(!this.content || this.content.trim() == ''){
+ uni.showToast({
+ title: '请输入反馈内容',
+ duration: 2000,
+ icon: 'none'
+ });
+ this.content = ''
+ return
+ }
+ feekback({
+ content: this.content,
+ userId: this.userId
+ }).then(res=>{
+ console.log(res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if(res.code == 200){
+ uni.navigateBack({
+ delta: 1
+ });
+ }
+ })
+
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.foot_tips{
+ position: fixed;
+ left: 0;
+ bottom: 100rpx;
+ right: 0;
+ text-align: center;
+ font-size: 30rpx;
+ color: rgba(0,0,0,0.7);
+ line-height: 38rpx;
+}
+.feedback_btn{
+ width: 423rpx;
+ height: 96rpx;
+ background: linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%);
+ box-shadow: 0rpx 12rpx 27rpx 0rpx rgba(56,151,243,0.38);
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ font-size: 30rpx;
+ color: #FFFFFF;
+ line-height: 96rpx;
+ text-align: center;
+ margin: 60rpx auto 0;
+}
+.feedback{
+ margin: 20rpx 30rpx 0;
+ background: linear-gradient( 180deg, #FFFFFF 0%, rgba(255,255,255,0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67,147,248,0.1), -8rpx -8rpx 23rpx 0rpx rgba(255,255,255,0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ box-sizing: border-box;
+ padding:30rpx;
+}
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/pages/index/index.vue b/ledApp/pages/index/index.vue
new file mode 100644
index 0000000..2c326cc
--- /dev/null
+++ b/ledApp/pages/index/index.vue
@@ -0,0 +1,1353 @@
+<template>
+ <view :class="deviceList.length == 0?'page_bg':'page_bg1'">
+ <uv-navbar title="我的设备" :placeholder="true" leftIcon="" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="hi" v-if="deviceList.length == 0">
+ HI,你来了~
+ </view>
+ <view class="add_device" v-if="deviceList.length == 0">
+ <navigator url="/pages/addDevice/addDevice" hover-class="none">
+ <image class="ico_add" src="/static/img/btn_1.png" mode="widthFix"></image>
+ <view class="name">
+ 添加设备
+ </view>
+ </navigator>
+ </view>
+
+ <view class="device_list" v-if="deviceList.length != 0">
+ <view class="device_item" v-for="(item,index) in deviceList" :key="index">
+ <view class="item">
+ <image class="ph" src="/static/img/ph_1.png" mode="aspectFill"></image>
+ <view class="info">
+ <view class="name">
+ {{item.name}}
+ </view>
+ <view class="version">
+ v {{item.versions}}
+ </view>
+ <view class="power">
+ <view v-if="devKwh && dId == item.id">
+ 电量{{devKwh * 10}}%
+ </view>
+ </view>
+
+ <view class="state" v-if="dId != item.id">
+ 未连接
+ </view>
+
+ <view class="state green" v-if="dId == item.id">
+ 已连接
+ </view>
+ <view class="btn" v-if="dId != item.id">
+ <uv-button type="primary" shape="circle" text="连接" size="small"
+ @click="createBLEConnection(item,index)"></uv-button>
+ <view class="btn_none"></view>
+ <uv-button type="primary" shape="circle" text="删除" size="small"
+ @click="clickEquipmentDel(item.id, index)"></uv-button>
+ </view>
+ <view class="btn" v-if="dId == item.id">
+ <uv-button type="primary" shape="circle" text="关闭" size="small"
+ @click="clickCloseBLEConnection(index)"></uv-button>
+ </view>
+
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="设备升级" size="small"></uv-button>
+ </view> -->
+ </view>
+ </view>
+ <view class="item_btn" v-if="dId == item.id">
+ <uv-button type="primary" shape="circle" text="调节亮度" size="small" :plain="true"
+ @click="this.$refs.popupBRIG.open();"></uv-button>
+ <uv-button type="primary" shape="circle" text="发送LED" size="small" :plain="true"
+ @click="getTempleteList"></uv-button>
+ </view>
+ </view>
+ <!-- <view class="device_item">
+ <view class="item">
+ <image class="ph" src="/static/img/ph_1.png" mode="aspectFill"></image>
+ <view class="info">
+ <view class="name">
+ 设备名称占位文字
+ </view>
+ <view class="version">
+ v2.21
+ </view>
+ <view class="power">
+
+ </view>
+ <view class="state">
+ 未连接
+ </view>
+ <view class="btn">
+ <uv-button type="primary" shape="circle" text="设备升级" size="small"></uv-button>
+ </view>
+ </view>
+ </view>
+ </view> -->
+
+ <navigator class="add_device_small" url="/pages/addDevice/addDevice" hover-class="none"
+ :render-link="false">
+ <image class="ico_add" src="/static/img/btn_1.png" mode="widthFix"></image>
+ <view class="name">
+ 添加设备
+ </view>
+ </navigator>
+
+ </view>
+
+
+ <!-- 调节亮度 -->
+ <uv-popup ref="popupBRIG" mode="center" round="30rpx">
+ <view class="popup_pwd">
+ <view class="title">调节亮度</view>
+ <view class="cont">
+ <uv-slider v-model="brightness" :min="0" :max="9" :step="1" block-size="24"
+ block-color="#2979ff"></uv-slider>
+ </view>
+ <view class="btns_two">
+ <view class="btn2">
+ <uv-button type="primary" plain @click="this.$refs.popupBRIG.close();">关闭</uv-button>
+ </view>
+ <view class="btn2">
+ <uv-button type="primary" @click="commandBRIG()">确定</uv-button>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+ <!-- 模版列表 -->
+ <uv-popup ref="popupTemplete" mode="center" round="30rpx">
+ <view class="popup_pwd">
+ <view class="title">发送LED</view>
+ <view class="cont cont_led">
+
+ <view class="led_item" v-for="(item,index) in templeteData" :key="index">
+ <view class="led_item_pic">
+ <uv-image :src="baseURL+item.pic" width="160rpx" height="120rpx" :radius="5"></uv-image>
+ </view>
+ <view class="led_item_text">
+ <view class="name">
+ {{item.name}}
+ </view>
+ <view class="text">
+ {{item.screen}}
+ </view>
+ </view>
+ <uv-button type="primary" shape="circle" size="small" @click="commandSendTemplate(item)">播放</uv-button>
+ </view>
+
+ </view>
+ <view class="btns">
+ <uv-button type="primary" plain @click="this.$refs.popupTemplete.close();">关闭</uv-button>
+ </view>
+ </view>
+ </uv-popup>
+
+ </view>
+</template>
+
+<script>
+ import {
+ equipmentList,
+ equipmentDel,
+ templeteList
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ searchIng: true,
+ userId: '',
+ deviceList: [],
+
+ bluetoothList: [], //搜索到的蓝牙设备列表
+ deviceId: '', //设备id
+ serviceId: '', //服务uuid
+ characteristicId1: '', //特征uuid
+ characteristicId2: '',
+ characteristicId3: '',
+ showPwdPopup: false,
+ password: '', //设置的密码
+ checId: '', //智能壳的ID
+ batchNumber: '', //生产顺序号
+ fileNames: [], //文件名称列表
+ brightness: 3, //亮度
+ nameIndex: 1,
+ name: '',
+
+ ledName: '', // 名称
+ screen: '',
+ ledTotal: 3, // 总帧数
+ ledCurrent: 0, // 当前帧数
+ ledData: [],
+ ledSpeed: '', //速度
+ isLink: false,
+ listIndex: 0,
+ templeteData: [],
+ baseURL: '',
+ dId: '' ,//已连接的设备id,
+ devKwh: '',
+ platform: ''
+ }
+ },
+ onLoad() {
+ //uni.setStorageSync('dId', '');
+ this.userId = uni.getStorageSync('userId');
+ this.baseURL = uni.$uv.http.config.baseURL
+ const systemInfo = uni.getSystemInfoSync();
+ if (systemInfo.platform === 'ios') {
+ // iOS 设备
+ this.platform = 'ios'
+ } else if (systemInfo.platform === 'android') {
+ // Android 设备
+ this.platform = 'android'
+ }
+ else{
+ this.platform = ''
+ }
+ },
+ onShow() {
+ this.getEquipmentList()
+ this.openBluetooth()
+
+ const that = this
+ this.userId = uni.getStorageSync('userId');
+
+ this.dId = uni.getStorageSync('dId');
+ if(this.dId){
+ this.deviceId = uni.getStorageSync('deviceId');
+ this.serviceId = uni.getStorageSync('serviceId');
+ this.characteristicId1 = uni.getStorageSync('characteristicId1');
+ this.characteristicId2 = uni.getStorageSync('characteristicId2');
+ this.characteristicId3 = uni.getStorageSync('characteristicId3');
+ this.checId = uni.getStorageSync('checId');
+ this.password = uni.getStorageSync('password');
+ this.startNotify()
+ }
+
+ console.log('dId---',this.dId)
+
+ //this.getTempleteList()
+ },
+ onHide() {
+ console.log('onHide-----')
+ uni.$off('BLECharacteristic')
+
+ uni.setStorageSync('dId', this.dId);
+ uni.setStorageSync('deviceId', this.deviceId);
+ uni.setStorageSync('serviceId', this.serviceId);
+ uni.setStorageSync('characteristicId1', this.characteristicId1);
+ uni.setStorageSync('characteristicId2', this.characteristicId2);
+ uni.setStorageSync('characteristicId3', this.characteristicId3);
+ uni.setStorageSync('checId', this.checId);
+ uni.setStorageSync('password', this.password);
+
+ //this.clickCloseBLEConnection()
+
+ //this.closeBluetooth()
+ //this.closeBLEConnection()
+ },
+ onUnload() {
+ console.log('onUnload-----')
+ uni.$off('BLECharacteristic')
+
+ //this.closeBluetooth()
+ //this.closeBLEConnection()
+ },
+ methods: {
+ // 获取模版列表
+ getTempleteList() {
+ uni.showLoading({
+ title: '加载中...',
+ mask: true
+ });
+ templeteList({
+ userId: this.userId,
+ pageNum: 1,
+ pageSize: 99
+ }).then(res => {
+ console.log('模版列表-------', res.rows)
+ uni.hideLoading();
+ this.templeteData = res.rows
+ if (this.templeteData.length != 0) {
+ this.$refs.popupTemplete.open();
+ } else {
+ uni.showToast({
+ title: '暂无数据,请先添加模版!',
+ icon: 'none',
+ mask: true
+ });
+ }
+
+ })
+ },
+ // 删除设备
+ clickEquipmentDel(id, index) {
+ uni.showModal({
+ title: '提示',
+ content: '删除后无法恢复,确定删除吗?',
+ success: (resa) => {
+ if (resa.confirm) {
+ console.log('用户点击确定');
+ equipmentDel(id).then(res=>{
+ console.log(res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if(res.code == 200){
+ //this.getTempleteList()
+ this.deviceList.splice(index, 1)
+ }
+ })
+ } else if (resa.cancel) {
+ console.log('用户点击取消');
+ }
+ }
+ });
+ },
+ // 设备列表
+ getEquipmentList() {
+ this.deviceList = []
+ equipmentList({
+ userId: this.userId,
+ code: this.platform,
+ pageNum: 1,
+ pageSize: 99
+ }).then(res => {
+ console.log('设备列表------', res.rows)
+ this.deviceList = res.rows
+ })
+ },
+ // 初始化蓝牙
+ openBluetooth() {
+ uni.openBluetoothAdapter({
+ success: (res) => {
+ console.log('蓝牙初始化成功');
+ this.onBLEStateChange()
+ },
+ fail: (err) => {
+ console.log('蓝牙初始化失败', err);
+ }
+ });
+ },
+ // 关闭蓝牙
+ closeBluetooth() {
+ uni.closeBluetoothAdapter({
+ success: (res) => {
+ console.log(res)
+ }
+ })
+ },
+ // 断开与低功耗蓝牙设备的连接
+ closeBLEConnection() {
+ const that = this
+ uni.closeBLEConnection({
+ deviceId: this.deviceId,
+ success(res) {
+ console.log('断开与低功耗蓝牙设备的连接', res)
+ that.dId = ''
+ uni.setStorageSync('dId', '');
+ }
+ })
+ },
+ // 关闭连接
+ clickCloseBLEConnection() {
+ const that = this
+ console.log('that.deviceId-----', that.deviceId)
+ uni.closeBLEConnection({
+ deviceId: that.deviceId,
+ success(res) {
+ console.log('关闭连接', res)
+ // 关闭连接清空已连接的设备id
+ that.dId = ''
+ uni.setStorageSync('dId', '');
+ //that.isLink = false
+ //that.deviceList[that.listIndex].isLink = false
+ }
+ })
+ },
+ // 发送LED
+ commandSendTemplate(item) {
+ console.log('--------------', item);
+
+ uni.showLoading({
+ title: '发送中...',
+ mask: true
+ });
+
+ const name = 'A' + this.nameIndex++;
+ this.ledName = name;
+ this.screen = item.screen;
+ this.ledTotal = item.fpsTotal;
+ this.ledSpeed = item.speed
+ this.ledCurrent = 0;
+
+ try {
+ this.ledData = JSON.parse(item.fpsData);
+ console.log('this.ledData------',this.ledData)
+ } catch (e) {
+ uni.hideLoading();
+ console.error('fpsData 解析失败:', e);
+ uni.showToast({
+ title: 'fpsData 解析失败',
+ duration: 2000
+ });
+ return;
+ }
+
+ if (this.ledData.length > 0) {
+ this.commandINST(this.ledData[this.ledCurrent]);
+ } else {
+ uni.hideLoading();
+ uni.showToast({
+ title: '无可发送的帧数据',
+ duration: 2000
+ });
+ console.warn('无可发送的帧数据');
+ }
+ },
+ // 连接
+ createBLEConnection(item, index) {
+ const that = this
+ if (this.dId) {
+ uni.showToast({
+ title: '请先关闭已连接的设备',
+ icon: 'none',
+ mask: true
+ });
+ return
+ }
+ this.deviceId = item.deviceId
+ this.serviceId = item.serviceId
+ this.characteristicId1 = item.characteristicId1
+ this.characteristicId2 = item.characteristicId2
+ this.characteristicId3 = item.characteristicId3
+ this.checId = item.checId
+ this.password = item.password
+
+
+ uni.showLoading({
+ title: '连接中...',
+ mask: true
+ });
+ uni.createBLEConnection({
+ deviceId: this.deviceId,
+ success: (res) => {
+ console.log('连接低功耗蓝牙设备', res);
+ this.startNotify()
+ // setTimeout(() => {
+ // that.setBaud()
+ // that.setBLEMTU()
+ // }, 500);
+ this.dId = item.id
+
+ setTimeout(() => {
+ this.commandPOWR()
+ }, 1500);
+ },
+ fail: (err) => {
+ //console.log('连接失败', err);
+ this.closeBLEConnection()
+ uni.hideLoading();
+ uni.showToast({
+ title: '连接失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ // 设置波特率
+ setBaud() {
+ var that = this
+ var hexStr = "06000900009600000801009F"
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId3,
+ value: this.hexStringToArrayBuffer(hexStr),
+ writeType: 'write',
+ success: (res) => {
+ console.log('设置波特率成功----', res);
+ //that.commandBIND()
+ uni.hideLoading();
+ },
+ fail: (err) => {
+ console.log('设置波特率失败----', err);
+ uni.hideLoading();
+ uni.showToast({
+ title: '设置波特率失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ // 设置蓝牙最大传输单元
+ setBLEMTU() {
+ var that = this
+ uni.setBLEMTU({
+ deviceId: this.deviceId,
+ mtu: 80, // 最大值一般 512,蓝牙 4.2 通常支持 247
+ success: (res) => {
+ console.log('设置MTU成功:', res);
+ uni.hideLoading();
+ },
+ fail: (err) => {
+ console.error('设置MTU失败:', err);
+ uni.hideLoading();
+ uni.showToast({
+ title: '设置MTU失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ commandCHEC() {
+ //555555555555555555557e41434845430095977e
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x43, 0x48, 0x45, 0x43]; // "CHEC"的ASCII码
+ const reserved = 0x00; // 保留字节
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ reserved
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('CHEC data----AAAA--------', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 500);
+ },
+ // 加密绑定号 发送绑定号
+ commandBIND() {
+ // uni.showToast({
+ // title: '发送绑定号',
+ // icon: 'none',
+ // mask: true
+ // });
+ //console.log('checId---', this.checId)
+ // 59 5C 37 39 71 4D 56 65 5A 37 12 18 07 06 2A 18 36 1E 1E 06
+ // 7e 41 42 49 4e 44 6d 71 41 45 95 4e 61 78 9e 39 49 12 15 13 71 42 94 50 50 17 7c c9 7e
+ //帧头+0x41+ BIND +加密绑定号+CRC校验码+帧尾
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x42, 0x49, 0x4E, 0x44]; // "BIND"的ASCII码
+ const reserved = this.replaceScreenSizeInID(this.checId, this.password); // 密码
+ //console.log('reserved----111', reserved)
+ const reservedId = this.encryptSmartShellID(reserved)
+ //console.log('reservedid--222', reservedId)
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ ...this.hexStringToArray(reservedId)
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ //console.log('BIND data----BBBB--------', this.hexArrayToHexString(data))
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 500);
+ },
+
+ //向低功耗蓝牙设备特征值中写入二进制数据
+ writeBLECharacteristicValue(buffer) {
+ const that = this
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId2,
+ value: buffer,
+ writeType: 'write',
+ success: (res) => {
+ //console.log('写入指令发送成功----', res);
+ uni.hideLoading();
+ },
+ fail: (err) => {
+ //console.log('写入指令发送失败----', err);
+ uni.hideLoading();
+ uni.showToast({
+ title: '写入指令发送失败',
+ icon: 'none',
+ mask: true
+ });
+ }
+ });
+ },
+ // 调节亮度
+ commandBRIG() {
+ console.log('brightness----', this.brightness)
+ //1.手机发指令:启动字节+帧头+0x41+ BRIG +亮度+CRC校验码+帧尾。
+ //2.智能壳回指令:启动字节+帧头+0x41+ BRIG +返回标志+CRC校验码+帧尾。
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x42, 0x52, 0x49, 0x47]; // "BRIG"的ASCII码
+
+ //亮度:单字节,0,1,2,3。。。ASIC码
+ const brightness = this.stringToASCIIByteArray(this.brightness.toString())
+ //const brightness = 0x32
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command,
+ brightness
+ ];
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ console.log('BRIG data----', data)
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 500);
+
+ },
+ //添加信息指令(INST)
+ commandINST(hexString) {
+ // 1.手机发指令:启动+帧头+包头段(0x43+简易绑定号+屏大小)+INST+信息属性+文件名+总帧数+当前帧*+块描述+块数据+CRC校验码+帧尾
+ //注意:当前帧如果是0xff,则表示是结束帧,后面四个字节,第一个字节(表示速度),第二个字节(播放次数,0表示一直播放),后面二个字节保留;其他数字表示表示当前是第几帧。
+ //2.智能壳指令:启动+帧头+包头段+INST+返回标志+加密地址+CRC校验码+帧尾
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ //const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ //console.log('屏大小ID----',this.checId)
+ //console.log('屏大小----',screenSize,this.checId.slice(20, 24))
+ const screenSize = [0x0C,0x18]
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x49, 0x4E, 0x53, 0x54]; // "INST"的ASCII码
+ const infoAttribute = 0x02; // 信息属性: 0=一般信息
+
+ const fileName = this.stringToASCIIByteArray(this.ledName) //文件名称
+ //console.log('fileName----', this.ledName)
+ // 总帧数
+ const totalFrame = this.hexStringToArray(this.ledTotal.toString(16).padStart(2, '0')
+ .toUpperCase()) //[0x01]; //this.hexStringToArray('01')
+ //console.log('totalFrame----', totalFrame)
+ // 当前帧
+ const currentFrame = this.hexStringToArray(this.ledCurrent.toString(16).padStart(2, '0').toUpperCase()) //0x00
+
+ // 块描述
+ const tblockDesc = 0x00
+
+ // 块数据
+ const blockDescription = this.hexStringToArray(hexString)
+ //const blockString = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF000000000000000000000000000000000000000000000000000000000000FF0000000000000000000000000000000000000000000000FF0000000000000000FF0000FF00000000000000000000000000000000FF0000FF000000000000000000FF0000000000FF0000FF00FF0000FF0000FF000000000000000000FF00FF0000FF000000000000000000FF00000000000000000000FF000000FF0000FF0000FF0000FF0000000000FF0000000000000000000000FF0000000000000000000000FF0000FF0000000000000000FF0000FF0000000000000000000000000000FF00FF000000000000000000000000FF00FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF0000000000000000FF0000FF000000000000000000000000FF0000000000FF0000000000000000000000000000000000000000FF0000000000000000000000000000000000FF0000000000000000000000000000000000FF00000000000000000000000000FF0000FF000000FF0000FF0000000000000000000000FF00000000000000FF0000FF000000FF00FF0000FF000000000000FF0000000000000000000000000000000000000000000000FF0000000000FF0000000000000000000000FF0000000000000000000000000000000000000000FF0000000000000000FF0000000000000000FF0000000000000000FF00000000000000FF0000FF0000FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF000000000000000000000000000000000000FF0000FF0000000000000000FF0000000000000000FF0000000000000000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ //const blockDescription = this.hexStringToArray(blockString)
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute,
+ ...fileName,
+ ...totalFrame,
+ ...currentFrame,
+ tblockDesc,
+ ...blockDescription
+ ];
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ //console.log(this.ledCurrent, '----INST data----', this.hexArrayToHexString(data))
+ //const buffer = new Uint8Array(data).buffer;
+ console.log('第几帧-------',this.ledCurrent)
+ setTimeout(() => {
+ //this.writeBLECharacteristicValue(buffer)
+ this.sendBLEDataInChunks(
+ this.hexArrayToHexString(data)
+ )
+ }, 50);
+ },
+ // 发送结束针
+ commandEnd() {
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x43; // 包类型
+ // 使用新的简易绑定号生成函数
+ const simpleBindId = this.generateSimpleBindId();
+
+ // 屏大小 (2 bytes): 从ID中获取
+ const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+ const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+ const command = [0x49, 0x4E, 0x53, 0x54]; // "INST"的ASCII码
+ const infoAttribute = 0x02; // 信息属性: 0=一般信息
+
+ const fileName = this.stringToASCIIByteArray(this.ledName) //文件名称
+ //console.log('fileName----', this.ledName)
+ // 总帧数
+ const totalFrame = this.hexStringToArray(this.ledTotal.toString(16).padStart(2, '0').toUpperCase()) //[0x01]; //this.hexStringToArray('01')
+ //console.log('totalFrame----', totalFrame)
+
+ //结束幕:
+ const ffdata = this.hexStringToArray('ff')
+ const speedData = this.hexStringToArray(this.ledSpeed.toString(16).padStart(2, '0').toUpperCase())
+
+ console.log('speedData------',speedData)
+ const ffend = this.hexStringToArray('000000')
+
+ const endData = [
+ ...startBytes,
+ frameHeader,
+ ...packageHeader,
+ ...command,
+ infoAttribute,
+ ...fileName,
+ ...totalFrame,
+ ...ffdata,
+ ...speedData,
+ ...ffend
+ ]
+ // 计算CRC校验码(从包头段开始计算)
+ const endcrc = this.calcCRC16(endData.slice(11));
+
+ // 添加CRC和帧尾
+ endData.push((endcrc >> 8) & 0xFF); // CRC高8位
+ endData.push(endcrc & 0xFF); // CRC低8位
+ endData.push(frameHeader); // 帧尾
+ //console.log('INST endData----', this.hexArrayToHexString(endData))
+
+ const endbuffer = new Uint8Array(endData).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(endbuffer)
+ }, 50);
+ },
+ async sendBLEDataInChunks(hexStr) {
+ const maxBytes = 76
+ const interval = 20
+ const chunks = this.splitHexData(hexStr, maxBytes)
+
+ //console.log(`📦 总共需要发送 ${chunks.length} 包,每包 ${maxBytes} 字节,间隔 ${interval}ms`)
+
+ for (let i = 0; i < chunks.length; i++) {
+ const buffer = this.hexToBuffer(chunks[i])
+ try {
+ await new Promise((resolve, reject) => {
+ uni.writeBLECharacteristicValue({
+ deviceId: this.deviceId,
+ serviceId: this.serviceId,
+ characteristicId: this.characteristicId2,
+ value: buffer,
+ success: (res) => {
+ //console.log(`✅ 第 ${i + 1} 包发送成功`, res)
+ resolve()
+ },
+ fail: (err) => {
+ //console.error(`❌ 第 ${i + 1} 包发送失败`, err)
+ uni.hideLoading();
+ reject(err)
+ }
+ })
+ })
+ } catch (err) {
+ console.error(`🚫 发送中断,第 ${i + 1} 包发送失败`)
+ uni.hideLoading();
+ break
+ }
+ // 延迟下一包发送
+ await new Promise(r => setTimeout(r, interval))
+ }
+ //console.log("🎉 数据发送完成")
+ },
+ // 读取电量
+ commandPOWR() {
+ console.log('读取电量POWR----', )
+ // 构建指令数据
+ const startBytes = new Array(10).fill(0x55); // 10个启动字节
+ const frameHeader = 0x7E; // 帧头
+ const packageType = 0x41; // 包类型
+ const command = [0x50, 0x4F, 0x57, 0x52]; // "POWR"的ASCII码
+
+ // 组合数据
+ const data = [
+ ...startBytes,
+ frameHeader,
+ packageType,
+ ...command
+ ];
+
+ // 计算CRC校验码(从包头段开始计算)
+ const crc = this.calcCRC16(data.slice(11));
+
+ // 添加CRC和帧尾
+ data.push((crc >> 8) & 0xFF); // CRC高8位
+ data.push(crc & 0xFF); // CRC低8位
+ data.push(frameHeader); // 帧尾
+ //console.log('POWR----', data)
+
+ const buffer = new Uint8Array(data).buffer;
+ setTimeout(() => {
+ this.writeBLECharacteristicValue(buffer)
+ }, 500);
+ },
+ // 监听蓝牙连接状态
+ onBLEStateChange(){
+ const that = this
+ uni.onBLEConnectionStateChange(res => {
+ //console.log('监听蓝牙连接状态------------',res)
+ if (!res.connected) {
+ // uni.showToast({
+ // title: '蓝牙设备已断开',
+ // icon: 'none',
+ // mask: true
+ // });
+ that.dId = ''
+ uni.setStorageSync('dId', '');
+ //console.log('蓝牙设备已断开------------',res)
+ //that.isLink = false
+ // for(let i = 0;i<that.deviceList.length;i++){
+ // if(that.deviceList[i].isLink){
+ // that.deviceList[i].isLink = false
+ // }
+ // }
+ }
+ })
+ },
+ // 开始监听
+ startNotify() {
+ const that = this
+ uni.notifyBLECharacteristicValueChange({
+ deviceId: that.deviceId,
+ serviceId: that.serviceId,
+ characteristicId: that.characteristicId1,
+ state: true,
+ success: () => {
+ console.log('开始监听------');
+ setTimeout(() => {
+ that.setBaud()
+ if (uni.getSystemInfoSync().platform === 'android') {
+ that.setBLEMTU();
+ }
+ else{
+ uni.hideLoading();
+ }
+
+ }, 500);
+
+ uni.$off('BLECharacteristic');
+ uni.$on('BLECharacteristic', function(res) {
+
+ //console.log('BLECharacteristic---------------index', res)
+ const resData = that.ab2hex(res.value);
+ const result = that.removeStartBytes(resData)
+ const resName = that.getCommandType(result);
+ //console.log("回调返回数据-------", resData);
+ //console.log('result-----------', result)
+ //console.log('resName----------', resName)
+ //uni.hideLoading();
+ if (resName == 'CHEC') {
+
+ } else if (resName == 'BIND') {
+ //that.isLink = true;
+ //that.deviceList[that.listIndex].isLink = true;
+ //that.commandPOWR()
+ } else if (resName == 'LIST') {
+ //that.parseFileList(result)
+ } else if (resName == 'BRIG') {
+
+ } else if (resName == 'PLAY') {
+
+ } else if (resName == 'INST') {
+ // 添加动画帧
+ that.ledCurrent++
+ if (that.ledCurrent < that.ledTotal) {
+ // uni.showLoading({
+ // title: '发送中',// + that.ledCurrent,
+ // mask: true
+ // });
+ that.commandINST(that.ledData[that.ledCurrent]);
+ } else if (that.ledCurrent == that.ledTotal) {
+ // uni.showLoading({
+ // title: '发送结束帧',
+ // mask: true
+ // });
+ that.commandEnd()
+ }
+
+ } else if (resName == 'DALL') {
+
+ } else if (resName == 'POWR') {
+ // 查询电量
+ let capacityHex = result.slice(-8, -6); // "0a"
+ let capacity = parseInt(capacityHex, 16); // 转十进制
+ that.devKwh = capacity
+ } else {
+ //that.isLink == false
+ uni.showToast({
+ title: '返回数据错误',
+ duration: 2000,
+ icon: 'none'
+ });
+ // 关闭蓝牙设备
+ that.closeBLEConnection()
+ }
+ })
+ },
+ fail: (err) => {
+ //console.log('监听失败----', err);
+ uni.hideLoading();
+ // uni.showToast({
+ // title: '监听失败',
+ // icon: 'none',
+ // mask: true
+ // });
+ that.dId = ''
+ uni.setStorageSync('dId', '');
+ }
+ });
+ },
+ // 生成简易绑定号
+ generateSimpleBindId() {
+ if (!this.checId || !this.password) {
+ //console.error("生成简易绑定号失败:checId或password不可用");
+ uni.hideLoading();
+ uni.showToast({
+ title: '请先连接设备',
+ icon: 'none',
+ mask: true
+ });
+ return;
+ }
+ // 从checId中提取生产顺序号(最后4字节,即8个字符)
+ const productionNumber = this.checId.slice(-8); // 生产顺序号
+ // 生产顺序号+密码
+ const simpleBindData = productionNumber + this.password;
+ // 用加密数字加密
+ const encryptedSimpleBind = this.encryptSmartShellID(simpleBindData);
+ // 取前6字节作为简易绑定号
+ const simpleBindId = this.hexStringToArray(encryptedSimpleBind.slice(0, 12));
+ //console.log('简易绑定号生成----', simpleBindId);
+ return simpleBindId;
+ },
+ // 判断并去除10个0x55启动字节(20个hex字符)
+ removeStartBytes(hexStr) {
+ // 10个0x55 = 20个hex字符
+ const startBytes = '55'.repeat(10); // '55555555555555555555'
+ if (hexStr.startsWith(startBytes)) {
+ return hexStr.slice(20); // 去掉前20个字符
+ }
+ return hexStr;
+ },
+ // 返回指令类型 判断
+ getCommandType(hexString) {
+ // 判断是否有 返回标志 单字节。0(0x30),工作无异常;1(0x31)工作有异常
+ // 判断是否包含返回标志 30 或 31
+ // 查找30或31的位置
+ let returnFlag = '';
+ let cmd = '';
+ let packageType = hexString.slice(2, 4);
+
+ if (packageType == '41') {
+ returnFlag = hexString.slice(12, 14);
+ const before8Chars = hexString.slice(4, 12);
+ cmd = before8Chars.match(/.{2}/g)
+ .map(h => String.fromCharCode(parseInt(h, 16)))
+ .join('');
+ return cmd;
+ } else if (packageType == '43') {
+ returnFlag = hexString.slice(28, 30);
+ const before8Chars = hexString.slice(20, 28);
+ cmd = before8Chars.match(/.{2}/g)
+ .map(h => String.fromCharCode(parseInt(h, 16)))
+ .join('');
+ return cmd;
+ }
+ //console.log('returnFlag------', returnFlag)
+ },
+ // 将十六进制字符串转为 ArrayBuffer
+ hexToBuffer(hexStr) {
+ const typedArray = new Uint8Array(hexStr.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
+ return typedArray.buffer
+ },
+ // 将大十六进制数据切分为每段 maxBytes 大小(单位:字节)
+ splitHexData(hexStr, maxBytes = 100) {
+ const result = []
+ for (let i = 0; i < hexStr.length; i += maxBytes * 2) {
+ result.push(hexStr.slice(i, i + maxBytes * 2))
+ }
+ return result
+ },
+ // 指令ASCII码转换
+ stringToASCIIByteArray(str) {
+ return str.split('').map(char => char.charCodeAt(0));
+ },
+ //16进制数组 转 16进制字符串
+ hexArrayToHexString(hexArray) {
+ return hexArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
+ },
+ // 16进制字符串转16进制数组
+ hexStringToArray(hexStr) {
+ if (!hexStr) return [];
+ return hexStr.match(/.{2}/g).map(byte => parseInt(byte, 16));
+ },
+ // 16进制数据转二进数据
+ hexStringToArrayBuffer(hexStr) {
+ const bytes = new Uint8Array(hexStr.match(/.{2}/g).map(byte => parseInt(byte, 16)));
+ return bytes.buffer;
+ },
+ // 二进数据转16进制数据
+ ab2hex(buffer) {
+ const hexArr = Array.prototype.map.call(
+ new Uint8Array(buffer),
+ function(bit) {
+ return ('00' + bit.toString(16)).slice(-2)
+ }
+ )
+ return hexArr.join('')
+ },
+ calcCRC16(data) {
+ // 创建查表
+ const table = this.buildCRC16Table();
+ let crc = 0;
+ // 计算CRC
+ for (let i = 0; i < data.length; i++) {
+ crc = ((crc << 8) & 0xFF00) ^ table[((crc >> 8) ^ data[i]) & 0xFF];
+ }
+ return crc & 0xFFFF;
+ },
+ buildCRC16Table() {
+ const table = new Array(256);
+ const poly = 0x1021; // CRC-CCITT多项式
+ for (let i = 0; i < 256; i++) {
+ let nData = i << 8;
+ let nAccum = 0;
+ for (let j = 0; j < 8; j++) {
+ if ((nData ^ nAccum) & 0x8000) {
+ nAccum = (nAccum << 1) ^ poly;
+ } else {
+ nAccum <<= 1;
+ }
+ nData <<= 1;
+ }
+ table[i] = nAccum & 0xFFFF;
+ }
+ return table;
+ },
+ // 密码置换掉ID号中"屏的大小",加密下发
+ replaceScreenSizeInID(originalHexString, password4Digit) {
+ // 密码4个数字,每二个合起来一个字节,如3472变成 0x34 0x72
+ const high = password4Digit.slice(0, 2); // "34"
+ const low = password4Digit.slice(2, 4); // "72"
+
+ // 替换屏幕大小字段(第 10、11 个字节,即第20-23个字符)
+ const before = originalHexString.slice(0, 20);
+ const after = originalHexString.slice(24);
+
+ return before + high + low + after;
+ },
+ // 解密函数
+ decryptSmartShellID(encryptedHex) {
+ const key = '2021101236011119680215001413714294505017';
+ const decrypted = [];
+ for (let i = 0; i < 20; i++) {
+ const byteHex = encryptedHex.substr(i * 2, 2);
+ const encryptedByte = parseInt(byteHex, 16);
+ const keyHex = key.substr(i * 2, 2)
+ const keyByte = parseInt(keyHex, 16);
+ const decryptedByte = (encryptedByte - keyByte);
+ decrypted.push(decryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+ return decrypted.join('');
+ },
+ // 加密函数
+ encryptSmartShellID(encryptedHex) {
+ const key = '2021101236011119680215001413714294505017';
+ const decrypted = [];
+ for (let i = 0; i < 20; i++) {
+ const byteHex = encryptedHex.substr(i * 2, 2);
+ const encryptedByte = parseInt(byteHex, 16);
+ const keyHex = key.substr(i * 2, 2)
+ const keyByte = parseInt(keyHex, 16);
+ const decryptedByte = (encryptedByte + keyByte);
+ decrypted.push(decryptedByte.toString(16).padStart(2, '0').toUpperCase());
+ }
+ return decrypted.join('');
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .btns_two {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 40rpx;
+ margin-top: 50rpx;
+
+ .btn2 {
+ width: 47%;
+ }
+ }
+
+ .cont_led {
+ max-height: 700rpx;
+ overflow-y: auto;
+ }
+
+ .popup_pwd {
+ width: 680rpx;
+ box-sizing: border-box;
+ padding: 0 40rpx 50rpx;
+
+ .led_item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 15rpx 0;
+ .led_item_text{
+ width: 100%;
+ padding:0 20rpx;
+ }
+ .name {
+ font-size: 30rpx;
+ color: #333;
+ line-height: 50rpx;
+ }
+ .text{
+ font-size: 28rpx;
+ color: #666;
+ line-height: 50rpx;
+ }
+
+ .btn {
+ flex-shrink: 0;
+ width: 120rpx;
+ }
+ }
+
+ .title {
+ text-align: center;
+ font-weight: 500;
+ font-size: 32rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 42rpx;
+ padding: 50rpx 0 40rpx;
+ }
+
+ .btns {
+ margin-top: 40rpx;
+ }
+ }
+
+ .device_list {
+ padding: 0 30rpx;
+
+ .device_item {
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 20rpx;
+ border: 2rpx solid #FFFFFF;
+ padding: 22rpx;
+ margin-top: 24rpx;
+ position: relative;
+
+ .item_btn {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin-top: 20rpx;
+ border-top: 1rpx dashed #ddd;
+ padding-top: 22rpx;
+ gap: 20rpx;
+ }
+
+ .item {
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ position: relative;
+ }
+
+ .ph {
+ display: block;
+ flex-shrink: 0;
+ width: 230rpx;
+ height: 170rpx;
+ border-radius: 20rpx;
+ margin-right: 24rpx;
+ }
+
+ .info {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ position: relative;
+
+ .name {
+ font-weight: 500;
+ font-size: 27rpx;
+ color: #000000;
+ line-height: 50rpx;
+ box-sizing: border-box;
+ padding-right: 80rpx;
+ }
+
+ .version {
+ font-size: 26rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 40rpx;
+ }
+
+ .power {
+ font-size: 26rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 40rpx;
+ height: 40rpx;
+ }
+
+ .state {
+ position: absolute;
+ top: 10rpx;
+ right: 0;
+ font-size: 22rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 38rpx;
+
+ &.green {
+ color: #2BA471;
+ }
+ }
+
+ .btn {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: end;
+
+ .btn_none {
+ flex-shrink: 0;
+ width: 20rpx;
+ }
+ }
+ }
+ }
+ }
+
+ .add_device_small {
+ margin: 30rpx auto 0;
+ background: rgba(255, 255, 255, 0.95) url('/static/img/bg_1.png') no-repeat center bottom;
+ background-size: 100% auto;
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.2), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20rpx 30rpx 10rpx;
+
+ .ico_add {
+ width: 100rpx;
+ }
+
+ .name {
+ font-weight: 400;
+ font-size: 31rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 40rpx;
+ margin-bottom: 10rpx;
+ text-align: center;
+ }
+ }
+
+ .add_device {
+ margin: 60rpx auto 0;
+ width: 688rpx;
+ height: 319rpx;
+ background: rgba(255, 255, 255, 0.95) url('/static/img/bg_1.png') no-repeat center bottom;
+ background-size: 100% auto;
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+
+ .ico_add {
+ width: 220rpx;
+ }
+
+ .name {
+ font-weight: 400;
+ font-size: 31rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 40rpx;
+ margin-bottom: 10rpx;
+ text-align: center;
+ }
+ }
+
+ .hi {
+ text-align: center;
+ color: #fff;
+ font-weight: 500;
+ font-size: 42rpx;
+ line-height: 42rpx;
+ padding: 40rpx 0 0;
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: linear-gradient(3deg, rgba(114, 229, 255, 0) 14%, rgba(114, 229, 255, 0.2) 33%, rgba(114, 229, 255, 0.4) 48%, #72E5FF 62%, #3B74EE 100%);
+ }
+
+ .page_bg1 {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/invite/invite.vue b/ledApp/pages/invite/invite.vue
new file mode 100644
index 0000000..cbd2614
--- /dev/null
+++ b/ledApp/pages/invite/invite.vue
@@ -0,0 +1,96 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="邀请好友" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="invite">
+ <view class="name">
+ app下载链接
+ </view>
+ <view class="link">
+ {{appDown}}
+ </view>
+ <view class="btn">
+ <uv-button type="primary" text="复制" :plain="true" size="small" @click="copyText"></uv-button>
+ </view>
+ </view>
+
+
+ </view>
+</template>
+
+<script>
+ import {
+ appDownload
+ } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ appDown: ''
+ }
+ },
+ onLoad() {
+ this.getAppDownload()
+ },
+ methods: {
+ getAppDownload() {
+ appDownload().then(res => {
+ console.log(res)
+ this.appDown = res.msg
+ })
+ },
+ copyText() {
+ uni.setClipboardData({
+ data: this.appDown,
+ success: () => {
+ uni.showToast({
+ title: '复制成功',
+ icon: 'none'
+ })
+ }
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .invite {
+ margin: 30rpx;
+ background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67, 147, 248, 0.1), -8rpx -8rpx 23rpx 0rpx rgba(255, 255, 255, 0.4);
+ border-radius: 20rpx;
+ border: 2rpx solid #FFFFFF;
+ box-sizing: border-box;
+ padding: 30rpx;
+ position: relative;
+
+ .name {
+ font-weight: 500;
+ font-size: 27rpx;
+ color: rgba(0, 0, 0, 0.9);
+ line-height: 46rpx;
+ }
+
+ .link {
+ font-size: 27rpx;
+ color: rgba(0, 0, 0, 0.7);
+ line-height: 46rpx;
+ width: 500%;
+ margin-top: 20rpx;
+ }
+
+ .btn {
+ position: absolute;
+ right: 30rpx;
+ bottom: 30rpx;
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/login/login.vue b/ledApp/pages/login/login.vue
new file mode 100644
index 0000000..785824e
--- /dev/null
+++ b/ledApp/pages/login/login.vue
@@ -0,0 +1,248 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title=" " :placeholder="true" leftIcon="" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="login_logo">
+ <image class="ph" src="/static/img/logo.png" mode="widthFix"></image>
+ <view class="name">
+ 欢迎,请登录
+ </view>
+ </view>
+
+ <view class="register">
+ <view class="item">
+ <uv-input placeholder="请输入手机号/邮箱" border="none" v-model="userName"></uv-input>
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入密码" :password="isEye" border="none" v-model="password"></uv-input>
+ <view class="eye" @click="isEye = !isEye">
+ <uv-icon v-if="isEye" name="eye-off" color="#666" size="40rpx"></uv-icon>
+ <uv-icon v-else name="eye" color="#666" size="40rpx"></uv-icon>
+ </view>
+ </view>
+ <view class="item_login">
+ <navigator class="link" url="/pages/register/register" hover-class="none">
+ 还没有账号?去注册
+ </navigator>
+ </view>
+ <view class="item_agree">
+ <uv-checkbox-group v-model="agree" size="28rpx" labelSize="28rpx">
+ <uv-checkbox activeColor="#268DFF" name="agree" label="我已阅读并同意" labelColor="#666"></uv-checkbox>
+ <navigator class="link" url="/pages/policy/policy?id=1" hover-class="none">《用户协议》</navigator>
+ 和
+ <navigator class="link" url="/pages/policy/policy?id=2" hover-class="none">《隐私政策》</navigator>
+ </uv-checkbox-group>
+ </view>
+ <view class="item_btn">
+ <uv-button text="登录" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" size="large" @click="clickLogin()"></uv-button>
+ </view>
+ </view>
+
+ <!-- 协议同意弹出框 -->
+ <uv-popup ref="popupAgree" round="15" :safeAreaInsetBottom="false">
+ <view class="popup_agree">
+ <view class="title">
+ 请阅读并同意以下条款
+ </view>
+ <view class="text">
+ 我已阅读并同意
+ <navigator class="link" url="/myPages/agreement/agreement" hover-class="none">《服务协议》</navigator>和
+ <navigator class="link" url="/myPages/privacy/privacy" hover-class="none">《隐私政策》</navigator>
+ </view>
+ <view class="btn">
+ <view class="btn_item">
+ <uv-button type="primary" shape="circle" text="取消" :plain="true" :custom-style="customStyle" @click="clickAgreeClose"></uv-button>
+ </view>
+ <view class="btn_item">
+ <uv-button type="primary" shape="circle" text="同意" color="#268DFF" @click="clickAgreeOk"></uv-button>
+ </view>
+ </view>
+ </view>
+ </uv-popup>
+
+
+ </view>
+</template>
+
+<script>
+ import { login,userInfo } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ userName: '18646195977',
+ password: '123456',
+ agree: [],
+ isEye: true
+ }
+ },
+ onLoad() {
+
+ },
+ methods: {
+ clickAgreeClose(){
+ this.$refs.popupAgree.close();
+ },
+ clickAgreeOk(){
+ this.agree = ['agree']
+ this.$refs.popupAgree.close();
+ },
+ clickLogin(){
+ if(!this.userName){
+ uni.showToast({
+ title: '请输入账号',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.password){
+ uni.showToast({
+ title: '请输入密码',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(this.agree.length == 0){
+ this.$refs.popupAgree.open();
+ return
+ }
+ uni.showLoading({
+ title: '登陆中'
+ });
+ login({
+ username: this.userName,
+ password: this.password
+ }).then(res=>{
+ console.log('登陆----',res)
+ if(res.code == 200){
+ uni.setStorageSync('token', res.token)
+ userInfo().then(res=>{
+ uni.setStorageSync('userId', res.user.userId)
+ uni.hideLoading();
+ uni.reLaunch({
+ url: '/pages/index/index'
+ });
+ })
+ }
+ else{
+ uni.hideLoading();
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ })
+ },
+ changeAgree(e){
+ //console.log(e.detail.value.length)
+ this.agree = e.detail.value.length
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .popup_agree{
+ width: 650rpx;
+ box-sizing: border-box;
+ padding:50rpx 40rpx;
+ .title{
+ text-align: center;
+ font-size: 34rpx;
+ color:#000;
+ font-weight: bold;
+ line-height: 50rpx;
+ }
+ .text{
+ color: #333;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding:30rpx 0 50rpx;
+ .link{
+ color: #3086F3;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ }
+ }
+ .btn{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .btn_item{
+ width: 47%;
+ }
+ }
+ }
+ .register{
+ padding:0 60rpx;
+ .item_agree{
+ display: flex;
+ align-items: center;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.85);
+ line-height: 30rpx;
+ margin-top: 80rpx;
+ .link{
+ font-size: 27rpx;
+ color: #3086F3;
+ }
+ .item_agree_info{
+ display: flex;
+ align-items: center;
+ }
+ }
+ .item_btn{
+ margin-top: 40rpx;
+ }
+ .item{
+ margin-top: 46rpx;
+ height: 100rpx;
+ background: #ECEFF6;
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ padding:0 20rpx;
+ }
+ .item_login{
+ padding:20rpx 0;
+ .link{
+ font-size: 27rpx;
+ color: #3086F3;
+ line-height: 40rpx;
+ }
+ }
+ }
+ .login_logo{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding-top: 100rpx;
+ .ph{
+ width: 200rpx;
+ height: 200rpx;
+ flex-shrink: 0;
+ }
+ .name{
+ font-weight: 600;
+ font-size: 38rpx;
+ color: rgba(0,0,0,0.65);
+ line-height: 50rpx;
+ margin-top: 30rpx;
+ }
+ }
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
diff --git a/ledApp/pages/my/my.vue b/ledApp/pages/my/my.vue
new file mode 100644
index 0000000..114460b
--- /dev/null
+++ b/ledApp/pages/my/my.vue
@@ -0,0 +1,176 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title=" " :placeholder="true" leftIcon="" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="my_info">
+ <image class="ph" :src="baseURL + user.avatar" mode="aspectFill"></image>
+ <view class="info">
+ <view class="name">
+ {{user.nickName}}
+ </view>
+ <view class="text">
+ 微信号: {{user.wx||'-'}}
+ </view>
+ </view>
+ </view>
+
+ <view class="my_nav">
+ <navigator class="item" url="/pages/personal/personal" hover-class="none" :render-link="false">
+ <view class="name">
+ 用户信息编辑
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="/pages/invite/invite" hover-class="none" :render-link="false">
+ <view class="name">
+ 邀请好友
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="/pages/about/about" hover-class="none" :render-link="false">
+ <view class="name">
+ 关于我们
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="/pages/feedback/feedback" hover-class="none" :render-link="false">
+ <view class="name">
+ 意见反馈
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="/pages/set/set" hover-class="none" :render-link="false">
+ <view class="name">
+ 设置
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ </view>
+
+ <view class="logout">
+ <uv-button type="primary" :hairline="true" :plain="true" text="退出登录" shape="circle" color="#999" @click="postLogout"></uv-button>
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import { userInfo,logout } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ baseURL: '',
+ user: {}
+ }
+ },
+ onLoad() {
+
+ },
+ onShow() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ this.getUserInfo()
+ },
+ methods: {
+ postLogout(){
+ uni.showModal({
+ title: '提示',
+ content: '确定退出登录吗?',
+ success: function (res) {
+ if (res.confirm) {
+ console.log('用户点击确定');
+ logout().then(res=>{
+ console.log('退出----',res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ uni.removeStorageSync('token');
+ uni.removeStorageSync('userId');
+ uni.reLaunch({
+ url: '/pages/login/login'
+ });
+ })
+ } else if (res.cancel) {
+ console.log('用户点击取消');
+ }
+ }
+ });
+
+ },
+ getUserInfo(){
+ userInfo().then(res=>{
+ console.log('用户信息----',res)
+ this.user = res.user
+ uni.setStorageSync('userId', res.user.userId)
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.logout{
+ width: 380rpx;
+ margin: 60rpx auto 0;
+}
+.my_nav{
+ margin-top: 70rpx;
+ padding:0 30rpx;
+ .item{
+ width: 100%;
+ height: 120rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ border-bottom: 1rpx solid #ddd;
+ box-sizing: border-box;
+ padding-right: 10rpx;
+ .name{
+ font-size: 30rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 46rpx;
+ }
+ }
+}
+.my_info{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding:20rpx 30rpx 0;
+ .info{
+ width: 100%;
+ .name{
+ font-weight: 500;
+ font-size: 35rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 48rpx;
+ }
+ .text{
+ font-weight: 400;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.7);
+ line-height: 46rpx;
+ margin-top: 10rpx;
+ }
+ }
+ .ph{
+ flex-shrink: 0;
+ width: 140rpx;
+ height: 140rpx;
+ border-radius: 50%;
+ border:4rpx solid #fff;
+ margin-right: 30rpx;
+ }
+}
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/pages/personal/personal.vue b/ledApp/pages/personal/personal.vue
new file mode 100644
index 0000000..772472b
--- /dev/null
+++ b/ledApp/pages/personal/personal.vue
@@ -0,0 +1,300 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="个人资料" :autoBack="true" :placeholder="true" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff" leftIconColor="#fff"></uv-navbar>
+
+ <view class="head_ph">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx" :previewFullImage="false"
+ @afterRead="afterRead" @delete="deletePic">
+ <!-- <image class="ico_head" :src="baseURL+avatar" mode="widthFix"
+ style="width: 200rpx;height: 200rpx;"></image> -->
+ </uv-upload>
+ <!-- <image class="camera" src="/static/img/ico_3.png" mode="widthFix"></image> -->
+ </view>
+
+ <view class="user_info">
+ <view class="item">
+ <view class="name">
+ 电话
+ </view>
+ <view class="info">
+ <uv-input placeholder="请输入电话" border="none" v-model="phonenumber" :clearable="true" ></uv-input>
+ </view>
+ </view>
+ <view class="item">
+ <view class="name">
+ 密码
+ </view>
+ <view class="info_eye">
+ <uv-input placeholder="请输入密码" :password="isEye" border="none" v-model="password" :clearable="true"></uv-input>
+ <view class="eye" @click="isEye = !isEye">
+ <uv-icon v-if="isEye" name="eye-off" color="#666" size="40rpx"></uv-icon>
+ <uv-icon v-else name="eye" color="#666" size="40rpx"></uv-icon>
+ </view>
+ </view>
+ </view>
+ <view class="item">
+ <view class="name">
+ 昵称
+ </view>
+ <view class="info">
+ <uv-input placeholder="请输入昵称" border="none" v-model="nickName" :clearable="true"></uv-input>
+ </view>
+ </view>
+ <view class="item">
+ <view class="name">
+ 微信号
+ </view>
+ <view class="info">
+ <uv-input placeholder="请输入微信号" border="none" v-model="wx" :clearable="true"></uv-input>
+ </view>
+ </view>
+
+ </view>
+
+ <view class="footer_btn">
+ <uv-button text="确认修改" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" size="large" @click="postUserProfile()"></uv-button>
+ </view>
+
+
+
+ </view>
+</template>
+
+<script>
+ import { userInfo, userProfile } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ userId: '',
+ userName: '',
+ password: '',
+ nickName: '',
+ wx: '',
+ avatar: '',
+ phonenumber: '',
+ fileList1: [],
+ baseURL: '',
+ agree: 0,
+ isEye: true
+ }
+ },
+ onLoad() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ this.getUserInfo()
+ },
+ methods: {
+ postUserProfile(){
+ if(this.fileList1.length == 0){
+ uni.showToast({
+ title: '请上传头像',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ // if(!this.avatar){
+ // uni.showToast({
+ // title: '请上传头像',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // return
+ // }
+ if(!this.phonenumber){
+ uni.showToast({
+ title: '请输入手机号码',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if (!/^1[3-9]\d{9}$/.test(this.phonenumber)) {
+ uni.showToast({
+ title: '手机号码格式不正确',
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.password){
+ uni.showToast({
+ title: '请输入密码',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.nickName){
+ uni.showToast({
+ title: '请输入昵称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.wx){
+ uni.showToast({
+ title: '请输入微信号',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ userProfile({
+ userId: this.userId,
+ phonenumber: this.phonenumber,
+ password: this.password,
+ nickName: this.nickName,
+ wx: this.wx,
+ avatar: this.avatar
+ }).then(res=>{
+ console.log('修改用户信息---',res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ this.getUserInfo()
+ if(res.code == 200){
+ uni.navigateBack({
+ delta: 1
+ });
+ }
+ })
+ },
+ getUserInfo(){
+ userInfo().then(res=>{
+ console.log(res.user)
+ this.fileList1 = []
+ this.userId = res.user.userId
+ this.phonenumber = res.user.phonenumber
+ this.password = res.user.password
+ this.nickName = res.user.nickName
+ this.wx = res.user.wx
+ this.avatar = res.user.avatar
+ this.fileList1.push({url:this.baseURL+res.user.avatar})
+ })
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---',event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ this.avatar = result
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL+'/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----',JSON.parse(res.data).fileName)
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+::v-deep .uv-upload__wrap__preview,::v-deep .uv-upload__button{
+ margin: 0 !important;
+}
+::v-deep .uv-upload__wrap__preview__image{
+ border-radius: 50%;
+}
+.footer_btn{
+ position: fixed;
+ left:0;
+ right: 0;
+ bottom: 0;
+ padding:35rpx;
+}
+.user_info{
+ .item{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1rpx solid #ddd;
+ padding:0 35rpx;
+ height: 120rpx;
+ .name{
+ width: 140rpx;
+ flex-shrink: 0;
+ font-size: 28rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 46rpx;
+ }
+ .info{
+ width: 100%;
+ }
+ .info_eye{
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .eye{
+ flex-shrink: 0;
+ margin-left: 20rpx;
+ }
+ }
+ }
+}
+.head_ph {
+ flex-shrink: 0;
+ width: 200rpx;
+ margin: 50rpx auto;
+ position: relative;
+ .camera{
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 50rpx;
+ z-index: 9;
+ }
+ .name{
+ text-align: center;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ margin-top: 20rpx;
+ }
+ .ico_head {
+ width: 200rpx;
+ height: 200rpx;
+ border-radius: 50%;
+ }
+}
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/pages/policy/policy.vue b/ledApp/pages/policy/policy.vue
new file mode 100644
index 0000000..d6e8a2d
--- /dev/null
+++ b/ledApp/pages/policy/policy.vue
@@ -0,0 +1,63 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar :title="title" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="policy" v-if="id == 1">
+ 请仔细阅读以下条款,为获得聚合包装提供的服务,服务使用人(以下称“用户”)应当审慎阅读、充分理解各条款内容,并按照页面上的提示完成全部的注册程序。用户在进行注册程序过程中点击“同意”按钮即表示用户完全接受本协议项下的全部条款。
+ 请仔细阅读以下条款,为获得聚合包装提供的服务,服务使用人(以下称“用户”)应当审慎阅读、充分理解各条款内容,并按照页面上的提示完成全部的注册程序。用户在进行注册程序过程中点击“同意”按钮即表示用户完全接受本协议项下的全部条款。
+ </view>
+
+ <view class="policy" v-if="id == 2">
+ <uv-parse :content="infoData"></uv-parse>
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import { content } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ infoData: '',
+ id: 0,
+ title: ''
+ }
+ },
+ onLoad(opt) {
+ if(opt.id){
+ this.id = opt.id
+ const titleMap = {
+ 1: '用户协议',
+ 2: '隐私协议'
+ }
+ this.title = titleMap[this.id]
+ }
+ this.getContent()
+ },
+ methods: {
+ getContent(){
+ content(1).then(res=>{
+ console.log(res.data.content)
+ this.infoData = res.data.content
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+.policy{
+ padding:20rpx 30rpx;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ background: #fff;
+ min-height: calc(100vh - var(--window-bottom));
+}
+</style>
diff --git a/ledApp/pages/register/register.vue b/ledApp/pages/register/register.vue
new file mode 100644
index 0000000..f513087
--- /dev/null
+++ b/ledApp/pages/register/register.vue
@@ -0,0 +1,264 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title=" " :placeholder="true" :autoBack="true" bgColor="rgba(0,0,0,0)"
+ titleStyle="color:#fff"></uv-navbar>
+
+ <view class="head_ph">
+ <uv-upload :fileList="fileList1" name="1" :maxCount="1" width="200rpx" height="200rpx"
+ @afterRead="afterRead" @delete="deletePic">
+ <image class="ico_head" src="/static/img/ico_head.png" mode="widthFix"
+ style="width: 200rpx;height: 200rpx;"></image>
+ </uv-upload>
+ <view class="name">
+ 上传头像
+ </view>
+ </view>
+
+ <view class="register">
+ <view class="item">
+ <uv-input placeholder="请输入手机号/邮箱" border="none" v-model="userName"></uv-input>
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入密码" :password="isEye" border="none" v-model="password"></uv-input>
+ <view class="eye" @click="isEye = !isEye">
+ <uv-icon v-if="isEye" name="eye-off" color="#666" size="40rpx"></uv-icon>
+ <uv-icon v-else name="eye" color="#666" size="40rpx"></uv-icon>
+ </view>
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入昵称" border="none" v-model="nickName"></uv-input>
+ </view>
+ <view class="item">
+ <uv-input placeholder="请输入微信号" border="none" v-model="wx"></uv-input>
+ </view>
+
+ <view class="item_login">
+ <navigator class="link" url="/pages/login/login" hover-class="none">
+ 已有账号?去登录
+ </navigator>
+ </view>
+ <view class="item_agree">
+ <checkbox-group @change="changeAgree">
+ <label class="item_agree_info">
+ <checkbox value="agree" :checked="false" style="transform:scale(0.75)" ></checkbox>
+ 同意本应用
+ <navigator class="link" url="/pages/policy/policy?id=1" hover-class="none">《用户协议》</navigator>
+ 和
+ <navigator class="link" url="/pages/policy/policy?id=2" hover-class="none">《隐私政策》</navigator>
+ </label>
+ </checkbox-group>
+ </view>
+ <view class="item_btn">
+ <uv-button text="注册" color="linear-gradient( 142deg, #6FD2FF 0%, #268DFF 100%)" shape="circle" size="large" @click="clickRegister()"></uv-button>
+ </view>
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import { register } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ userName: '',
+ password: '',
+ nickName: '',
+ wx: '',
+ avatar: '',
+ fileList1: [],
+ baseURL: '',
+ agree: 0,
+ isEye: true
+ }
+ },
+ onLoad() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ console.log(this.baseURL)
+
+ },
+ methods: {
+ clickRegister(){
+ if(this.fileList1.length == 0){
+ uni.showToast({
+ title: '请上传头像',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.userName){
+ uni.showToast({
+ title: '请输入账号',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.password){
+ uni.showToast({
+ title: '请输入密码',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.nickName){
+ uni.showToast({
+ title: '请输入昵称',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+ if(!this.agree){
+ uni.showToast({
+ title: '请勾选用户协议和隐私政策',
+ duration: 2000,
+ icon: 'none'
+ });
+ return
+ }
+
+ register({
+ userName: this.userName,
+ password: this.password,
+ nickName: this.nickName,
+ wx: this.wx,
+ avatar: this.fileList1[0].url,
+ }).then(res=>{
+ console.log('注册----',res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if(res.code == 200){
+ uni.reLaunch({
+ url: '/pages/login/login'
+ });
+ }
+ else{
+
+ }
+ })
+ },
+ changeAgree(e){
+ //console.log(e.detail.value.length)
+ this.agree = e.detail.value.length
+ },
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ },
+ // 新增图片
+ async afterRead(event) {
+ console.log('event---',event)
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ })
+ })
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ }
+ },
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: this.baseURL+'/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ success: (res) => {
+ console.log('图片-----',JSON.parse(res.data).fileName)
+ setTimeout(() => {
+ resolve(JSON.parse(res.data).fileName)
+ }, 1000)
+ }
+ });
+ })
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+
+ .register{
+ padding:0 60rpx;
+ .item_agree{
+ display: flex;
+ align-items: center;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.85);
+ line-height: 30rpx;
+ margin-top: 80rpx;
+ .link{
+ font-size: 27rpx;
+ color: #3086F3;
+ }
+ .item_agree_info{
+ display: flex;
+ align-items: center;
+ }
+ }
+ .item_btn{
+ margin-top: 40rpx;
+ }
+ .item{
+ margin-top: 46rpx;
+ height: 100rpx;
+ background: #ECEFF6;
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ padding:0 20rpx;
+ }
+ .item_login{
+ padding:20rpx 0;
+ .link{
+ font-size: 27rpx;
+ color: #3086F3;
+ line-height: 40rpx;
+ }
+ }
+ }
+ .head_ph {
+ flex-shrink: 0;
+ width: 200rpx;
+ margin: 50rpx auto;
+ .name{
+ text-align: center;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ margin-top: 20rpx;
+ }
+ .ico_head {
+ width: 200rpx;
+ height: 200rpx;
+ border-radius: 50%;
+
+ }
+ }
+
+ .page_bg {
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background: url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+ }
+</style>
\ No newline at end of file
diff --git a/ledApp/pages/set/set.vue b/ledApp/pages/set/set.vue
new file mode 100644
index 0000000..9ebddc6
--- /dev/null
+++ b/ledApp/pages/set/set.vue
@@ -0,0 +1,129 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="设置" :autoBack="true" :placeholder="true" leftIconColor="#fff" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="set_nav">
+ <navigator class="item" url="/pages/policy/policy?id=1" hover-class="none" :render-link="false">
+ <view class="name">
+ 用户协议
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="/pages/policy/policy?id=2" hover-class="none" :render-link="false">
+ <view class="name">
+ 隐私政策
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ <navigator class="item" url="#" hover-class="none" :render-link="false">
+ <view class="name">
+ APP升级
+ </view>
+ <uv-icon name="arrow-right" color="rgba(0, 0, 0, 0.40)" size="36rpx"></uv-icon>
+ </navigator>
+ </view>
+ <view class="set_btn" @click="postUnRegistry">
+ 注销用户
+ </view>
+
+ <view class="foot_tips">
+ APP版本:V1.2
+ </view>
+
+ </view>
+</template>
+
+<script>
+ import { unRegistry } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+
+ }
+ },
+ onLoad() {
+
+ },
+ methods: {
+ postUnRegistry(){
+ uni.showModal({
+ title: '提示',
+ content: '确定注销用户吗?',
+ success: function (res) {
+ if (res.confirm) {
+ console.log('用户点击确定');
+ unRegistry().then(res=>{
+ console.log('注销----',res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ uni.removeStorageSync('token');
+ uni.removeStorageSync('userId');
+ uni.reLaunch({
+ url: '/pages/login/login'
+ });
+ })
+ } else if (res.cancel) {
+ console.log('用户点击取消');
+ }
+ }
+ });
+
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.set_btn{
+ width: 386rpx;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 96rpx;
+ height: 96rpx;
+ background: #FFFFFF;
+ border-radius: 385rpx 385rpx 385rpx 385rpx;
+ border: 2rpx solid rgba(0,0,0,0.15);
+ text-align: center;
+ margin: 60rpx auto;
+}
+.set_nav{
+ padding:0 30rpx;
+ .item{
+ height: 138rpx;
+ background: linear-gradient( 180deg, #FFFFFF 0%, rgba(255,255,255,0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67,147,248,0.1), -8rpx -8rpx 23rpx 0rpx rgba(255,255,255,0.4);
+ border-radius: 23rpx 23rpx 23rpx 23rpx;
+ border: 2rpx solid #FFFFFF;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding:0 30rpx;
+ margin-top: 24rpx;
+ .name{
+ font-weight: 500;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 46rpx;
+ }
+ }
+}
+.foot_tips{
+ position: fixed;
+ left: 0;
+ bottom: 100rpx;
+ right: 0;
+ text-align: center;
+ font-size: 30rpx;
+ color: rgba(0,0,0,0.7);
+ line-height: 38rpx;
+}
+.page_bg{
+ width: 100%;
+ min-height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/pages/template/template.vue b/ledApp/pages/template/template.vue
new file mode 100644
index 0000000..735921d
--- /dev/null
+++ b/ledApp/pages/template/template.vue
@@ -0,0 +1,183 @@
+<template>
+ <view class="page_bg">
+ <uv-navbar title="模版" :placeholder="true" leftIcon="" bgColor="rgba(0,0,0,0)" titleStyle="color:#fff"></uv-navbar>
+
+ <view class="temp_add" @click="gotoTemp">
+ <uv-icon name="plus" color="#3086F3" size="30rpx"></uv-icon>
+ <view class="name">
+ 新增动画
+ </view>
+ </view>
+
+ <view class="temp_list" v-if="listData.length > 0">
+ <view class="item" v-for="(item,index) in listData" :key="index">
+ <view class="pic" >
+ <uv-image :src="baseURL+item.pic" width="200rpx" height="150rpx" :radius="5"></uv-image>
+ </view>
+ <view class="info">
+ <view class="name">
+ {{item.name}}
+ </view>
+ <view class="text">
+ {{item.screen}}
+ </view>
+ </view>
+ <view class="btn">
+ <uv-button type="primary" :plain="true" shape="circle" text="编辑" size="small" @click="clickTempleteEdit(item.id)"></uv-button>
+ <uv-button type="primary" :plain="true" shape="circle" text="删除" size="small" custom-style="marginTop:20rpx" @click="clickTempleteDel(item.id)"></uv-button>
+ </view>
+ <!-- <view class="btn">
+ <uv-button type="primary" shape="circle" text="编辑" size="small" @click="gotoTemp"></uv-button>
+ </view> -->
+ </view>
+ </view>
+ <uv-empty v-if="listData.length == 0" mode="list" marginTop="300rpx" iconColor="#eee" textColor="#ddd"></uv-empty>
+
+
+ </view>
+</template>
+
+<script>
+ import { templeteList,templeteDel } from '@/api/api.js'
+ export default {
+ data() {
+ return {
+ userId: '',
+ listData: [],
+ baseURL: '',
+ }
+ },
+ onLoad() {
+ this.baseURL = uni.$uv.http.config.baseURL
+ },
+ onShow() {
+ this.userId = uni.getStorageSync('userId');
+ this.getTempleteList()
+ },
+ methods: {
+ // 编辑
+ clickTempleteEdit(id){
+ uni.navigateTo({
+ url: '/pages/addTemplate/addTemplate?id='+id
+ });
+ },
+ // 删除
+ clickTempleteDel(id){
+ uni.showModal({
+ title: '提示',
+ content: '删除后无法恢复,确定删除吗?',
+ success: (resa) => {
+ if (resa.confirm) {
+ console.log('用户点击确定');
+ templeteDel(id).then(res=>{
+ console.log(res)
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ if(res.code == 200){
+ this.getTempleteList()
+ }
+ })
+ } else if (resa.cancel) {
+ console.log('用户点击取消');
+ }
+ }
+ });
+
+ },
+ getTempleteList(){
+ templeteList({
+ userId: this.userId,
+ pageNum: 1,
+ pageSize: 99
+ }).then(res=>{
+ console.log('模版list-----------',res)
+ if(res.code == 200){
+ this.listData = res.rows
+ }
+ else{
+ uni.showToast({
+ title: res.msg,
+ duration: 2000,
+ icon: 'none'
+ });
+ }
+ })
+ },
+ gotoTemp(){
+ uni.navigateTo({
+ url: '/pages/addTemplate/addTemplate'
+ });
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+.temp_list{
+ padding:0 30rpx;
+ .item{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 160rpx;
+ background: linear-gradient( 180deg, #FFFFFF 0%, rgba(255,255,255,0.5) 100%);
+ box-shadow: 8rpx 8rpx 23rpx 0rpx rgba(67,147,248,0.1), -8rpx -8rpx 23rpx 0rpx rgba(255,255,255,0.4);
+ border-radius: 20rpx;
+ border: 2rpx solid #FFFFFF;
+ margin-bottom: 20rpx;
+ box-sizing: border-box;
+ padding:15px 30rpx;
+ .pic{
+ width: 200rpx;
+ flex-shrink: 0;
+ margin-right: 25rpx;
+ }
+ .info{
+ width: 100%;
+ .name{
+ font-weight: 500;
+ font-size: 28rpx;
+ color: rgba(0,0,0,0.9);
+ line-height: 40rpx;
+ }
+ .text{
+ font-weight: 600;
+ font-size: 27rpx;
+ color: rgba(0,0,0,0.7);
+ line-height: 40rpx;
+ margin-top: 10rpx;
+ }
+ }
+ .btn{
+ flex-shrink: 0;
+ margin-left: 20rpx;
+ }
+ }
+}
+.temp_add{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin:10rpx 30rpx 20rpx;
+ height: 90rpx;
+ background: #FFFFFF;
+ box-shadow: 0rpx 12rpx 27rpx 0rpx rgba(56,151,243,0.38);
+ border-radius: 20rpx;
+ border: 2rpx solid #3086F3;
+ .name{
+ font-size: 28rpx;
+ color: #3086F3;
+ line-height: 42rpx;
+ margin-left: 6rpx;
+ }
+}
+.page_bg{
+ width: 100%;
+ height: calc(100vh - var(--window-bottom));
+ background:url('/static/img/bg_2.jpg') no-repeat center top;
+ background-size: 100% auto;
+}
+</style>
diff --git a/ledApp/static/img/bg_1.png b/ledApp/static/img/bg_1.png
new file mode 100644
index 0000000..a7329b8
--- /dev/null
+++ b/ledApp/static/img/bg_1.png
Binary files differ
diff --git a/ledApp/static/img/bg_2.jpg b/ledApp/static/img/bg_2.jpg
new file mode 100644
index 0000000..27a3b23
--- /dev/null
+++ b/ledApp/static/img/bg_2.jpg
Binary files differ
diff --git a/ledApp/static/img/bg_2.png b/ledApp/static/img/bg_2.png
new file mode 100644
index 0000000..31da1b6
--- /dev/null
+++ b/ledApp/static/img/bg_2.png
Binary files differ
diff --git a/ledApp/static/img/bg_3.png b/ledApp/static/img/bg_3.png
new file mode 100644
index 0000000..084040b
--- /dev/null
+++ b/ledApp/static/img/bg_3.png
Binary files differ
diff --git a/ledApp/static/img/bg_4.png b/ledApp/static/img/bg_4.png
new file mode 100644
index 0000000..5eac465
--- /dev/null
+++ b/ledApp/static/img/bg_4.png
Binary files differ
diff --git a/ledApp/static/img/btn_1.png b/ledApp/static/img/btn_1.png
new file mode 100644
index 0000000..b648905
--- /dev/null
+++ b/ledApp/static/img/btn_1.png
Binary files differ
diff --git a/ledApp/static/img/ico_1.png b/ledApp/static/img/ico_1.png
new file mode 100644
index 0000000..1ce8b97
--- /dev/null
+++ b/ledApp/static/img/ico_1.png
Binary files differ
diff --git a/ledApp/static/img/ico_2.png b/ledApp/static/img/ico_2.png
new file mode 100644
index 0000000..d60a16b
--- /dev/null
+++ b/ledApp/static/img/ico_2.png
Binary files differ
diff --git a/ledApp/static/img/ico_3.png b/ledApp/static/img/ico_3.png
new file mode 100644
index 0000000..2e3203b
--- /dev/null
+++ b/ledApp/static/img/ico_3.png
Binary files differ
diff --git a/ledApp/static/img/ico_head.png b/ledApp/static/img/ico_head.png
new file mode 100644
index 0000000..f2f566a
--- /dev/null
+++ b/ledApp/static/img/ico_head.png
Binary files differ
diff --git a/ledApp/static/img/ico_m1.png b/ledApp/static/img/ico_m1.png
new file mode 100644
index 0000000..1254487
--- /dev/null
+++ b/ledApp/static/img/ico_m1.png
Binary files differ
diff --git a/ledApp/static/img/ico_m1a.png b/ledApp/static/img/ico_m1a.png
new file mode 100644
index 0000000..cdc32d8
--- /dev/null
+++ b/ledApp/static/img/ico_m1a.png
Binary files differ
diff --git a/ledApp/static/img/ico_m2.png b/ledApp/static/img/ico_m2.png
new file mode 100644
index 0000000..0c51373
--- /dev/null
+++ b/ledApp/static/img/ico_m2.png
Binary files differ
diff --git a/ledApp/static/img/ico_m2a.png b/ledApp/static/img/ico_m2a.png
new file mode 100644
index 0000000..136d888
--- /dev/null
+++ b/ledApp/static/img/ico_m2a.png
Binary files differ
diff --git a/ledApp/static/img/ico_m3.png b/ledApp/static/img/ico_m3.png
new file mode 100644
index 0000000..a6f4f2b
--- /dev/null
+++ b/ledApp/static/img/ico_m3.png
Binary files differ
diff --git a/ledApp/static/img/ico_m3a.png b/ledApp/static/img/ico_m3a.png
new file mode 100644
index 0000000..c75a5ea
--- /dev/null
+++ b/ledApp/static/img/ico_m3a.png
Binary files differ
diff --git a/ledApp/static/img/logo.png b/ledApp/static/img/logo.png
new file mode 100644
index 0000000..c3666a4
--- /dev/null
+++ b/ledApp/static/img/logo.png
Binary files differ
diff --git a/ledApp/static/img/ph_1.png b/ledApp/static/img/ph_1.png
new file mode 100644
index 0000000..27fb87d
--- /dev/null
+++ b/ledApp/static/img/ph_1.png
Binary files differ
diff --git a/ledApp/static/img/vip_head.jpg b/ledApp/static/img/vip_head.jpg
new file mode 100644
index 0000000..114ffe1
--- /dev/null
+++ b/ledApp/static/img/vip_head.jpg
Binary files differ
diff --git a/ledApp/static/logo.png b/ledApp/static/logo.png
new file mode 100644
index 0000000..b5771e2
--- /dev/null
+++ b/ledApp/static/logo.png
Binary files differ
diff --git a/ledApp/uni.promisify.adaptor.js b/ledApp/uni.promisify.adaptor.js
new file mode 100644
index 0000000..5fec4f3
--- /dev/null
+++ b/ledApp/uni.promisify.adaptor.js
@@ -0,0 +1,13 @@
+uni.addInterceptor({
+ returnValue (res) {
+ if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+ return res;
+ }
+ return new Promise((resolve, reject) => {
+ res.then((res) => {
+ if (!res) return resolve(res)
+ return res[0] ? reject(res[0]) : resolve(res[1])
+ });
+ });
+ },
+});
\ No newline at end of file
diff --git a/ledApp/uni.scss b/ledApp/uni.scss
new file mode 100644
index 0000000..b9249e9
--- /dev/null
+++ b/ledApp/uni.scss
@@ -0,0 +1,76 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;
diff --git a/ledApp/util/request.js b/ledApp/util/request.js
new file mode 100644
index 0000000..289b402
--- /dev/null
+++ b/ledApp/util/request.js
@@ -0,0 +1,70 @@
+// 此vm参数为页面的实例,可以通过它引用vuex中的变量
+export const Request = (vm)=>{
+ // 初始化请求配置
+ uni.$uv.http.setConfig((config) => {
+ /* config 为默认全局配置*/
+ config.baseURL = 'https://shoujike.shenzhenbenwo.com/prod-api'; /* 根域名 */
+ return config
+ })
+
+ // 请求拦截
+ uni.$uv.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+ // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
+ // config.data = config.data || {}
+ // // 根据custom参数中配置的是否需要token,添加对应的请求头
+ // if(config?.custom?.auth) {
+ // // 可以在此通过vm引用vuex中的变量,具体值在vm.$store.state中
+ // config.header.token = vm.$store.state.userInfo.token
+ // }
+ const token = uni.getStorageSync('token');
+ //console.log('token----',token)
+ if (token) {
+ config.header.Authorization = token; // 设置 Authorization 头部
+ }
+ return config
+ }, config => { // 可使用async await 做异步操作
+ return Promise.reject(config)
+ })
+
+ // 响应拦截
+ uni.$uv.http.interceptors.response.use((response) => { /* 对响应成功做点什么 可使用async await 做异步操作*/
+ //return response.data
+ const res = response.data
+ const token = uni.getStorageSync('token');
+ //console.log('拦截器---',res)
+ if(res.code == 401){ //判断token 过期 跳转重新登录
+ //ElMessage.error("请先登录!")
+ // uni.showToast({
+ // title: res.msg,
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ uni.removeStorageSync('token');
+ uni.showToast({
+ title: '请重新登录!',
+ duration: 2000,
+ icon: 'none'
+ });
+ uni.reLaunch({
+ url: '/pages/login/login'
+ });
+ // setTimeout(() => {
+ // //window.location.href = '/';
+
+ // }, 1000);
+ return
+ }
+ // if(response.code == 502){
+ // uni.showToast({
+ // title: '服务器错误,请稍后重试',
+ // duration: 2000,
+ // icon: 'none'
+ // });
+ // return
+ // }
+ return res;
+ }, (response) => {
+ // 对响应错误做点什么 (statusCode !== 200)
+ return Promise.reject(response)
+ })
+}
\ No newline at end of file
--
Gitblit v1.10.0