lefengyang
2026-08-09 c71e806d1966a83aad55880c8a456358182a82be
uniapp
50 files added
11442 ■■■■■ changed files
ledApp/App.vue 39 ●●●●● patch | view | raw | blame | history
ledApp/api/api.js 60 ●●●●● patch | view | raw | blame | history
ledApp/index.html 20 ●●●●● patch | view | raw | blame | history
ledApp/main.js 30 ●●●●● patch | view | raw | blame | history
ledApp/manifest.json 146 ●●●●● patch | view | raw | blame | history
ledApp/pages.json 122 ●●●●● patch | view | raw | blame | history
ledApp/pages/about/about.vue 59 ●●●●● patch | view | raw | blame | history
ledApp/pages/addDevice/addDevice.vue 1622 ●●●●● patch | view | raw | blame | history
ledApp/pages/addDevice/addDevice0.vue 487 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate copy 1.vue 548 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate copy 2.vue 548 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate copy 3.vue 1056 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate copy 4.vue 1056 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate copy.vue 348 ●●●●● patch | view | raw | blame | history
ledApp/pages/addTemplate/addTemplate.vue 1062 ●●●●● patch | view | raw | blame | history
ledApp/pages/animation/animation copy.vue 533 ●●●●● patch | view | raw | blame | history
ledApp/pages/animation/animation.vue 624 ●●●●● patch | view | raw | blame | history
ledApp/pages/feedback/feedback.vue 111 ●●●●● patch | view | raw | blame | history
ledApp/pages/index/index.vue 1353 ●●●●● patch | view | raw | blame | history
ledApp/pages/invite/invite.vue 96 ●●●●● patch | view | raw | blame | history
ledApp/pages/login/login.vue 248 ●●●●● patch | view | raw | blame | history
ledApp/pages/my/my.vue 176 ●●●●● patch | view | raw | blame | history
ledApp/pages/personal/personal.vue 300 ●●●●● patch | view | raw | blame | history
ledApp/pages/policy/policy.vue 63 ●●●●● patch | view | raw | blame | history
ledApp/pages/register/register.vue 264 ●●●●● patch | view | raw | blame | history
ledApp/pages/set/set.vue 129 ●●●●● patch | view | raw | blame | history
ledApp/pages/template/template.vue 183 ●●●●● patch | view | raw | blame | history
ledApp/static/img/bg_1.png patch | view | raw | blame | history
ledApp/static/img/bg_2.jpg patch | view | raw | blame | history
ledApp/static/img/bg_2.png patch | view | raw | blame | history
ledApp/static/img/bg_3.png patch | view | raw | blame | history
ledApp/static/img/bg_4.png patch | view | raw | blame | history
ledApp/static/img/btn_1.png patch | view | raw | blame | history
ledApp/static/img/ico_1.png patch | view | raw | blame | history
ledApp/static/img/ico_2.png patch | view | raw | blame | history
ledApp/static/img/ico_3.png patch | view | raw | blame | history
ledApp/static/img/ico_head.png patch | view | raw | blame | history
ledApp/static/img/ico_m1.png patch | view | raw | blame | history
ledApp/static/img/ico_m1a.png patch | view | raw | blame | history
ledApp/static/img/ico_m2.png patch | view | raw | blame | history
ledApp/static/img/ico_m2a.png patch | view | raw | blame | history
ledApp/static/img/ico_m3.png patch | view | raw | blame | history
ledApp/static/img/ico_m3a.png patch | view | raw | blame | history
ledApp/static/img/logo.png patch | view | raw | blame | history
ledApp/static/img/ph_1.png patch | view | raw | blame | history
ledApp/static/img/vip_head.jpg patch | view | raw | blame | history
ledApp/static/logo.png patch | view | raw | blame | history
ledApp/uni.promisify.adaptor.js 13 ●●●●● patch | view | raw | blame | history
ledApp/uni.scss 76 ●●●●● patch | view | raw | blame | history
ledApp/util/request.js 70 ●●●●● patch | view | raw | blame | history
ledApp/App.vue
New file
@@ -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>
ledApp/api/api.js
New file
@@ -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);
ledApp/index.html
New file
@@ -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>
ledApp/main.js
New file
@@ -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
ledApp/manifest.json
New file
@@ -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"
}
ledApp/pages.json
New file
@@ -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": {}
}
ledApp/pages/about/about.vue
New file
@@ -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>
ledApp/pages/addDevice/addDevice.vue
New file
@@ -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>
ledApp/pages/addDevice/addDevice0.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate copy 1.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate copy 2.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate copy 3.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate copy 4.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate copy.vue
New file
@@ -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>
ledApp/pages/addTemplate/addTemplate.vue
New file
@@ -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>
ledApp/pages/animation/animation copy.vue
New file
@@ -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>
ledApp/pages/animation/animation.vue
New file
@@ -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>
ledApp/pages/feedback/feedback.vue
New file
@@ -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>
ledApp/pages/index/index.vue
New file
@@ -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>
ledApp/pages/invite/invite.vue
New file
@@ -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>
ledApp/pages/login/login.vue
New file
@@ -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>
ledApp/pages/my/my.vue
New file
@@ -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>
ledApp/pages/personal/personal.vue
New file
@@ -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>
ledApp/pages/policy/policy.vue
New file
@@ -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>
ledApp/pages/register/register.vue
New file
@@ -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>
ledApp/pages/set/set.vue
New file
@@ -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>
ledApp/pages/template/template.vue
New file
@@ -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>
ledApp/static/img/bg_1.png
ledApp/static/img/bg_2.jpg
ledApp/static/img/bg_2.png
ledApp/static/img/bg_3.png
ledApp/static/img/bg_4.png
ledApp/static/img/btn_1.png
ledApp/static/img/ico_1.png
ledApp/static/img/ico_2.png
ledApp/static/img/ico_3.png
ledApp/static/img/ico_head.png
ledApp/static/img/ico_m1.png
ledApp/static/img/ico_m1a.png
ledApp/static/img/ico_m2.png
ledApp/static/img/ico_m2a.png
ledApp/static/img/ico_m3.png
ledApp/static/img/ico_m3a.png
ledApp/static/img/logo.png
ledApp/static/img/ph_1.png
ledApp/static/img/vip_head.jpg
ledApp/static/logo.png
ledApp/uni.promisify.adaptor.js
New file
@@ -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])
      });
    });
  },
});
ledApp/uni.scss
New file
@@ -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;
ledApp/util/request.js
New file
@@ -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)
    })
}