|
|
|
|
@ -0,0 +1,995 @@
|
|
|
|
|
/**
|
|
|
|
|
* 蓝牙设备控制页面
|
|
|
|
|
* 功能:手动控制、参数设置、自动模式、报警浏览
|
|
|
|
|
*/
|
|
|
|
|
import { router } from '@kit.ArkUI';
|
|
|
|
|
import { connection, socket } from '@kit.ConnectivityKit';
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
|
|
|
|
// 路由参数接口
|
|
|
|
|
interface RouterParams {
|
|
|
|
|
deviceId: string;
|
|
|
|
|
deviceName: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 回路信息接口
|
|
|
|
|
interface LoopInfo {
|
|
|
|
|
index: number;
|
|
|
|
|
name: string;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
pwm: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tab项接口
|
|
|
|
|
interface TabItem {
|
|
|
|
|
name: string;
|
|
|
|
|
index: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载状态接口
|
|
|
|
|
interface LoadingState {
|
|
|
|
|
deviceTime: boolean;
|
|
|
|
|
deviceInfo: boolean;
|
|
|
|
|
sunRiseTime: boolean;
|
|
|
|
|
settingDeviceInfo: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Entry
|
|
|
|
|
@Component
|
|
|
|
|
struct BluetoothControlPage {
|
|
|
|
|
@State deviceId: string = '';
|
|
|
|
|
@State deviceName: string = '';
|
|
|
|
|
@State activeTab: number = 0;
|
|
|
|
|
@State isManualMode: boolean = false;
|
|
|
|
|
@State deviceTime: string = '--:--:--';
|
|
|
|
|
@State loops: LoopInfo[] = [];
|
|
|
|
|
@State currentBrightness: number = 50;
|
|
|
|
|
@State showBrightnessDialog: boolean = false;
|
|
|
|
|
@State selectedLoopIndex: number = -1;
|
|
|
|
|
@State sunriseTime: string = '--:--';
|
|
|
|
|
@State sunsetTime: string = '--:--';
|
|
|
|
|
@State isLoading: boolean = false;
|
|
|
|
|
@State toastMessage: string = '';
|
|
|
|
|
@State showToast: boolean = false;
|
|
|
|
|
@State showBackDialog: boolean = false;
|
|
|
|
|
@State isConnected: boolean = false;
|
|
|
|
|
|
|
|
|
|
// 加载状态
|
|
|
|
|
@State dataLoading: LoadingState = {
|
|
|
|
|
deviceTime: false,
|
|
|
|
|
deviceInfo: false,
|
|
|
|
|
sunRiseTime: false,
|
|
|
|
|
settingDeviceInfo: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private tabs: TabItem[] = [
|
|
|
|
|
{ name: '手动控制', index: 0 },
|
|
|
|
|
{ name: '参数设置', index: 1 },
|
|
|
|
|
{ name: '自动模式', index: 2 },
|
|
|
|
|
{ name: '报警浏览', index: 3 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// SPP UUID (串口通信)
|
|
|
|
|
private readonly SPP_UUID: string = '00001101-0000-1000-8000-00805F9B34FB';
|
|
|
|
|
private clientNumber: number = -1;
|
|
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
|
|
|
// 获取路由参数
|
|
|
|
|
const params = router.getParams() as RouterParams;
|
|
|
|
|
if (params) {
|
|
|
|
|
this.deviceId = params.deviceId || '';
|
|
|
|
|
this.deviceName = params.deviceName || '未知设备';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化10个回路
|
|
|
|
|
this.initLoops();
|
|
|
|
|
|
|
|
|
|
// 连接设备
|
|
|
|
|
this.connectSpp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aboutToDisappear(): void {
|
|
|
|
|
this.disconnectSpp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示Toast
|
|
|
|
|
showToastMessage(message: string): void {
|
|
|
|
|
this.toastMessage = message;
|
|
|
|
|
this.showToast = true;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.showToast = false;
|
|
|
|
|
}, 2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化回路
|
|
|
|
|
initLoops(): void {
|
|
|
|
|
this.loops = [];
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
this.loops.push({
|
|
|
|
|
index: i,
|
|
|
|
|
name: `#${i + 1}`,
|
|
|
|
|
isOpen: false,
|
|
|
|
|
pwm: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 连接 SPP(串口协议)
|
|
|
|
|
connectSpp(): void {
|
|
|
|
|
if (!this.deviceId) {
|
|
|
|
|
console.error('设备ID为空');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用 SPP 连接
|
|
|
|
|
socket.sppConnect(this.deviceId, {
|
|
|
|
|
uuid: this.SPP_UUID,
|
|
|
|
|
secure: true,
|
|
|
|
|
type: socket.SppType.SPP_RFCOMM
|
|
|
|
|
}, (err: BusinessError, clientNumber: number) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
console.error('SPP 连接失败:', JSON.stringify(err));
|
|
|
|
|
this.showToastMessage('SPP 连接失败');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.clientNumber = clientNumber;
|
|
|
|
|
this.isConnected = true;
|
|
|
|
|
console.info('SPP 连接成功, clientNumber:', clientNumber);
|
|
|
|
|
|
|
|
|
|
// 监听数据接收
|
|
|
|
|
socket.on('sppRead', this.clientNumber, (data: ArrayBuffer) => {
|
|
|
|
|
this.handleReceivedData(data);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 获取初始数据
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.getDeviceTime();
|
|
|
|
|
this.getManualControlInfo();
|
|
|
|
|
}, 500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('SPP 连接异常:', JSON.stringify(err));
|
|
|
|
|
this.showToastMessage('SPP 连接异常');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 断开 SPP 连接
|
|
|
|
|
disconnectSpp(): void {
|
|
|
|
|
if (this.clientNumber >= 0) {
|
|
|
|
|
try {
|
|
|
|
|
socket.off('sppRead', this.clientNumber);
|
|
|
|
|
socket.sppCloseClientSocket(this.clientNumber);
|
|
|
|
|
this.clientNumber = -1;
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
console.info('SPP 断开成功');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('SPP 断开失败:', JSON.stringify(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送数据
|
|
|
|
|
sendData(hexString: string): void {
|
|
|
|
|
console.log('hexString',hexString)
|
|
|
|
|
if (this.clientNumber < 0) {
|
|
|
|
|
console.error('未连接 SPP');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.info('发送数据:', 1111);
|
|
|
|
|
try {
|
|
|
|
|
const data = this.hexStringToArrayBuffer(hexString);
|
|
|
|
|
socket.sppWrite(this.clientNumber, data);
|
|
|
|
|
console.info('发送数据:', hexString);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('发送数据失败:', JSON.stringify(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理接收到的数据
|
|
|
|
|
handleReceivedData(data: ArrayBuffer): void {
|
|
|
|
|
const hexString = this.arrayBufferToHexString(data);
|
|
|
|
|
console.info('接收数据:', hexString);
|
|
|
|
|
|
|
|
|
|
// 解析 M9Z 协议响应
|
|
|
|
|
this.parseM9zResponse(hexString);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析 M9Z 响应
|
|
|
|
|
parseM9zResponse(hexString: string): void {
|
|
|
|
|
const bytes = this.hexStringToBytes(hexString);
|
|
|
|
|
|
|
|
|
|
// 验证帧格式
|
|
|
|
|
if (bytes.length < 5 || bytes[0] !== 0xEE || bytes[bytes.length - 1] !== 0xFF) {
|
|
|
|
|
console.warn('无效的响应帧');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const instruction = bytes[1];
|
|
|
|
|
const cmdType = bytes[2];
|
|
|
|
|
|
|
|
|
|
// 检查是否为响应帧 (D7=1)
|
|
|
|
|
if ((instruction & 0x80) === 0) {
|
|
|
|
|
console.warn('非响应帧');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const realInstruction = instruction & 0x7F;
|
|
|
|
|
|
|
|
|
|
// 根据指令码处理
|
|
|
|
|
switch (realInstruction) {
|
|
|
|
|
case 0x02: // 设备时间
|
|
|
|
|
if (bytes.length >= 9 && cmdType === 0x01) {
|
|
|
|
|
const timestamp = this.bytesToUint32LE(bytes.slice(5, 9));
|
|
|
|
|
const date = new Date(timestamp * 1000);
|
|
|
|
|
this.deviceTime = this.formatDateTime(date);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 0x01: // 手动控制
|
|
|
|
|
if (bytes.length >= 20 && cmdType === 0x01) {
|
|
|
|
|
this.parseManualControlResponse(bytes);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析手动控制响应
|
|
|
|
|
parseManualControlResponse(bytes: number[]): void {
|
|
|
|
|
const disable = bytes[5] === 0x01;
|
|
|
|
|
this.isManualMode = disable;
|
|
|
|
|
|
|
|
|
|
const relayBits = bytes[7] | (bytes[8] << 8);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
this.loops[i].isOpen = ((relayBits >> i) & 0x01) === 1;
|
|
|
|
|
this.loops[i].pwm = bytes[10 + i] || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 触发UI更新
|
|
|
|
|
this.loops = [...this.loops];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取设备时间
|
|
|
|
|
getDeviceTime(): void {
|
|
|
|
|
// M9Z 读取设备时间命令: EE 02 01 00 36 FF
|
|
|
|
|
const command = this.buildM9zCommand(0x02, 0x01, 0x00, []);
|
|
|
|
|
this.sendData(command);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取手动控制信息
|
|
|
|
|
getManualControlInfo(): void {
|
|
|
|
|
// M9Z 读取手动控制命令: EE 01 01 00 35 FF
|
|
|
|
|
const command = this.buildM9zCommand(0x01, 0x01, 0x00, []);
|
|
|
|
|
this.sendData(command);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 同步设备时间
|
|
|
|
|
syncDeviceTime(): void {
|
|
|
|
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
|
|
|
const data = this.uint32ToBytesLE(timestamp);
|
|
|
|
|
const command = this.buildM9zCommand(0x02, 0x02, 0x00, data);
|
|
|
|
|
this.sendData(command);
|
|
|
|
|
|
|
|
|
|
this.showToastMessage('同步时间中...');
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.getDeviceTime();
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置手动控制
|
|
|
|
|
setManualControl(): void {
|
|
|
|
|
const data: number[] = new Array(15).fill(0);
|
|
|
|
|
|
|
|
|
|
// Byte0: 手动控制使能
|
|
|
|
|
data[0] = this.isManualMode ? 0x01 : 0x00;
|
|
|
|
|
data[1] = 0x00;
|
|
|
|
|
|
|
|
|
|
// Byte2-3: 继电器状态位图
|
|
|
|
|
let relayBits = 0;
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
if (this.loops[i].isOpen) {
|
|
|
|
|
relayBits |= (1 << i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
data[2] = relayBits & 0xFF;
|
|
|
|
|
data[3] = (relayBits >> 8) & 0xFF;
|
|
|
|
|
data[4] = 0x00;
|
|
|
|
|
|
|
|
|
|
// Byte5-14: PWM值
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
data[5 + i] = this.loops[i].pwm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const command = this.buildM9zCommand(0x01, 0x02, 0x00, data);
|
|
|
|
|
this.sendData(command);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重启设备 (CMD: EE 7E 02 00 B3 FF)
|
|
|
|
|
restartDevice(): void {
|
|
|
|
|
const command = 'EE 7E 02 00 B3 FF';
|
|
|
|
|
this.sendData(command);
|
|
|
|
|
this.showToastMessage('重启指令已发送');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建 M9Z 命令
|
|
|
|
|
buildM9zCommand(instruction: number, cmdType: number, index: number, data: number[]): string {
|
|
|
|
|
const payload: number[] = [instruction, cmdType, index];
|
|
|
|
|
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
payload.push(data.length);
|
|
|
|
|
payload.push(...data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算校验和
|
|
|
|
|
let checksum = 0x33;
|
|
|
|
|
for (const byte of payload) {
|
|
|
|
|
checksum += byte;
|
|
|
|
|
}
|
|
|
|
|
checksum = checksum & 0xFF;
|
|
|
|
|
|
|
|
|
|
// 构建完整帧
|
|
|
|
|
const frame = [0xEE, ...payload, checksum, 0xFF];
|
|
|
|
|
|
|
|
|
|
return frame.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换回路状态
|
|
|
|
|
toggleLoop(index: number): void {
|
|
|
|
|
if (!this.isManualMode) {
|
|
|
|
|
this.showToastMessage('请先切换到手动模式');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loops[index].isOpen = !this.loops[index].isOpen;
|
|
|
|
|
this.loops = [...this.loops];
|
|
|
|
|
this.setManualControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 打开亮度调节
|
|
|
|
|
openBrightnessDialog(index: number): void {
|
|
|
|
|
if (!this.isManualMode) {
|
|
|
|
|
this.showToastMessage('请先切换到手动模式');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.selectedLoopIndex = index;
|
|
|
|
|
this.currentBrightness = index >= 0 ? this.loops[index].pwm : 50;
|
|
|
|
|
this.showBrightnessDialog = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确认亮度设置
|
|
|
|
|
confirmBrightness(): void {
|
|
|
|
|
if (this.selectedLoopIndex >= 0) {
|
|
|
|
|
this.loops[this.selectedLoopIndex].pwm = this.currentBrightness;
|
|
|
|
|
} else {
|
|
|
|
|
// 全局设置
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
this.loops[i].pwm = this.currentBrightness;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loops = [...this.loops];
|
|
|
|
|
this.showBrightnessDialog = false;
|
|
|
|
|
this.setManualControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 全开
|
|
|
|
|
turnOnAll(): void {
|
|
|
|
|
if (!this.isManualMode) {
|
|
|
|
|
this.showToastMessage('请先切换到手动模式');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
this.loops[i].isOpen = true;
|
|
|
|
|
}
|
|
|
|
|
this.loops = [...this.loops];
|
|
|
|
|
this.setManualControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 全关
|
|
|
|
|
turnOffAll(): void {
|
|
|
|
|
if (!this.isManualMode) {
|
|
|
|
|
this.showToastMessage('请先切换到手动模式');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
this.loops[i].isOpen = false;
|
|
|
|
|
}
|
|
|
|
|
this.loops = [...this.loops];
|
|
|
|
|
this.setManualControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换模式
|
|
|
|
|
toggleMode(): void {
|
|
|
|
|
this.isManualMode = !this.isManualMode;
|
|
|
|
|
this.setManualControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 刷新数据
|
|
|
|
|
refresh(): void {
|
|
|
|
|
this.getDeviceTime();
|
|
|
|
|
this.getManualControlInfo();
|
|
|
|
|
this.showToastMessage('刷新中...');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 返回上一页
|
|
|
|
|
goBack(): void {
|
|
|
|
|
this.showBackDialog = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行返回
|
|
|
|
|
doGoBack(): void {
|
|
|
|
|
this.disconnectSpp();
|
|
|
|
|
router.back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 工具方法
|
|
|
|
|
hexStringToArrayBuffer(hexString: string): ArrayBuffer {
|
|
|
|
|
const cleanHex = hexString.replace(/\s+/g, '');
|
|
|
|
|
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
|
|
|
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
|
|
|
bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);
|
|
|
|
|
}
|
|
|
|
|
return bytes.buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
arrayBufferToHexString(buffer: ArrayBuffer): string {
|
|
|
|
|
const bytes = new Uint8Array(buffer);
|
|
|
|
|
return Array.from(bytes).map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hexStringToBytes(hexString: string): number[] {
|
|
|
|
|
const cleanHex = hexString.replace(/\s+/g, '');
|
|
|
|
|
const bytes: number[] = [];
|
|
|
|
|
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
|
|
|
bytes.push(parseInt(cleanHex.substr(i, 2), 16));
|
|
|
|
|
}
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bytesToUint32LE(bytes: number[]): number {
|
|
|
|
|
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32ToBytesLE(value: number): number[] {
|
|
|
|
|
return [
|
|
|
|
|
value & 0xFF,
|
|
|
|
|
(value >> 8) & 0xFF,
|
|
|
|
|
(value >> 16) & 0xFF,
|
|
|
|
|
(value >> 24) & 0xFF
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatDateTime(date: Date): string {
|
|
|
|
|
const y = date.getFullYear();
|
|
|
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
const h = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
const min = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
return `${y}-${m}-${d} ${h}:${min}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loading遮罩层
|
|
|
|
|
@Builder
|
|
|
|
|
LoadingOverlay() {
|
|
|
|
|
if (this.dataLoading.deviceInfo || this.dataLoading.settingDeviceInfo) {
|
|
|
|
|
Stack() {
|
|
|
|
|
Rect()
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
.fill('rgba(11, 24, 48, 0.8)')
|
|
|
|
|
|
|
|
|
|
Column() {
|
|
|
|
|
LoadingProgress()
|
|
|
|
|
.width(60)
|
|
|
|
|
.height(60)
|
|
|
|
|
.color('#22fdc8')
|
|
|
|
|
Text('数据加载中...')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
.margin({ top: 10 })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
.position({ x: 0, y: 0 })
|
|
|
|
|
.zIndex(999)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建手动控制页面
|
|
|
|
|
@Builder
|
|
|
|
|
ManualControlContent() {
|
|
|
|
|
Stack() {
|
|
|
|
|
Column() {
|
|
|
|
|
// 设备信息
|
|
|
|
|
Column() {
|
|
|
|
|
Text(`设备地址:${this.deviceId || 'N/A'}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.margin({ bottom: 4 })
|
|
|
|
|
Text(`设备名称:${this.deviceName}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.margin({ bottom: 4 })
|
|
|
|
|
Row() {
|
|
|
|
|
Text(`设备时间:${this.deviceTime}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
if (this.dataLoading.deviceTime) {
|
|
|
|
|
LoadingProgress()
|
|
|
|
|
.width(12)
|
|
|
|
|
.height(12)
|
|
|
|
|
.color('#22fdc8')
|
|
|
|
|
.margin({ left: 4 })
|
|
|
|
|
}
|
|
|
|
|
Blank()
|
|
|
|
|
Text(this.isConnected ? '已连接' : '未连接')
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
.fontColor(this.isConnected ? '#4caf50' : '#ff5050')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.padding(10)
|
|
|
|
|
.backgroundColor('rgba(15, 30, 60, 0.4)')
|
|
|
|
|
.borderRadius(8)
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
// 控制模式
|
|
|
|
|
Row() {
|
|
|
|
|
Text('控制模式')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
Blank()
|
|
|
|
|
Button(this.isManualMode ? '手动模式' : '自动模式')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor(this.isManualMode ? '#22fdc8' : '#b2ebf2')
|
|
|
|
|
.backgroundColor(this.isManualMode ? 'rgba(34, 253, 200, 0.1)' : 'rgba(20, 38, 81, 0.95)')
|
|
|
|
|
.border({ width: 1, color: this.isManualMode ? '#22fdc8' : '#213b67' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.padding({ left: 15, right: 15, top: 8, bottom: 8 })
|
|
|
|
|
.onClick(() => this.toggleMode())
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
// 回路控制
|
|
|
|
|
Row() {
|
|
|
|
|
Text('回路控制')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
if (this.dataLoading.deviceInfo) {
|
|
|
|
|
LoadingProgress()
|
|
|
|
|
.width(16)
|
|
|
|
|
.height(16)
|
|
|
|
|
.color('#22fdc8')
|
|
|
|
|
.margin({ left: 8 })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 10 })
|
|
|
|
|
|
|
|
|
|
Grid() {
|
|
|
|
|
ForEach(this.loops, (loop: LoopInfo, index: number) => {
|
|
|
|
|
GridItem() {
|
|
|
|
|
Column() {
|
|
|
|
|
Row() {
|
|
|
|
|
Text('💡')
|
|
|
|
|
.fontSize(20)
|
|
|
|
|
}
|
|
|
|
|
.width(40)
|
|
|
|
|
.height(40)
|
|
|
|
|
.borderRadius(20)
|
|
|
|
|
.backgroundColor(loop.isOpen ? '#22fdc8' : '#2d3a51')
|
|
|
|
|
.border({ width: 1, color: loop.isOpen ? '#22fdc8' : '#3e7ba1' })
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
|
|
|
|
Text(`回路${index + 1}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
.margin({ top: 4 })
|
|
|
|
|
Text(loop.isOpen ? '开启' : '关闭')
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
}
|
|
|
|
|
.padding(8)
|
|
|
|
|
.onClick(() => this.toggleLoop(index))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
|
|
|
|
|
.rowsGap(10)
|
|
|
|
|
.columnsGap(10)
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
// 调光控制
|
|
|
|
|
Text('调光控制')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 10 })
|
|
|
|
|
|
|
|
|
|
Grid() {
|
|
|
|
|
ForEach(this.loops, (loop: LoopInfo, index: number) => {
|
|
|
|
|
GridItem() {
|
|
|
|
|
Column() {
|
|
|
|
|
Row() {
|
|
|
|
|
Text(`${loop.pwm}%`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
}
|
|
|
|
|
.width(50)
|
|
|
|
|
.height(50)
|
|
|
|
|
.borderRadius(25)
|
|
|
|
|
.backgroundColor(loop.pwm > 0 ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51')
|
|
|
|
|
.border({ width: 1, color: loop.pwm > 0 ? '#22fdc8' : '#3e7ba1' })
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
|
|
|
|
Text(`通道${index + 1}`)
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.margin({ top: 4 })
|
|
|
|
|
}
|
|
|
|
|
.onClick(() => this.openBrightnessDialog(index))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
|
|
|
|
|
.rowsGap(10)
|
|
|
|
|
.columnsGap(10)
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
// 快捷操作
|
|
|
|
|
Row() {
|
|
|
|
|
Button('全开')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.backgroundColor('rgba(34, 253, 200, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#22fdc8' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.onClick(() => this.turnOnAll())
|
|
|
|
|
|
|
|
|
|
Button('全关')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#fa5050')
|
|
|
|
|
.backgroundColor('rgba(250, 80, 80, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#fa5050' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 8 })
|
|
|
|
|
.onClick(() => this.turnOffAll())
|
|
|
|
|
|
|
|
|
|
Button('一键调光')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#ffcb4d')
|
|
|
|
|
.backgroundColor('rgba(255, 203, 77, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#ffcb4d' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 8 })
|
|
|
|
|
.onClick(() => this.openBrightnessDialog(-1))
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ bottom: 10 })
|
|
|
|
|
|
|
|
|
|
Row() {
|
|
|
|
|
Button(this.isLoading ? '同步中...' : '同步时间')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#40a7fa')
|
|
|
|
|
.backgroundColor('rgba(64, 167, 250, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#40a7fa' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.onClick(() => this.syncDeviceTime())
|
|
|
|
|
|
|
|
|
|
Button('刷新数据')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.95)')
|
|
|
|
|
.border({ width: 1, color: '#213b67' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 8 })
|
|
|
|
|
.onClick(() => this.refresh())
|
|
|
|
|
|
|
|
|
|
Button('重启')
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#ff5050')
|
|
|
|
|
.backgroundColor('rgba(250, 80, 80, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#ff5050' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 8 })
|
|
|
|
|
.onClick(() => this.restartDevice())
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
// 日出日落时间
|
|
|
|
|
Row() {
|
|
|
|
|
Text(`日出时间:${this.sunriseTime}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
if (this.dataLoading.sunRiseTime) {
|
|
|
|
|
LoadingProgress()
|
|
|
|
|
.width(12)
|
|
|
|
|
.height(12)
|
|
|
|
|
.color('#22fdc8')
|
|
|
|
|
.margin({ left: 4 })
|
|
|
|
|
}
|
|
|
|
|
Blank()
|
|
|
|
|
Text(`日落时间:${this.sunsetTime}`)
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.padding(10)
|
|
|
|
|
.backgroundColor('rgba(15, 30, 60, 0.4)')
|
|
|
|
|
.borderRadius(8)
|
|
|
|
|
.margin({ top: 15 })
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
this.LoadingOverlay()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建参数设置页面
|
|
|
|
|
@Builder
|
|
|
|
|
ParameterSettingsContent() {
|
|
|
|
|
Column() {
|
|
|
|
|
Text('参数设置')
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
|
|
|
|
Text('功能开发中...')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#8a9ba8')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.padding(20)
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建自动模式页面
|
|
|
|
|
@Builder
|
|
|
|
|
AutoModeContent() {
|
|
|
|
|
Column() {
|
|
|
|
|
Text('自动模式')
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
|
|
|
|
Text('功能开发中...')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#8a9ba8')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.padding(20)
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建报警浏览页面
|
|
|
|
|
@Builder
|
|
|
|
|
AlarmBrowserContent() {
|
|
|
|
|
Column() {
|
|
|
|
|
Text('报警浏览')
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
|
|
|
|
Text('功能开发中...')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#8a9ba8')
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.padding(20)
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
build() {
|
|
|
|
|
Stack() {
|
|
|
|
|
Column() {
|
|
|
|
|
// Tab栏
|
|
|
|
|
Row() {
|
|
|
|
|
ForEach(this.tabs, (tab: TabItem) => {
|
|
|
|
|
Column() {
|
|
|
|
|
Text(tab.name)
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor(this.activeTab === tab.index ? '#22fdc8' : '#b2ebf2')
|
|
|
|
|
.fontWeight(this.activeTab === tab.index ? FontWeight.Medium : FontWeight.Normal)
|
|
|
|
|
|
|
|
|
|
if (this.activeTab === tab.index) {
|
|
|
|
|
Divider()
|
|
|
|
|
.strokeWidth(2)
|
|
|
|
|
.color('#22fdc8')
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ top: 8 })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
|
|
|
|
.backgroundColor(this.activeTab === tab.index ? 'rgba(34, 253, 200, 0.1)' : 'transparent')
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.activeTab = tab.index;
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.95)')
|
|
|
|
|
.border({ width: { bottom: 1 }, color: '#213b67' })
|
|
|
|
|
|
|
|
|
|
// 内容区域
|
|
|
|
|
Scroll() {
|
|
|
|
|
Column() {
|
|
|
|
|
if (this.activeTab === 0) {
|
|
|
|
|
this.ManualControlContent()
|
|
|
|
|
} else if (this.activeTab === 1) {
|
|
|
|
|
this.ParameterSettingsContent()
|
|
|
|
|
} else if (this.activeTab === 2) {
|
|
|
|
|
this.AutoModeContent()
|
|
|
|
|
} else if (this.activeTab === 3) {
|
|
|
|
|
this.AlarmBrowserContent()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(10)
|
|
|
|
|
}
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
.linearGradient({
|
|
|
|
|
direction: GradientDirection.RightBottom,
|
|
|
|
|
colors: [['#0b1830', 0], ['#1d3553', 1]]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Toast 提示
|
|
|
|
|
if (this.showToast) {
|
|
|
|
|
Text(this.toastMessage)
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#ffffff')
|
|
|
|
|
.backgroundColor('rgba(0, 0, 0, 0.7)')
|
|
|
|
|
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
|
|
|
|
|
.borderRadius(20)
|
|
|
|
|
.position({ x: '50%', y: '80%' })
|
|
|
|
|
.translate({ x: '-50%' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 亮度调节弹窗
|
|
|
|
|
if (this.showBrightnessDialog) {
|
|
|
|
|
Column() {
|
|
|
|
|
Column() {
|
|
|
|
|
Text('亮度调节')
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
|
|
|
|
Text(`${this.currentBrightness}%`)
|
|
|
|
|
.fontSize(32)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
Slider({
|
|
|
|
|
value: this.currentBrightness,
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 100,
|
|
|
|
|
step: 1
|
|
|
|
|
})
|
|
|
|
|
.trackColor('#2d3a51')
|
|
|
|
|
.selectedColor('#22fdc8')
|
|
|
|
|
.blockColor('#22fdc8')
|
|
|
|
|
.width('100%')
|
|
|
|
|
.onChange((value: number) => {
|
|
|
|
|
this.currentBrightness = value;
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Row() {
|
|
|
|
|
Button('取消')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.95)')
|
|
|
|
|
.border({ width: 1, color: '#213b67' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.showBrightnessDialog = false;
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Button('确认')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#22fdc8')
|
|
|
|
|
.backgroundColor('rgba(34, 253, 200, 0.1)')
|
|
|
|
|
.border({ width: 1, color: '#22fdc8' })
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 10 })
|
|
|
|
|
.onClick(() => this.confirmBrightness())
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.margin({ top: 20 })
|
|
|
|
|
}
|
|
|
|
|
.width('80%')
|
|
|
|
|
.padding(20)
|
|
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.98)')
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
.border({ width: 1, color: '#213b67' })
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
.backgroundColor('rgba(0, 0, 0, 0.5)')
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.showBrightnessDialog = false;
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 返回确认对话框
|
|
|
|
|
if (this.showBackDialog) {
|
|
|
|
|
Column() {
|
|
|
|
|
Column() {
|
|
|
|
|
Text('提示')
|
|
|
|
|
.fontSize(18)
|
|
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
|
|
|
|
Text('确定要断开蓝牙设备连接吗?')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#b2ebf2')
|
|
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
|
|
|
|
Row() {
|
|
|
|
|
Button('取消')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#999999')
|
|
|
|
|
.backgroundColor('rgba(255, 255, 255, 0.1)')
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.showBackDialog = false;
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Button('断开')
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
.fontColor('#ff5050')
|
|
|
|
|
.backgroundColor('rgba(255, 80, 80, 0.2)')
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
.margin({ left: 15 })
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.showBackDialog = false;
|
|
|
|
|
this.doGoBack();
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
}
|
|
|
|
|
.width('80%')
|
|
|
|
|
.padding(20)
|
|
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.98)')
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
.border({ width: 1, color: '#213b67' })
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
.backgroundColor('rgba(0, 0, 0, 0.5)')
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.width('100%')
|
|
|
|
|
.height('100%')
|
|
|
|
|
}
|
|
|
|
|
}
|