1170 lines
44 KiB
JavaScript
1170 lines
44 KiB
JavaScript
if (typeof Promise !== "undefined" && !Promise.prototype.finally) {
|
||
Promise.prototype.finally = function(callback) {
|
||
const promise = this.constructor;
|
||
return this.then(
|
||
(value) => promise.resolve(callback()).then(() => value),
|
||
(reason) => promise.resolve(callback()).then(() => {
|
||
throw reason;
|
||
})
|
||
);
|
||
};
|
||
}
|
||
;
|
||
if (typeof uni !== "undefined" && uni && uni.requireGlobal) {
|
||
const global = uni.requireGlobal();
|
||
ArrayBuffer = global.ArrayBuffer;
|
||
Int8Array = global.Int8Array;
|
||
Uint8Array = global.Uint8Array;
|
||
Uint8ClampedArray = global.Uint8ClampedArray;
|
||
Int16Array = global.Int16Array;
|
||
Uint16Array = global.Uint16Array;
|
||
Int32Array = global.Int32Array;
|
||
Uint32Array = global.Uint32Array;
|
||
Float32Array = global.Float32Array;
|
||
Float64Array = global.Float64Array;
|
||
BigInt64Array = global.BigInt64Array;
|
||
BigUint64Array = global.BigUint64Array;
|
||
}
|
||
;
|
||
if (uni.restoreGlobal) {
|
||
uni.restoreGlobal(Vue, weex, plus, setTimeout, clearTimeout, setInterval, clearInterval);
|
||
}
|
||
(function(vue) {
|
||
"use strict";
|
||
function formatAppLog(type, filename, ...args) {
|
||
if (uni.__log__) {
|
||
uni.__log__(type, filename, ...args);
|
||
} else {
|
||
console[type].apply(console, [...args, filename]);
|
||
}
|
||
}
|
||
const _export_sfc = (sfc, props) => {
|
||
const target = sfc.__vccOpts || sfc;
|
||
for (const [key, val] of props) {
|
||
target[key] = val;
|
||
}
|
||
return target;
|
||
};
|
||
const _sfc_main$2 = {
|
||
data() {
|
||
return {
|
||
foundDevices: [],
|
||
isScanning: false,
|
||
bluetoothEnabled: false,
|
||
connectedDeviceId: ""
|
||
};
|
||
},
|
||
onLoad() {
|
||
this.initBluetooth();
|
||
},
|
||
onUnload() {
|
||
this.stopScan();
|
||
uni.closeBluetoothAdapter();
|
||
uni.offBluetoothDeviceFound(this.onDeviceFound);
|
||
},
|
||
methods: {
|
||
// 初始化蓝牙适配器
|
||
initBluetooth() {
|
||
uni.openBluetoothAdapter({
|
||
success: () => {
|
||
this.bluetoothEnabled = true;
|
||
},
|
||
fail: (err) => {
|
||
this.bluetoothEnabled = false;
|
||
uni.showModal({
|
||
title: "蓝牙开启失败",
|
||
content: "请检查设备蓝牙是否开启",
|
||
showCancel: false
|
||
});
|
||
formatAppLog("error", "at pages/index/index.vue:88", "蓝牙初始化失败:", err);
|
||
}
|
||
});
|
||
},
|
||
// 切换扫描状态(开始/停止)
|
||
toggleScan() {
|
||
if (!this.bluetoothEnabled) {
|
||
this.initBluetooth();
|
||
return;
|
||
}
|
||
if (this.isScanning) {
|
||
this.stopScan();
|
||
} else {
|
||
this.startScan();
|
||
}
|
||
},
|
||
// 开始扫描设备
|
||
startScan() {
|
||
this.isScanning = true;
|
||
this.foundDevices = [];
|
||
uni.startBluetoothDevicesDiscovery({
|
||
services: [],
|
||
allowDuplicatesKey: false,
|
||
// 不允许重复上报
|
||
success: () => {
|
||
uni.showToast({ title: "开始扫描", icon: "none" });
|
||
uni.onBluetoothDeviceFound(this.onDeviceFound);
|
||
},
|
||
fail: (err) => {
|
||
this.isScanning = false;
|
||
uni.showToast({ title: "扫描失败", icon: "none" });
|
||
formatAppLog("error", "at pages/index/index.vue:124", "扫描失败:", err);
|
||
}
|
||
});
|
||
setTimeout(() => {
|
||
if (this.isScanning)
|
||
this.stopScan();
|
||
}, 5e3);
|
||
},
|
||
// 停止扫描
|
||
stopScan() {
|
||
if (!this.isScanning)
|
||
return;
|
||
uni.stopBluetoothDevicesDiscovery({
|
||
success: () => {
|
||
this.isScanning = false;
|
||
if (this.foundDevices.length == 0) {
|
||
uni.showToast({
|
||
title: `暂未扫描到任何设备`,
|
||
icon: "none"
|
||
});
|
||
return;
|
||
}
|
||
uni.showToast({
|
||
title: `扫描完成,发现${this.foundDevices.length}台设备`,
|
||
icon: "none"
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
formatAppLog("error", "at pages/index/index.vue:154", "停止扫描失败:", err);
|
||
}
|
||
});
|
||
},
|
||
// 设备发现回调(处理去重和数据格式化)
|
||
onDeviceFound(res) {
|
||
const devices = res.devices || [];
|
||
devices.forEach((device) => {
|
||
if (!device.deviceId)
|
||
return;
|
||
const isExist = this.foundDevices.some(
|
||
(d) => d.deviceId === device.deviceId
|
||
);
|
||
if (isExist)
|
||
return;
|
||
var is_bj = false;
|
||
const advData = new Uint8Array(device.advertisData);
|
||
const devicenameData = advData.slice(2, 6);
|
||
const devicename = String.fromCharCode(...devicenameData);
|
||
if (devicename == "dzbj")
|
||
is_bj = true;
|
||
this.foundDevices.push({
|
||
is_bj,
|
||
name: device.name || "未知设备",
|
||
deviceId: device.deviceId,
|
||
rssi: device.RSSI,
|
||
connected: device.deviceId === this.connectedDeviceId
|
||
});
|
||
});
|
||
},
|
||
// 计算信号强度条宽度(-100dBm为0%,-30dBm为100%)
|
||
getRssiWidth(rssi) {
|
||
const minRssi = -100;
|
||
const maxRssi = -30;
|
||
let ratio = (rssi - minRssi) / (maxRssi - minRssi);
|
||
ratio = Math.max(0, Math.min(1, ratio));
|
||
return `${ratio * 100}%`;
|
||
},
|
||
connectDevice(device) {
|
||
var that = this;
|
||
this.stopScan();
|
||
uni.showLoading({ title: "连接中..." });
|
||
uni.createBLEConnection({
|
||
deviceId: device.deviceId,
|
||
success(res) {
|
||
that.foundDevices = that.foundDevices.map((d) => ({
|
||
...d,
|
||
connected: d.deviceId === device.deviceId
|
||
}));
|
||
uni.hideLoading();
|
||
uni.showToast({ title: `已连接${device.name}`, icon: "none" });
|
||
uni.navigateTo({
|
||
url: "../connect/connect?deviceId=" + device.deviceId,
|
||
fail: (err) => {
|
||
uni.showToast({ title: `连接失败,请稍后重试`, icon: "error" });
|
||
uni.closeBLEConnection({
|
||
deviceId: device.deviceId,
|
||
success() {
|
||
console("断开连接成功");
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
fail() {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: `连接失败`, icon: "error" });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
||
return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
|
||
vue.createElementVNode("view", { class: "title-bar" }, [
|
||
vue.createElementVNode("text", { class: "title" }, "连接设备"),
|
||
vue.createElementVNode("button", {
|
||
class: "scan-btn",
|
||
disabled: $data.isScanning,
|
||
onClick: _cache[0] || (_cache[0] = (...args) => $options.toggleScan && $options.toggleScan(...args))
|
||
}, vue.toDisplayString($data.isScanning ? "停止扫描" : "开始扫描"), 9, ["disabled"])
|
||
]),
|
||
$data.isScanning ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 0,
|
||
class: "status-tip"
|
||
}, [
|
||
vue.createElementVNode("text", null, "正在扫描设备..."),
|
||
vue.createElementVNode("view", { class: "loading" })
|
||
])) : $data.foundDevices.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 1,
|
||
class: "status-tip"
|
||
}, [
|
||
vue.createElementVNode("text", null, '未发现设备,请点击"开始扫描"')
|
||
])) : vue.createCommentVNode("v-if", true),
|
||
vue.createElementVNode("view", { class: "device-list" }, [
|
||
(vue.openBlock(true), vue.createElementBlock(
|
||
vue.Fragment,
|
||
null,
|
||
vue.renderList($data.foundDevices, (device, index) => {
|
||
return vue.openBlock(), vue.createElementBlock("view", {
|
||
class: "device-item",
|
||
key: device.deviceId,
|
||
onClick: ($event) => $options.connectDevice(device)
|
||
}, [
|
||
vue.createElementVNode("view", { class: "device-name" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
null,
|
||
vue.toDisplayString(device.name || "未知设备"),
|
||
1
|
||
/* TEXT */
|
||
),
|
||
device.is_bj ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 0,
|
||
class: "is_bj"
|
||
}, "吧唧")) : vue.createCommentVNode("v-if", true),
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "device-id" },
|
||
vue.toDisplayString(device.deviceId),
|
||
1
|
||
/* TEXT */
|
||
)
|
||
]),
|
||
vue.createElementVNode("view", { class: "device-rssi" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
null,
|
||
"信号: " + vue.toDisplayString(device.rssi) + " dBm",
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode(
|
||
"view",
|
||
{
|
||
class: "rssi-bar",
|
||
style: vue.normalizeStyle({ width: $options.getRssiWidth(device.rssi) })
|
||
},
|
||
null,
|
||
4
|
||
/* STYLE */
|
||
)
|
||
]),
|
||
device.connected ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 0,
|
||
class: "device-status"
|
||
}, [
|
||
vue.createElementVNode("text", { class: "connected" }, "已连接")
|
||
])) : vue.createCommentVNode("v-if", true)
|
||
], 8, ["onClick"]);
|
||
}),
|
||
128
|
||
/* KEYED_FRAGMENT */
|
||
))
|
||
])
|
||
]);
|
||
}
|
||
const PagesIndexIndex = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$1], ["__scopeId", "data-v-1cf27b2a"], ["__file", "/Users/rdzleo/Desktop/uniapp_code/pages/index/index.vue"]]);
|
||
const _sfc_main$1 = {
|
||
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,
|
||
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) {
|
||
await this.delay(20 * (i + 1));
|
||
} else {
|
||
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) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:266", "readImageAsArrayBuffer 开始, 路径:", filePath);
|
||
plus.io.resolveLocalFileSystemURL(filePath, (fileEntry) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:268", "resolveLocalFileSystemURL 成功");
|
||
fileEntry.file((file) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:270", "获取File对象成功, 大小:", file.size);
|
||
const reader = new plus.io.FileReader();
|
||
reader.onloadend = (e) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:273", "FileReader onloadend, 有数据:", !!e.target.result);
|
||
if (e.target.result) {
|
||
resolve(e.target.result);
|
||
} else {
|
||
reject(new Error("读取文件结果为空"));
|
||
}
|
||
};
|
||
reader.onerror = (e) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:281", "FileReader onerror:", e);
|
||
reject(new Error("FileReader错误"));
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}, (err) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:286", "fileEntry.file 失败:", JSON.stringify(err));
|
||
reject(err);
|
||
});
|
||
}, (err) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:290", "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;
|
||
formatAppLog("log", "at pages/connect/connect.vue:311", "开始传输图片:", filename, "大小:", len, "字节");
|
||
const header = new Uint8Array(26);
|
||
header[0] = 253;
|
||
for (let i = 0; i < Math.min(filename.length, 22); i++) {
|
||
header[i + 1] = filename.charCodeAt(i);
|
||
}
|
||
header[23] = len >> 16 & 255;
|
||
header[24] = len >> 8 & 255;
|
||
header[25] = len & 255;
|
||
await this.bleWrite(this.imageWriteuuid, header.buffer);
|
||
await this.delay(50);
|
||
const CHUNK_SIZE = 507;
|
||
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 ? 1 : 0;
|
||
const packet = new Uint8Array(2 + chunkLen);
|
||
packet[0] = packetNo & 255;
|
||
packet[1] = isEnd;
|
||
packet.set(data.slice(offset, offset + chunkLen), 2);
|
||
await this.bleWrite(this.imageWriteuuid, packet.buffer);
|
||
await this.delay(5);
|
||
offset += chunkLen;
|
||
packetNo++;
|
||
if (packetNo % 10 === 0 || isEnd) {
|
||
this.transferProgress = Math.floor(offset / len * 100);
|
||
uni.showLoading({ title: "传输中 " + this.transferProgress + "%", mask: true });
|
||
}
|
||
}
|
||
formatAppLog("log", "at pages/connect/connect.vue:355", "图片传输完成,共", 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();
|
||
if (uuid === fullTarget)
|
||
return true;
|
||
if (uuid === hex4 || uuid === "0000" + hex4)
|
||
return true;
|
||
if (uuid.startsWith("0000" + hex4 + "-"))
|
||
return true;
|
||
return false;
|
||
});
|
||
},
|
||
getBleService() {
|
||
var that = this;
|
||
uni.getBLEDeviceServices({
|
||
deviceId: that.deviceId,
|
||
success(res) {
|
||
formatAppLog("log", "at pages/connect/connect.vue:378", "发现服务数量:", res.services.length);
|
||
res.services.forEach((s, i) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:380", " 服务[" + i + "]:", s.uuid);
|
||
});
|
||
const service = that.findByUuid16(res.services, 2816);
|
||
if (service) {
|
||
that.imageServiceuuid = service.uuid;
|
||
formatAppLog("log", "at pages/connect/connect.vue:385", "图片服务已找到:", service.uuid);
|
||
that.getBleChar();
|
||
} else {
|
||
formatAppLog("error", "at pages/connect/connect.vue:388", "未找到图片传输服务 0x0B00,请检查上方打印的UUID格式");
|
||
uni.showToast({ title: "未找到图片服务", icon: "none" });
|
||
}
|
||
},
|
||
fail() {
|
||
formatAppLog("log", "at pages/connect/connect.vue:393", "获取服务Id失败");
|
||
}
|
||
});
|
||
},
|
||
getBleChar() {
|
||
var that = this;
|
||
uni.getBLEDeviceCharacteristics({
|
||
deviceId: that.deviceId,
|
||
serviceId: that.imageServiceuuid,
|
||
success(res) {
|
||
formatAppLog("log", "at pages/connect/connect.vue:403", "发现特征数量:", res.characteristics.length);
|
||
res.characteristics.forEach((c, i) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:405", " 特征[" + i + "]:", c.uuid, "属性:", JSON.stringify(c.properties));
|
||
});
|
||
const writeChar = that.findByUuid16(res.characteristics, 2817);
|
||
const editChar = that.findByUuid16(res.characteristics, 2818);
|
||
if (writeChar) {
|
||
that.imageWriteuuid = writeChar.uuid;
|
||
formatAppLog("log", "at pages/connect/connect.vue:411", "写入特征已找到:", writeChar.uuid);
|
||
}
|
||
if (editChar) {
|
||
that.imageEdituuid = editChar.uuid;
|
||
formatAppLog("log", "at pages/connect/connect.vue:415", "编辑特征已找到:", editChar.uuid);
|
||
}
|
||
if (!writeChar || !editChar) {
|
||
formatAppLog("error", "at pages/connect/connect.vue:418", "特征发现不完整, write:", !!writeChar, "edit:", !!editChar);
|
||
}
|
||
},
|
||
fail() {
|
||
formatAppLog("log", "at pages/connect/connect.vue:422", "获取特征Id失败");
|
||
}
|
||
});
|
||
},
|
||
setBleMtu() {
|
||
var that = this;
|
||
uni.setBLEMTU({
|
||
deviceId: that.deviceId,
|
||
mtu: 512,
|
||
success() {
|
||
that.isConnected = true;
|
||
formatAppLog("log", "at pages/connect/connect.vue:434", "MTU设置成功");
|
||
that.getBleService();
|
||
},
|
||
fail() {
|
||
formatAppLog("log", "at pages/connect/connect.vue:438", "MTU设置失败");
|
||
}
|
||
});
|
||
},
|
||
// 初始化图片保存目录
|
||
initImageDir() {
|
||
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 }, () => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:453", "目录创建成功");
|
||
});
|
||
});
|
||
}
|
||
);
|
||
},
|
||
loadSavedImages() {
|
||
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;
|
||
}
|
||
});
|
||
});
|
||
},
|
||
// 判断是否为图片文件
|
||
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) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:520", res);
|
||
const tempFilePath = res.tempFilePaths[0];
|
||
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);
|
||
});
|
||
});
|
||
this.saveImageToLocal(tempFilePath);
|
||
},
|
||
fail: (err) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:551", "选择图片失败:", err);
|
||
uni.showToast({ title: "选择图片失败", icon: "none" });
|
||
}
|
||
});
|
||
},
|
||
// 选择GIF动图(不裁剪,保留原始动画)
|
||
chooseGif() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ["original"],
|
||
sourceType: ["album"],
|
||
success: (res) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:564", res);
|
||
const tempFilePath = res.tempFilePaths[0];
|
||
if (!tempFilePath.toLowerCase().endsWith(".gif")) {
|
||
uni.showToast({ title: "请选择GIF动图文件", icon: "none" });
|
||
return;
|
||
}
|
||
uni.getImageInfo({
|
||
src: tempFilePath,
|
||
success: (imgInfo) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:577", "GIF尺寸:", imgInfo.width, "×", imgInfo.height);
|
||
const sizeOk = imgInfo.width === 360 && imgInfo.height === 360;
|
||
const doSave = () => {
|
||
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);
|
||
}
|
||
const reader = new FileReader();
|
||
reader.onloadend = (e) => {
|
||
this.imageBuffer = e.target.result;
|
||
this.log = "GIF读取成功,大小:" + this.imageBuffer.byteLength + "字节";
|
||
};
|
||
reader.readAsArrayBuffer(file);
|
||
});
|
||
});
|
||
};
|
||
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: () => {
|
||
formatAppLog("warn", "at pages/connect/connect.vue:632", "获取GIF尺寸失败,跳过检测");
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:653", "选择GIF失败:", err);
|
||
uni.showToast({ title: "选择GIF失败", icon: "none" });
|
||
}
|
||
});
|
||
},
|
||
// 保存图片到本地目录
|
||
saveImageToLocal(tempPath) {
|
||
const isGif = tempPath.toLowerCase().endsWith(".gif");
|
||
const fileName = `face_${Date.now()}.${isGif ? "gif" : "jpg"}`;
|
||
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) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:680", "保存图片失败:", err);
|
||
uni.showToast({ title: "保存图片失败", icon: "none" });
|
||
});
|
||
});
|
||
});
|
||
},
|
||
// 选择轮播中的图片
|
||
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) => {
|
||
formatAppLog("log", "at pages/connect/connect.vue:724", "JPEG压缩完成:", res.tempFilePath);
|
||
resolve(res.tempFilePath);
|
||
},
|
||
fail: (err) => {
|
||
formatAppLog("error", "at pages/connect/connect.vue:728", "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) {
|
||
uni.showLoading({ title: "读取GIF...", mask: true });
|
||
const dataURL = await this.readImageAsArrayBuffer(currentImage.url);
|
||
buffer = this.dataURLtoArrayBuffer(dataURL);
|
||
bleName = currentImage.name;
|
||
formatAppLog("log", "at pages/connect/connect.vue:762", "GIF读取完成,大小:", buffer.byteLength, "字节");
|
||
if (buffer.byteLength > 200 * 1024) {
|
||
const sizeKB = (buffer.byteLength / 1024).toFixed(0);
|
||
uni.showLoading({ title: `传输GIF ${sizeKB}KB...`, mask: true });
|
||
}
|
||
} else {
|
||
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");
|
||
formatAppLog("log", "at pages/connect/connect.vue:778", "JPEG读取完成,大小:", buffer.byteLength, "字节");
|
||
}
|
||
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;
|
||
formatAppLog("error", "at pages/connect/connect.vue:791", "传输失败:", err);
|
||
uni.showToast({ title: "传输失败: " + (err.errMsg || err.message || err), icon: "none" });
|
||
setTimeout(() => {
|
||
this.faceStatus = 0;
|
||
}, 3e3);
|
||
} 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;
|
||
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 == 1e4) {
|
||
uni.openBluetoothAdapter({
|
||
success() {
|
||
that.isConnected = false;
|
||
},
|
||
fail() {
|
||
formatAppLog("log", "at pages/connect/connect.vue:844", "初始化失败");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
// 开始模拟数据更新
|
||
startDataSimulation() {
|
||
if (this.dataTimer) {
|
||
clearInterval(this.dataTimer);
|
||
}
|
||
this.batteryLevel = Math.floor(Math.random() * 30) + 70;
|
||
this.temperature = Math.floor(Math.random() * 10) + 20;
|
||
this.humidity = Math.floor(Math.random() * 30) + 40;
|
||
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)));
|
||
}, 5e3);
|
||
},
|
||
// 停止数据模拟
|
||
stopDataSimulation() {
|
||
if (this.dataTimer) {
|
||
clearInterval(this.dataTimer);
|
||
this.dataTimer = null;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||
const _component_uni_icons = vue.resolveComponent("uni-icons");
|
||
return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
|
||
vue.createElementVNode("view", { class: "nav-bar" }, [
|
||
vue.createElementVNode("text", { class: "nav-title" }, "表盘管理器")
|
||
]),
|
||
vue.createElementVNode("view", { class: "content" }, [
|
||
vue.createElementVNode("view", { class: "preview-container" }, [
|
||
vue.createElementVNode("view", { class: "preview-frame" }, [
|
||
$data.currentImageUrl ? (vue.openBlock(), vue.createElementBlock("image", {
|
||
key: 0,
|
||
src: $data.currentImageUrl,
|
||
class: "preview-image",
|
||
mode: "aspectFill"
|
||
}, null, 8, ["src"])) : (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 1,
|
||
class: "empty-preview"
|
||
}, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "image",
|
||
size: "60",
|
||
color: "#ccc"
|
||
}),
|
||
vue.createElementVNode("text", null, "请选择表盘图片")
|
||
]))
|
||
]),
|
||
$data.currentImageUrl ? (vue.openBlock(), vue.createElementBlock("button", {
|
||
key: 0,
|
||
class: "set-btn",
|
||
onClick: _cache[0] || (_cache[0] = (...args) => $options.setAsCurrentFace && $options.setAsCurrentFace(...args)),
|
||
disabled: $data.isSending
|
||
}, vue.toDisplayString($data.isSending ? "传输中 " + $data.transferProgress + "%" : "设置为当前表盘"), 9, ["disabled"])) : vue.createCommentVNode("v-if", true),
|
||
vue.createElementVNode("view", { class: "btn-row" }, [
|
||
vue.createElementVNode("button", {
|
||
class: "upload-btn",
|
||
onClick: _cache[1] || (_cache[1] = (...args) => $options.chooseImage && $options.chooseImage(...args))
|
||
}, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "camera",
|
||
size: "24",
|
||
color: "#fff"
|
||
}),
|
||
vue.createElementVNode("text", null, "上传图片")
|
||
]),
|
||
vue.createElementVNode("button", {
|
||
class: "upload-btn gif-btn",
|
||
onClick: _cache[2] || (_cache[2] = (...args) => $options.chooseGif && $options.chooseGif(...args))
|
||
}, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "videocam",
|
||
size: "24",
|
||
color: "#fff"
|
||
}),
|
||
vue.createElementVNode("text", null, "上传GIF")
|
||
])
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "carousel-section" }, [
|
||
vue.createElementVNode("text", { class: "section-title" }, "已上传表盘"),
|
||
vue.createElementVNode("view", { class: "carousel-container" }, [
|
||
vue.createElementVNode(
|
||
"swiper",
|
||
{
|
||
class: "card-swiper",
|
||
circular: "",
|
||
"previous-margin": "200rpx",
|
||
"next-margin": "200rpx",
|
||
onChange: _cache[3] || (_cache[3] = (...args) => $options.handleCarouselChange && $options.handleCarouselChange(...args))
|
||
},
|
||
[
|
||
(vue.openBlock(true), vue.createElementBlock(
|
||
vue.Fragment,
|
||
null,
|
||
vue.renderList($data.uploadedImages, (img, index) => {
|
||
return vue.openBlock(), vue.createElementBlock("swiper-item", { key: index }, [
|
||
vue.createElementVNode("view", {
|
||
class: "card-item",
|
||
onClick: ($event) => $options.selectImage(index)
|
||
}, [
|
||
vue.createElementVNode("view", { class: "card-frame" }, [
|
||
vue.createElementVNode("image", {
|
||
src: img.url,
|
||
class: "card-image",
|
||
mode: "aspectFill"
|
||
}, null, 8, ["src"]),
|
||
$data.currentImageIndex === index ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 0,
|
||
class: "card-overlay"
|
||
}, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "checkmark",
|
||
size: "30",
|
||
color: "#007aff"
|
||
})
|
||
])) : vue.createCommentVNode("v-if", true)
|
||
])
|
||
], 8, ["onClick"])
|
||
]);
|
||
}),
|
||
128
|
||
/* KEYED_FRAGMENT */
|
||
))
|
||
],
|
||
32
|
||
/* NEED_HYDRATION */
|
||
),
|
||
$data.uploadedImages.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", {
|
||
key: 0,
|
||
class: "carousel-hint"
|
||
}, [
|
||
vue.createElementVNode("text", null, "暂无上传图片,请点击上方上传按钮添加")
|
||
])) : vue.createCommentVNode("v-if", true)
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-section" }, [
|
||
vue.createElementVNode("text", { class: "section-title" }, "设备状态"),
|
||
vue.createElementVNode("view", { class: "data-grid" }, [
|
||
vue.createElementVNode("view", { class: "data-card" }, [
|
||
vue.createElementVNode("view", { class: "data-icon battery-icon" }, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "battery",
|
||
size: "36",
|
||
color: $options.batteryColor
|
||
}, null, 8, ["color"])
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-info" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "data-value" },
|
||
vue.toDisplayString($data.batteryLevel) + "%",
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode("text", { class: "data-label" }, "电量")
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-card" }, [
|
||
vue.createElementVNode("view", { class: "data-icon temp-icon" }, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "thermometer",
|
||
size: "36",
|
||
color: "#ff7a45"
|
||
})
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-info" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "data-value" },
|
||
vue.toDisplayString($data.temperature) + "°C",
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode("text", { class: "data-label" }, "温度")
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-card" }, [
|
||
vue.createElementVNode("view", { class: "data-icon humidity-icon" }, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "water",
|
||
size: "36",
|
||
color: "#40a9ff"
|
||
})
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-info" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "data-value" },
|
||
vue.toDisplayString($data.humidity) + "%",
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode("text", { class: "data-label" }, "湿度")
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-card" }, [
|
||
vue.createElementVNode("view", { class: "data-icon status-icon" }, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "watch",
|
||
size: "36",
|
||
color: "#52c41a"
|
||
})
|
||
]),
|
||
vue.createElementVNode("view", { class: "data-info" }, [
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "data-value" },
|
||
vue.toDisplayString($options.faceStatusText),
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode("text", { class: "data-label" }, "表盘状态")
|
||
])
|
||
])
|
||
]),
|
||
vue.createElementVNode("view", { class: "connection-status" }, [
|
||
vue.createVNode(_component_uni_icons, {
|
||
type: "bluetooth",
|
||
size: "24",
|
||
color: $data.isConnected ? "#52c41a" : "#ff4d4f",
|
||
animation: $data.isScanning ? "spin" : ""
|
||
}, null, 8, ["color", "animation"]),
|
||
vue.createElementVNode(
|
||
"view",
|
||
{
|
||
class: "status-pot",
|
||
style: vue.normalizeStyle({
|
||
backgroundColor: $data.isConnected ? "Green" : "red"
|
||
})
|
||
},
|
||
null,
|
||
4
|
||
/* STYLE */
|
||
),
|
||
vue.createElementVNode(
|
||
"text",
|
||
{ class: "status-text" },
|
||
vue.toDisplayString($data.isConnected ? "已连接设备" : $data.isScanning ? "正在搜索设备..." : "未连接设备"),
|
||
1
|
||
/* TEXT */
|
||
),
|
||
vue.createElementVNode("button", {
|
||
class: "connect-btn",
|
||
onClick: _cache[4] || (_cache[4] = (...args) => $options.toggleConnection && $options.toggleConnection(...args)),
|
||
disabled: $data.isScanning
|
||
}, vue.toDisplayString($data.isConnected ? "断开连接" : "连接设备"), 9, ["disabled"])
|
||
])
|
||
])
|
||
])
|
||
]);
|
||
}
|
||
const PagesConnectConnect = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__scopeId", "data-v-ea8c7664"], ["__file", "/Users/rdzleo/Desktop/uniapp_code/pages/connect/connect.vue"]]);
|
||
__definePage("pages/index/index", PagesIndexIndex);
|
||
__definePage("pages/connect/connect", PagesConnectConnect);
|
||
const _sfc_main = {
|
||
onLaunch: function() {
|
||
formatAppLog("log", "at App.vue:7", "App Launch");
|
||
},
|
||
onShow: function() {
|
||
formatAppLog("log", "at App.vue:10", "App Show");
|
||
},
|
||
onHide: function() {
|
||
formatAppLog("log", "at App.vue:13", "App Hide");
|
||
},
|
||
onExit: function() {
|
||
formatAppLog("log", "at App.vue:34", "App Exit");
|
||
}
|
||
};
|
||
const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "/Users/rdzleo/Desktop/uniapp_code/App.vue"]]);
|
||
function createApp() {
|
||
const app = vue.createVueApp(App);
|
||
return {
|
||
app
|
||
};
|
||
}
|
||
const { app: __app__, Vuex: __Vuex__, Pinia: __Pinia__ } = createApp();
|
||
uni.Vuex = __Vuex__;
|
||
uni.Pinia = __Pinia__;
|
||
__app__.provide("__globalStyles", __uniConfig.styles);
|
||
__app__._component.mpType = "app";
|
||
__app__._component.render = () => {
|
||
};
|
||
__app__.mount("#app");
|
||
})(Vue);
|