uniapp_code/pages/connect/connect.vue

1174 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<!-- 顶部导航 -->
<view class="nav-bar">
<text class="nav-title">表盘管理器</text>
</view>
<!-- 主内容区 -->
<view class="content">
<!-- 图片预览区 (圆形表盘) -->
<view class="preview-container">
<view class="preview-frame">
<image
:src="currentImageUrl"
class="preview-image"
mode="aspectFill"
v-if="currentImageUrl"
></image>
<view class="empty-preview" v-else>
<uni-icons type="image" size="60" color="#ccc"></uni-icons>
<text>请选择表盘图片</text>
</view>
</view>
<button
class="set-btn"
@click="setAsCurrentFace"
v-if="currentImageUrl"
:disabled="isSending"
>
{{ isSending ? '传输中 ' + transferProgress + '%' : '设置为当前表盘' }}
</button>
<view class="btn-row">
<button class="upload-btn" @click="chooseImage">
<uni-icons type="camera" size="24" color="#fff"></uni-icons>
<text>上传图片</text>
</button>
<button class="upload-btn gif-btn" @click="chooseGif">
<uni-icons type="videocam" size="24" color="#fff"></uni-icons>
<text>上传GIF</text>
</button>
</view>
</view>
<!-- 卡片式轮播区 -->
<view class="carousel-section">
<text class="section-title">已上传表盘</text>
<view class="carousel-container">
<!-- 卡片式轮播核心修改 -->
<swiper
class="card-swiper"
circular
previous-margin="200rpx"
next-margin="200rpx"
@change="handleCarouselChange"
>
<swiper-item v-for="(img, index) in uploadedImages" :key="index">
<view class="card-item" @click="selectImage(index)">
<view class="card-frame">
<image
:src="img.url"
class="card-image"
mode="aspectFill"
></image>
<view class="card-overlay" v-if="currentImageIndex === index">
<uni-icons type="checkmark" size="30" color="#007aff"></uni-icons>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="carousel-hint" v-if="uploadedImages.length === 0">
<text>暂无上传图片,请点击上方上传按钮添加</text>
</view>
</view>
</view>
<!-- BLE数据展示区 -->
<view class="data-section">
<text class="section-title">设备状态</text>
<view class="data-grid">
<!-- 电量信息 -->
<view class="data-card">
<view class="data-icon battery-icon">
<uni-icons type="battery" size="36" :color="batteryColor"></uni-icons>
</view>
<view class="data-info">
<text class="data-value">{{ batteryLevel }}%</text>
<text class="data-label">电量</text>
</view>
</view>
<!-- 温度信息 -->
<view class="data-card">
<view class="data-icon temp-icon">
<uni-icons type="thermometer" size="36" color="#ff7a45"></uni-icons>
</view>
<view class="data-info">
<text class="data-value">{{ temperature }}°C</text>
<text class="data-label">温度</text>
</view>
</view>
<!-- 湿度信息 -->
<view class="data-card">
<view class="data-icon humidity-icon">
<uni-icons type="water" size="36" color="#40a9ff"></uni-icons>
</view>
<view class="data-info">
<text class="data-value">{{ humidity }}%</text>
<text class="data-label">湿度</text>
</view>
</view>
<!-- 表盘状态 -->
<view class="data-card">
<view class="data-icon status-icon">
<uni-icons type="watch" size="36" color="#52c41a"></uni-icons>
</view>
<view class="data-info">
<text class="data-value">{{ faceStatusText }}</text>
<text class="data-label">表盘状态</text>
</view>
</view>
</view>
<!-- 设备连接状态 -->
<view class="connection-status">
<uni-icons
type="bluetooth"
size="24"
:color="isConnected ? '#52c41a' : '#ff4d4f'"
:animation="isScanning ? 'spin' : ''"
></uni-icons>
<view class="status-pot"
:style="{
backgroundColor:isConnected?'Green':'red'
}"
></view>
<text class="status-text">
{{ isConnected ? '已连接设备' : isScanning ? '正在搜索设备...' : '未连接设备' }}
</text>
<button
class="connect-btn"
@click="toggleConnection"
:disabled="isScanning"
>
{{ isConnected ? '断开连接' : '连接设备' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script>
// 脚本部分与原代码一致,无需修改
export default {
data() {
return {
// 图片相关
uploadedImages: [], // 已上传的图片列表
currentImageUrl: '', // 当前选中的图片
currentImageIndex: -1, // 当前选中的图片索引
imageDir: '', // 图片保存目录
// BLE设备数据
isConnected: true, // 是否连接
isScanning: false, // 是否正在扫描
batteryLevel: 0, // 电量
temperature: 0, // 温度
humidity: 0, // 湿度
faceStatus: 0, // 表盘状态 0-正常 1-更新中 2-异常
// statusPotColor:'Green',
deviceId:'',
imageServiceuuid:'',
imageWriteuuid:'',
imageEdituuid:'',
isSending: false,
transferProgress: 0,
// 模拟数据定时器
dataTimer: null
};
},
computed: {
// 表盘状态文本
faceStatusText() {
switch(this.faceStatus) {
case 0: return '正常';
case 1: return '更新中';
case 2: return '异常';
default: return '未知';
}
},
// 电池颜色(根据电量变化)
batteryColor() {
if (this.batteryLevel > 70) return '#52c41a';
if (this.batteryLevel > 30) return '#faad14';
return '#ff4d4f';
}
},
onLoad(options) {
this.deviceId = options.deviceId
this.initImageDir();
this.loadSavedImages();
this.setBleMtu();
},
onUnload() {
// 页面卸载时清理
this.stopDataSimulation();
if (this.isConnected) {
this.disconnectDevice();
}
},
methods: {
// BLE 单次写入(内部方法)
_bleWriteOnce(characteristicId, buffer, writeType) {
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.imageServiceuuid,
characteristicId: characteristicId,
value: buffer,
writeType: writeType || 'writeNoResponse',
success: resolve,
fail: reject
});
});
},
// BLE 写入封装带重试writeNoResponse 失败自动降级为 write
async bleWrite(characteristicId, buffer) {
const MAX_RETRY = 3;
for (let i = 0; i < MAX_RETRY; i++) {
try {
await this._bleWriteOnce(characteristicId, buffer, 'writeNoResponse');
return;
} catch (err) {
if (i < MAX_RETRY - 1) {
// BLE 写队列拥塞,等待后重试(退避递增)
await this.delay(20 * (i + 1));
} else {
// 最后一次尝试用 write带应答更可靠
try {
await this._bleWriteOnce(characteristicId, buffer, 'write');
return;
} catch (fallbackErr) {
throw fallbackErr;
}
}
}
}
},
// 延时
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// 读取图片文件为 ArrayBuffer
readImageAsArrayBuffer(filePath) {
return new Promise((resolve, reject) => {
console.log('readImageAsArrayBuffer 开始, 路径:', filePath);
plus.io.resolveLocalFileSystemURL(filePath, (fileEntry) => {
console.log('resolveLocalFileSystemURL 成功');
fileEntry.file((file) => {
console.log('获取File对象成功, 大小:', file.size);
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
console.log('FileReader onloadend, 有数据:', !!e.target.result);
if (e.target.result) {
resolve(e.target.result);
} else {
reject(new Error('读取文件结果为空'));
}
};
reader.onerror = (e) => {
console.error('FileReader onerror:', e);
reject(new Error('FileReader错误'));
};
reader.readAsDataURL(file);
}, (err) => {
console.error('fileEntry.file 失败:', JSON.stringify(err));
reject(err);
});
}, (err) => {
console.error('resolveLocalFileSystemURL 失败:', JSON.stringify(err));
reject(err);
});
});
},
// DataURL 转 ArrayBuffer
dataURLtoArrayBuffer(dataURL) {
const base64 = dataURL.split(',')[1];
const binary = atob(base64);
const len = binary.length;
const buffer = new ArrayBuffer(len);
const view = new Uint8Array(buffer);
for (let i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
return buffer;
},
// 图片分包传输
async writeBleImage(filename, imageArrayBuffer) {
const data = new Uint8Array(imageArrayBuffer);
const len = data.length;
console.log('开始传输图片:', filename, '大小:', len, '字节');
// 1. 发送前序帧26字节
const header = new Uint8Array(26);
header[0] = 0xFD;
for (let i = 0; i < Math.min(filename.length, 22); i++) {
header[i + 1] = filename.charCodeAt(i);
}
header[23] = (len >> 16) & 0xFF;
header[24] = (len >> 8) & 0xFF;
header[25] = len & 0xFF;
await this.bleWrite(this.imageWriteuuid, header.buffer);
await this.delay(50);
// 2. 分包发送图片数据无应答写入不等ACK
const CHUNK_SIZE = 507; // (MTU-3) - 2字节帧头 = 509 - 2
let offset = 0;
let packetNo = 0;
while (offset < len) {
const remaining = len - offset;
const chunkLen = Math.min(CHUNK_SIZE, remaining);
const isEnd = (offset + chunkLen >= len) ? 0x01 : 0x00;
const packet = new Uint8Array(2 + chunkLen);
packet[0] = packetNo & 0xFF;
packet[1] = isEnd;
packet.set(data.slice(offset, offset + chunkLen), 2);
await this.bleWrite(this.imageWriteuuid, packet.buffer);
// 每包间隔5ms平衡传输速度与BLE控制器队列容量
await this.delay(5);
offset += chunkLen;
packetNo++;
// 每10包更新一次进度减少UI刷新开销
if (packetNo % 10 === 0 || isEnd) {
this.transferProgress = Math.floor(offset / len * 100);
uni.showLoading({ title: '传输中 ' + this.transferProgress + '%', mask: true });
}
}
console.log('图片传输完成,共', packetNo, '包');
},
// 通过 16-bit UUID 在列表中查找(兼容多种格式)
findByUuid16(list, uuid16) {
const hex4 = uuid16.toString(16).padStart(4, '0').toLowerCase();
const fullTarget = '0000' + hex4 + '-0000-1000-8000-00805f9b34fb';
return list.find(item => {
const uuid = item.uuid.toLowerCase();
// 完整 128-bit 格式匹配
if (uuid === fullTarget) return true;
// 短格式匹配(如 "0b00" 或 "00000b00"
if (uuid === hex4 || uuid === '0000' + hex4) return true;
// 包含 16-bit 值的 128-bit 格式不同基础UUID
if (uuid.startsWith('0000' + hex4 + '-')) return true;
return false;
});
},
getBleService(){
var that = this;
uni.getBLEDeviceServices({
deviceId: that.deviceId,
success(res) {
// 打印所有服务UUID便于调试格式问题
console.log('发现服务数量:', res.services.length);
res.services.forEach((s, i) => {
console.log(' 服务[' + i + ']:', s.uuid);
});
const service = that.findByUuid16(res.services, 0x0B00);
if (service) {
that.imageServiceuuid = service.uuid;
console.log('图片服务已找到:', service.uuid);
that.getBleChar();
} else {
console.error('未找到图片传输服务 0x0B00请检查上方打印的UUID格式');
uni.showToast({ title: '未找到图片服务', icon: 'none' });
}
},
fail() {
console.log('获取服务Id失败');
}
});
},
getBleChar(){
var that = this;
uni.getBLEDeviceCharacteristics({
deviceId: that.deviceId,
serviceId: that.imageServiceuuid,
success(res) {
console.log('发现特征数量:', res.characteristics.length);
res.characteristics.forEach((c, i) => {
console.log(' 特征[' + i + ']:', c.uuid, '属性:', JSON.stringify(c.properties));
});
const writeChar = that.findByUuid16(res.characteristics, 0x0B01);
const editChar = that.findByUuid16(res.characteristics, 0x0B02);
if (writeChar) {
that.imageWriteuuid = writeChar.uuid;
console.log('写入特征已找到:', writeChar.uuid);
}
if (editChar) {
that.imageEdituuid = editChar.uuid;
console.log('编辑特征已找到:', editChar.uuid);
}
if (!writeChar || !editChar) {
console.error('特征发现不完整, write:', !!writeChar, 'edit:', !!editChar);
}
},
fail() {
console.log('获取特征Id失败');
}
});
},
setBleMtu(){
var that = this
uni.setBLEMTU({
deviceId:that.deviceId,
mtu:512,
success() {
that.isConnected = true
// that.statusPotColor = 'Green'
console.log('MTU设置成功')
that.getBleService();
},
fail() {
console.log('MTU设置失败')
}
})
},
// 初始化图片保存目录
initImageDir() {
// #ifdef APP-PLUS
// 获取应用私有目录
const docPath = plus.io.convertLocalFileSystemURL('_doc/');
this.imageDir = docPath + 'watch_faces/';
plus.io.resolveLocalFileSystemURL(this.imageDir,
() => {},
() => {
plus.io.resolveLocalFileSystemURL('_doc/', (root) => {
root.getDirectory('watch_faces', { create: true }, () => {
console.log('目录创建成功');
});
});
}
);
// #endif
// H5和小程序使用本地存储模拟
// #ifndef APP-PLUS
this.imageDir = 'watch_faces/';
// #endif
},
loadSavedImages() {
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(this.imageDir, (dir) => {
const reader = dir.createReader();
reader.readEntries((entries) => {
const images = [];
entries.forEach((entry) => {
if (entry.isFile && this.isImageFile(entry.name)) {
images.push({
name: entry.name,
url: entry.toLocalURL()
});
}
});
this.uploadedImages = images;
// 如果有图片,默认选中第一张
if (images.length > 0) {
this.currentImageUrl = images[0].url;
this.currentImageIndex = 0;
}
});
});
// #endif
// H5和小程序从本地存储加载
// #ifndef APP-PLUS
const savedImages = uni.getStorageSync('watch_face_images') || [];
this.uploadedImages = savedImages;
if (savedImages.length > 0) {
this.currentImageUrl = savedImages[0].url;
this.currentImageIndex = 0;
}
// #endif
},
// 判断是否为图片文件
isImageFile(filename) {
const ext = filename.toLowerCase().split('.').pop();
return ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext);
},
// 选择图片(支持 GIF 和普通图片)
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
crop: {
quality: 100,
width: 360,
height: 360,
resize: true
},
success: (res) => {
console.log(res);
const tempFilePath = res.tempFilePaths[0];
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(tempFilePath, (fileEntry) => {
fileEntry.file((file) => {
const reader = new FileReader();
reader.onloadend = (e) => {
this.imageBuffer = e.target.result;
this.log = '图片读取成功,大小:' + this.imageBuffer.byteLength + '字节';
};
reader.readAsArrayBuffer(file);
});
});
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager();
fs.readFile({
filePath: tempFilePath,
encoding: 'binary',
success: res => {
this.imageBuffer = res.data;
this.log = '图片读取成功,大小:' + this.imageBuffer.byteLength + '字节';
}
});
// #endif
this.saveImageToLocal(tempFilePath);
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
});
},
// 选择GIF动图不裁剪保留原始动画
chooseGif() {
uni.chooseImage({
count: 1,
sizeType: ['original'],
sourceType: ['album'],
success: (res) => {
console.log(res);
const tempFilePath = res.tempFilePaths[0];
// 检查是否为GIF文件
if (!tempFilePath.toLowerCase().endsWith('.gif')) {
uni.showToast({ title: '请选择GIF动图文件', icon: 'none' });
return;
}
// 检测 GIF 尺寸,非 360×360 提示用户
uni.getImageInfo({
src: tempFilePath,
success: (imgInfo) => {
console.log('GIF尺寸:', imgInfo.width, '×', imgInfo.height);
const sizeOk = (imgInfo.width === 360 && imgInfo.height === 360);
const doSave = () => {
// #ifdef APP-PLUS
// 检查文件大小SPIFFS 限制GIF 不宜超过 500KB
plus.io.resolveLocalFileSystemURL(tempFilePath, (fileEntry) => {
fileEntry.file((file) => {
if (file.size > 500 * 1024) {
uni.showModal({
title: '文件较大',
content: `GIF文件 ${(file.size / 1024).toFixed(0)}KB设备存储有限建议不超过500KB。是否继续`,
success: (modalRes) => {
if (modalRes.confirm) {
this.saveImageToLocal(tempFilePath);
}
}
});
} else {
this.saveImageToLocal(tempFilePath);
}
// 读取文件到buffer
const reader = new FileReader();
reader.onloadend = (e) => {
this.imageBuffer = e.target.result;
this.log = 'GIF读取成功大小' + this.imageBuffer.byteLength + '字节';
};
reader.readAsArrayBuffer(file);
});
});
// #endif
// #ifndef APP-PLUS
this.saveImageToLocal(tempFilePath);
// #endif
};
if (!sizeOk) {
uni.showModal({
title: 'GIF尺寸不匹配',
content: `当前GIF尺寸为 ${imgInfo.width}×${imgInfo.height},设备屏幕为 360×360。非标准尺寸的GIF播放可能不流畅且无法铺满屏幕建议使用360×360的GIF动图。是否继续上传`,
confirmText: '继续上传',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
doSave();
}
}
});
} else {
doSave();
}
},
fail: () => {
// 获取尺寸失败,仍然允许上传
console.warn('获取GIF尺寸失败跳过检测');
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(tempFilePath, (fileEntry) => {
fileEntry.file((file) => {
this.saveImageToLocal(tempFilePath);
const reader = new FileReader();
reader.onloadend = (e) => {
this.imageBuffer = e.target.result;
this.log = 'GIF读取成功大小' + this.imageBuffer.byteLength + '字节';
};
reader.readAsArrayBuffer(file);
});
});
// #endif
// #ifndef APP-PLUS
this.saveImageToLocal(tempFilePath);
// #endif
}
});
},
fail: (err) => {
console.error('选择GIF失败:', err);
uni.showToast({ title: '选择GIF失败', icon: 'none' });
}
});
},
// 保存图片到本地目录
saveImageToLocal(tempPath) {
// #ifdef APP-PLUS
// 根据文件类型保留扩展名GIF 保持 .gif其他统一 .jpg
const isGif = tempPath.toLowerCase().endsWith('.gif');
const fileName = `face_${Date.now()}.${isGif ? 'gif' : 'jpg'}`;
const targetPath = this.imageDir + fileName;
plus.io.resolveLocalFileSystemURL(tempPath, (file) => {
plus.io.resolveLocalFileSystemURL(this.imageDir, (dir) => {
file.copyTo(dir, fileName, (newFile) => {
const imageUrl = newFile.toLocalURL();
this.uploadedImages.push({
name: fileName,
url: imageUrl
});
// 保存到列表后选中新图片
this.currentImageIndex = this.uploadedImages.length - 1;
this.currentImageUrl = imageUrl;
uni.showToast({ title: '图片保存成功', icon: 'success' });
}, (err) => {
console.error('保存图片失败:', err);
uni.showToast({ title: '保存图片失败', icon: 'none' });
});
});
});
// #endif
// H5和小程序保存到本地存储
// #ifndef APP-PLUS
const isGif2 = tempPath.toLowerCase().endsWith('.gif');
const fileName = `face_${Date.now()}.${isGif2 ? 'gif' : 'jpg'}`;
this.uploadedImages.push({
name: fileName,
url: tempPath
});
// 保存到本地存储
uni.setStorageSync('watch_face_images', this.uploadedImages);
// 选中新图片
this.currentImageIndex = this.uploadedImages.length - 1;
this.currentImageUrl = tempPath;
uni.showToast({ title: '图片保存成功', icon: 'success' });
// #endif
},
// 选择轮播中的图片
selectImage(index) {
this.currentImageIndex = index;
this.currentImageUrl = this.uploadedImages[index].url;
},
// 轮播图变化时触发
handleCarouselChange(e) {
const index = e.detail.current;
this.currentImageIndex = index;
this.currentImageUrl = this.uploadedImages[index].url;
},
// 压缩图片为JPEG格式设备端只支持JPEG解码
compressToJpeg(srcPath) {
return new Promise((resolve, reject) => {
uni.compressImage({
src: srcPath,
quality: 100,
success: (res) => {
console.log('JPEG压缩完成:', res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => {
console.error('JPEG压缩失败:', err);
reject(err);
}
});
});
},
// 设置为当前表盘BLE 传输图片到设备,支持 JPEG 和 GIF
async setAsCurrentFace() {
if (this.isSending) return;
if (this.currentImageIndex < 0 || !this.imageWriteuuid) {
uni.showToast({ title: '请先选择图片并等待BLE就绪', icon: 'none' });
return;
}
const currentImage = this.uploadedImages[this.currentImageIndex];
if (!currentImage) return;
this.isSending = true;
this.faceStatus = 1;
this.transferProgress = 0;
const isGif = currentImage.name.toLowerCase().endsWith('.gif');
try {
let buffer;
let bleName;
if (isGif) {
// GIF: 直接读取原始文件,不压缩(压缩会破坏动画帧)
uni.showLoading({ title: '读取GIF...', mask: true });
const dataURL = await this.readImageAsArrayBuffer(currentImage.url);
buffer = this.dataURLtoArrayBuffer(dataURL);
bleName = currentImage.name; // 保留 .gif 后缀
console.log('GIF读取完成大小:', buffer.byteLength, '字节');
// GIF 文件较大时提示传输时间
if (buffer.byteLength > 200 * 1024) {
const sizeKB = (buffer.byteLength / 1024).toFixed(0);
uni.showLoading({ title: `传输GIF ${sizeKB}KB...`, mask: true });
}
} else {
// JPEG: 压缩后传输
uni.showLoading({ title: '压缩图片...', mask: true });
const jpegPath = await this.compressToJpeg(currentImage.url);
uni.showLoading({ title: '读取图片...', mask: true });
const dataURL = await this.readImageAsArrayBuffer(jpegPath);
buffer = this.dataURLtoArrayBuffer(dataURL);
bleName = currentImage.name.replace(/\.\w+$/, '.jpg');
console.log('JPEG读取完成大小:', buffer.byteLength, '字节');
}
// BLE 分包传输
await this.writeBleImage(bleName, buffer);
uni.hideLoading();
this.faceStatus = 0;
uni.showToast({ title: '表盘已更新', icon: 'success' });
uni.setStorageSync('current_watch_face', this.currentImageUrl);
} catch (err) {
uni.hideLoading();
this.faceStatus = 2;
console.error('传输失败:', err);
uni.showToast({ title: '传输失败: ' + (err.errMsg || err.message || err), icon: 'none' });
setTimeout(() => { this.faceStatus = 0; }, 3000);
} finally {
this.isSending = false;
}
},
// 切换设备连接状态
toggleConnection() {
if (this.isConnected) {
this.disconnectDevice();
} else {
this.connectDevice();
}
},
// 连接设备
connectDevice() {
var that = this;
this.isScanning = true;
uni.showToast({ title: '正在连接设备...', icon: 'none' });
uni.createBLEConnection({
deviceId:that.deviceId,
success() {
that.isScanning = false;
that.isConnected = true;
// that.statusPotColor = 'Green';
uni.showToast({ title: '设备连接成功', icon: 'success' });
that.setBleMtu();
that.startDataSimulation();
}
})
},
// 断开连接
disconnectDevice() {
var that = this;
uni.closeBLEConnection({
deviceId:that.deviceId,
success() {
that.isConnected = false;
that.statusPotColor = 'red';
uni.showToast({ title: '已断开连接', icon: 'none' });
that.stopDataSimulation();
},
fail(res) {
if(res == 10000){
uni.openBluetoothAdapter({
success() {
that.isConnected = false;
},
fail() {
console.log('初始化失败')
}
})
}
}
})
},
// 开始模拟数据更新
startDataSimulation() {
// 清除之前的定时器
if (this.dataTimer) {
clearInterval(this.dataTimer);
}
// 初始化随机数据
this.batteryLevel = Math.floor(Math.random() * 30) + 70; // 70-100%
this.temperature = Math.floor(Math.random() * 10) + 20; // 20-30°C
this.humidity = Math.floor(Math.random() * 30) + 40; // 40-70%
// 每5秒更新一次数据
this.dataTimer = setInterval(() => {
// 电量缓慢减少
if (this.batteryLevel > 1) {
this.batteryLevel = Math.max(1, this.batteryLevel - Math.random() * 2);
}
// 温度小幅波动
this.temperature = Math.max(15, Math.min(35, this.temperature + (Math.random() * 2 - 1)));
// 湿度小幅波动
this.humidity = Math.max(30, Math.min(80, this.humidity + (Math.random() * 4 - 2)));
}, 5000);
},
// 停止数据模拟
stopDataSimulation() {
if (this.dataTimer) {
clearInterval(this.dataTimer);
this.dataTimer = null;
}
}
}
};
</script>
<style scoped>
.status-pot{
width: 16px;
height: 16px;
border-radius: 50%;
}
.container {
background-color: #f5f5f7;
min-height: 100vh;
}
/* 导航栏 */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background-color: #ffffff;
}
.nav-title {
color: white;
font-size: 36rpx;
font-weight: 500;
}
.btn-row {
display: flex;
justify-content: center;
gap: 20rpx;
margin-top: 10px;
}
.upload-btn {
align-items: center;
gap: 8rpx;
background-color: #28d50e;
color: white;
width: 35%;
margin: 0;
padding: 14rpx 0rpx;
border-radius: 8rpx;
font-size: 26rpx;
}
.gif-btn {
background-color: #7c3aed;
}
/* 内容区 */
.content {
padding: 30rpx;
}
.section-title {
display: block;
font-size: 32rpx;
color: #333;
margin: 30rpx 0 20rpx;
font-weight: 500;
}
/* 图片预览区 */
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20rpx;
}
.preview-frame {
width: 480rpx;
height: 480rpx;
border-radius: 50%;
background-color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
.preview-image {
width: 100%;
height: 100%;
}
.empty-preview {
display: flex;
flex-direction: column;
align-items: center;
color: #ccc;
}
.empty-preview text {
margin-top: 20rpx;
font-size: 28rpx;
}
.set-btn {
margin-top: 20rpx;
background-color: #3cbb19;
color: white;
width: 35%;
padding: 15rpx 0rpx;
border-radius: 8rpx;
font-size: 28rpx;
}
/* 卡片式轮播区 */
.carousel-section {
margin-bottom: 30rpx;
}
.carousel-container {
background-color: white;
border-radius: 16rpx;
padding: 20rpx 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
}
/* 卡片轮播核心样式 */
.card-swiper {
width: 100%;
height: 240rpx;
}
.card-swiper .swiper-item {
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
/* 卡片样式 */
.card-item {
width: 240rpx;
height: 240rpx;
}
.card-frame {
width: 240rpx;
height: 100%;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.15);
position: relative;
}
.card-image {
width: 240rpx;
height: 100%;
}
/* 选中状态覆盖层 */
.card-overlay {
position: absolute;
top: 0;
left: 0;
width: 240rpx;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
/* 卡片轮播动画效果 */
.card-swiper .swiper-item:not(.swiper-item-active) {
transform: scale(0.8);
opacity: 0.6;
z-index: 1;
}
.card-swiper .swiper-item-active {
transform: scale(1);
z-index: 2;
}
.carousel-hint {
height: 200rpx;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 26rpx;
text-align: center;
}
/* 数据展示区 */
.data-section {
background-color: white;
border-radius: 16rpx;
padding: 20rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.data-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
margin-bottom: 30rpx;
}
.data-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
}
.data-icon {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
justify-content: center;
align-items: center;
}
.battery-icon {
background-color: #f6ffed;
}
.temp-icon {
background-color: #fff7e6;
}
.humidity-icon {
background-color: #e6f7ff;
}
.status-icon {
background-color: #f0f9ff;
}
.data-info {
flex: 1;
}
.data-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.data-label {
font-size: 24rpx;
color: #666;
}
/* 连接状态 */
.connection-status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
border-top: 1rpx solid #f0f0f0;
}
.status-text {
flex: 1;
margin: 0 15rpx;
font-size: 28rpx;
color: #333;
}
.connect-btn {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 26rpx;
background-color: #007aff;
color: white;
}
.connect-btn:disabled {
background-color: #ccc;
}
</style>