1098 lines
40 KiB
Dart
1098 lines
40 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:web_frontend/controllers/ControlPanelController.dart';
|
|
import 'package:web_frontend/models/LogType.dart';
|
|
import 'package:web_frontend/models/UserInfo.dart';
|
|
import 'package:web_frontend/services/WebSocketService.dart';
|
|
|
|
class ControlPanelView extends StatelessWidget {
|
|
ControlPanelView({super.key});
|
|
|
|
final GlobalKey _dropdownButtonKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Get.put(ControlPanelController());
|
|
final webSocketService = Get.find<WebSocketService>();
|
|
|
|
return Scaffold(
|
|
body: Row(
|
|
children: [
|
|
// 左侧边栏 - 占15%宽度
|
|
_buildSidebar(controller, context),
|
|
|
|
// 右侧主区域 - 占85%宽度
|
|
Expanded(
|
|
child: _buildMainArea(controller, webSocketService),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSidebar(
|
|
ControlPanelController controller, BuildContext context) {
|
|
return Container(
|
|
width: MediaQuery.of(context).size.width * 0.15,
|
|
color: Colors.grey[900],
|
|
child: Column(
|
|
children: [
|
|
// 用户选择区域
|
|
_buildUserSelection(controller, context),
|
|
|
|
// 模糊查询输入框
|
|
_buildSearchField(controller),
|
|
|
|
// 信号强度过滤
|
|
_buildRssiFilter(controller),
|
|
|
|
// 设备统计
|
|
_buildDeviceCount(controller),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
// 设备列表 - 使用 Expanded 让它占据剩余空间
|
|
Expanded(
|
|
child: _buildDeviceList(controller),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildUserSelection(
|
|
ControlPanelController controller, BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'当前用户',
|
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Obx(() {
|
|
if (controller.users.isEmpty) {
|
|
return Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[800],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'暂无在线设备',
|
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 使用 InkWell 实现可点击的选择器
|
|
return InkWell(
|
|
key: _dropdownButtonKey,
|
|
onTap: () => _showCustomUserMenu(controller, context),
|
|
child: Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[800],
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey[700]!),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
controller.selectedUser.value != null
|
|
? '${controller.selectedUser.value!.deviceModel} (${_getShortUuid(controller.selectedUser.value!.uuid)})'
|
|
: '选择设备',
|
|
style: TextStyle(
|
|
color: controller.selectedUser.value != null
|
|
? Colors.white
|
|
: Colors.grey,
|
|
fontSize: 12,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const Icon(Icons.arrow_drop_down,
|
|
color: Colors.grey, size: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
// 添加连接状态提示
|
|
Obx(() => Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
controller.webSocketService.isConnected.value
|
|
? Icons.wifi
|
|
: Icons.wifi_off,
|
|
size: 12,
|
|
color: controller.webSocketService.isConnected.value
|
|
? Colors.green
|
|
: Colors.red,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
controller.webSocketService.connectionStatus.value,
|
|
style: TextStyle(
|
|
color: controller.webSocketService.isConnected.value
|
|
? Colors.green
|
|
: Colors.red,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 构建 PopupMenu 的菜单项
|
|
List<PopupMenuEntry<UserInfo>> _buildPopupMenuItems(
|
|
ControlPanelController controller, BuildContext context) {
|
|
// 使用 StatefulBuilder 来实现搜索功能
|
|
final List<PopupMenuEntry<UserInfo>> entries = [];
|
|
|
|
// 添加搜索框
|
|
entries.add(
|
|
PopupMenuItem<UserInfo>(
|
|
enabled: false,
|
|
padding: EdgeInsets.zero,
|
|
child: Container(
|
|
width: 280,
|
|
padding: const EdgeInsets.all(8),
|
|
child: TextField(
|
|
autofocus: true,
|
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
|
decoration: InputDecoration(
|
|
hintText: '搜索设备...',
|
|
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
prefixIcon:
|
|
const Icon(Icons.search, size: 18, color: Colors.grey),
|
|
filled: true,
|
|
fillColor: Colors.grey[800],
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
),
|
|
onChanged: (value) {
|
|
// 这里需要通过 StatefulBuilder 来更新
|
|
// 由于 PopupMenuButton 不支持动态更新,我们使用另一种方式
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// 添加分隔线
|
|
entries.add(const PopupMenuDivider());
|
|
|
|
// 添加设备列表
|
|
final users = controller.users;
|
|
for (var user in users) {
|
|
final isSelected = controller.selectedUser.value?.uuid == user.uuid;
|
|
entries.add(
|
|
PopupMenuItem<UserInfo>(
|
|
value: user,
|
|
padding: EdgeInsets.zero,
|
|
child: Container(
|
|
width: 280,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? Colors.grey[800] : null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.devices,
|
|
size: 16,
|
|
color: isSelected ? Colors.green : Colors.grey,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.deviceModel,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.green : Colors.grey[500],
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
Text(
|
|
// _getShortUuid(user.uuid),
|
|
user.uuid,
|
|
style: TextStyle(
|
|
color: Colors.grey[500],
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isSelected)
|
|
const Icon(Icons.check_circle, size: 16, color: Colors.green),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
String _getShortUuid(String uuid) {
|
|
if (uuid.length >= 8) {
|
|
return '${uuid.substring(0, 8)}...';
|
|
}
|
|
return uuid;
|
|
}
|
|
|
|
Widget _buildSearchField(ControlPanelController controller) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: TextField(
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
hintText: '模糊查询...',
|
|
hintStyle: const TextStyle(color: Colors.grey),
|
|
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
|
filled: true,
|
|
fillColor: Colors.grey[800],
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
onChanged: (value) => controller.searchKeyword.value = value,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 信号强度过滤组件
|
|
Widget _buildRssiFilter(ControlPanelController controller) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'信号强度过滤',
|
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 16),
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return Obx(() {
|
|
final double minValue = -100;
|
|
final double maxValue = 65;
|
|
final double valueRange = maxValue - minValue;
|
|
final double percent =
|
|
(controller.rssiFilterValue.value - minValue) / valueRange;
|
|
final double thumbPosition = percent * constraints.maxWidth;
|
|
|
|
return Column(
|
|
children: [
|
|
// 滑块区域
|
|
GestureDetector(
|
|
onHorizontalDragUpdate: (details) {
|
|
final newPercent = (thumbPosition + details.delta.dx) /
|
|
constraints.maxWidth;
|
|
final newPercentClamped = newPercent.clamp(0.0, 1.0);
|
|
final newValue =
|
|
minValue + (newPercentClamped * valueRange);
|
|
controller.updateRssiFilter(newValue.round());
|
|
},
|
|
onTapDown: (details) {
|
|
final localX = details.localPosition.dx;
|
|
final newPercent =
|
|
(localX / constraints.maxWidth).clamp(0.0, 1.0);
|
|
final newValue = minValue + (newPercent * valueRange);
|
|
controller.updateRssiFilter(newValue.round());
|
|
},
|
|
child: Container(
|
|
height: 40,
|
|
child: Stack(
|
|
alignment: Alignment.centerLeft,
|
|
children: [
|
|
// 背景线(灰色)
|
|
Container(
|
|
height: 3,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(1.5),
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
// 左侧绿色线(从起点到圆点位置)
|
|
Container(
|
|
width: thumbPosition,
|
|
height: 3,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(1.5),
|
|
color: Colors.green,
|
|
),
|
|
),
|
|
// 白色实心圆
|
|
Positioned(
|
|
left: thumbPosition - 12,
|
|
child: Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white,
|
|
border: Border.all(
|
|
color: Colors.green,
|
|
width: 2,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.2),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 强度值显示和范围标签
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'-100 dBm',
|
|
style: TextStyle(color: Colors.grey, fontSize: 10),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.green,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Text(
|
|
'${controller.rssiFilterValue.value} dBm',
|
|
style: const TextStyle(
|
|
color: Colors.green,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const Text(
|
|
'65 dBm',
|
|
style: TextStyle(color: Colors.grey, fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getRssiColor(int rssi) {
|
|
if (rssi >= -50) return Colors.green;
|
|
if (rssi >= -70) return Colors.lightGreen;
|
|
if (rssi >= -80) return Colors.yellow.shade700;
|
|
if (rssi >= -90) return Colors.orange;
|
|
return Colors.red;
|
|
}
|
|
|
|
Widget _buildDeviceCount(ControlPanelController controller) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Obx(() => Text(
|
|
'匹配出 ${controller.filteredDevices.length} 个设备',
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeviceList(ControlPanelController controller) {
|
|
return Obx(() {
|
|
if (controller.selectedUser.value == null) {
|
|
return const Center(
|
|
child: Text(
|
|
'请先选择设备',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (controller.filteredDevices.isEmpty) {
|
|
return const Center(
|
|
child: Text(
|
|
'暂无设备,请点击"扫描"按钮',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: controller.filteredDevices.length,
|
|
itemBuilder: (context, index) {
|
|
final device = controller.filteredDevices[index];
|
|
return ListTile(
|
|
title: Text(
|
|
device.name.isEmpty ? '未知设备' : device.name,
|
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
|
),
|
|
subtitle: Text(
|
|
device.id,
|
|
style: const TextStyle(color: Colors.grey, fontSize: 10),
|
|
),
|
|
trailing: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: _getRssiColor(device.rssi),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'${device.rssi} dBm',
|
|
style: const TextStyle(color: Colors.white, fontSize: 10),
|
|
),
|
|
),
|
|
onTap: () => controller.connectToDevice(device),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildMainArea(
|
|
ControlPanelController controller, WebSocketService webSocketService) {
|
|
return Column(
|
|
children: [
|
|
_buildMenuBar(webSocketService),
|
|
_buildToolBar(controller),
|
|
_buildLogFilterBar(controller),
|
|
Expanded(
|
|
child: _buildLogArea(controller),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMenuBar(WebSocketService webSocketService) {
|
|
return Container(
|
|
height: 48,
|
|
color: Colors.grey[100],
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Row(
|
|
children: [
|
|
const Text(
|
|
'指令和日志',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const Spacer(),
|
|
Obx(() => Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: webSocketService.isConnected.value
|
|
? Colors.green.withOpacity(0.2)
|
|
: Colors.red.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: webSocketService.isConnected.value
|
|
? Colors.green
|
|
: Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
webSocketService.connectionStatus.value,
|
|
style: TextStyle(
|
|
color: webSocketService.isConnected.value
|
|
? Colors.green
|
|
: Colors.red,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildToolBar(ControlPanelController controller) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// 扫描按钮
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
if (controller.selectedUser.value == null) {
|
|
controller.addLog('请先选择设备', LogType.warning);
|
|
return;
|
|
}
|
|
controller.sendScanCommand();
|
|
},
|
|
icon: const Icon(Icons.bluetooth_searching, size: 18),
|
|
label: const Text('扫描'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// 停止扫描按钮
|
|
OutlinedButton.icon(
|
|
onPressed: () {
|
|
if (controller.selectedUser.value == null) {
|
|
controller.addLog('请先选择设备', LogType.warning);
|
|
return;
|
|
}
|
|
controller.sendStopScanCommand();
|
|
},
|
|
icon: const Icon(Icons.stop, size: 18),
|
|
label: const Text('停止扫描'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// 断开设备按钮
|
|
OutlinedButton.icon(
|
|
onPressed: () {
|
|
if (controller.selectedUser.value == null) {
|
|
controller.addLog('请先选择设备', LogType.warning);
|
|
return;
|
|
}
|
|
controller.disconnectDevice();
|
|
},
|
|
icon: const Icon(Icons.bluetooth_disabled, size: 18),
|
|
label: const Text('断开设备'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.red,
|
|
),
|
|
),
|
|
|
|
const Spacer(),
|
|
|
|
// 命令输入框和执行按钮
|
|
SizedBox(
|
|
width: 300,
|
|
child: TextField(
|
|
controller: controller.commandController,
|
|
decoration: InputDecoration(
|
|
hintText: '输入命令或数据...',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.send),
|
|
onPressed: () {
|
|
if (controller.selectedUser.value == null) {
|
|
controller.addLog('请先选择设备', LogType.warning);
|
|
return;
|
|
}
|
|
controller.sendCustomData();
|
|
},
|
|
tooltip: '执行',
|
|
),
|
|
),
|
|
onSubmitted: (_) {
|
|
if (controller.selectedUser.value == null) {
|
|
controller.addLog('请先选择设备', LogType.warning);
|
|
return;
|
|
}
|
|
controller.sendCustomData();
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
// 自动滚动开关
|
|
Row(
|
|
children: [
|
|
const Text('自动滚动'),
|
|
const SizedBox(width: 8),
|
|
Obx(() => Switch(
|
|
value: controller.autoScroll.value,
|
|
onChanged: controller.toggleAutoScroll,
|
|
)),
|
|
],
|
|
),
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
// 清空日志按钮
|
|
ElevatedButton(
|
|
onPressed: controller.clearLogs,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('清空日志'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogFilterBar(ControlPanelController controller) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Text('日志过滤器:', style: TextStyle(fontSize: 12)),
|
|
const SizedBox(width: 16),
|
|
Obx(() => FilterChip(
|
|
label: const Text('全部'),
|
|
selected: controller.showAllLogs.value,
|
|
onSelected: controller.toggleShowAllLogs,
|
|
backgroundColor: Colors.white,
|
|
selectedColor: Colors.blue.shade100,
|
|
)),
|
|
const SizedBox(width: 8),
|
|
Obx(() => FilterChip(
|
|
label: const Text('连接日志'),
|
|
selected: !controller.showAllLogs.value &&
|
|
controller.showConnectionLogs.value,
|
|
onSelected: (selected) {
|
|
if (!controller.showAllLogs.value) {
|
|
controller.toggleShowConnectionLogs(selected);
|
|
}
|
|
},
|
|
backgroundColor: Colors.white,
|
|
selectedColor: Colors.green.shade100,
|
|
)),
|
|
const SizedBox(width: 8),
|
|
Obx(() => FilterChip(
|
|
label: const Text('设备日志'),
|
|
selected: !controller.showAllLogs.value &&
|
|
controller.showDeviceLogs.value,
|
|
onSelected: (selected) {
|
|
if (!controller.showAllLogs.value) {
|
|
controller.toggleShowDeviceLogs(selected);
|
|
}
|
|
},
|
|
backgroundColor: Colors.white,
|
|
selectedColor: Colors.orange.shade100,
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogArea(ControlPanelController controller) {
|
|
return Container(
|
|
color: Colors.grey[50],
|
|
child: Obx(() {
|
|
final logs = controller.displayLogs;
|
|
|
|
if (logs.isEmpty) {
|
|
return const Center(
|
|
child: Text('暂无日志'),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
controller: controller.logScrollController,
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: logs.length,
|
|
itemBuilder: (context, index) {
|
|
final log = logs[index];
|
|
return _buildLogItem(log);
|
|
},
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogItem(LogEntry log) {
|
|
Color textColor;
|
|
String prefix;
|
|
IconData? icon;
|
|
|
|
switch (log.type) {
|
|
case LogType.success:
|
|
textColor = Colors.green;
|
|
prefix = '[SUCCESS]';
|
|
icon = Icons.check_circle_outline;
|
|
break;
|
|
case LogType.error:
|
|
textColor = Colors.red;
|
|
prefix = '[ERROR]';
|
|
icon = Icons.error_outline;
|
|
break;
|
|
case LogType.warning:
|
|
textColor = Colors.orange;
|
|
prefix = '[WARNING]';
|
|
icon = Icons.warning_amber_outlined;
|
|
break;
|
|
case LogType.device:
|
|
textColor = Colors.purple;
|
|
prefix = '[DEVICE]';
|
|
icon = Icons.devices;
|
|
break;
|
|
case LogType.connection:
|
|
textColor = Colors.blue;
|
|
prefix = '[CONN]';
|
|
icon = Icons.wifi;
|
|
break;
|
|
default:
|
|
textColor = Colors.black87;
|
|
prefix = '[INFO]';
|
|
icon = Icons.info_outline;
|
|
}
|
|
|
|
final timeStr =
|
|
'${log.time.hour.toString().padLeft(2, '0')}:${log.time.minute.toString().padLeft(2, '0')}:${log.time.second.toString().padLeft(2, '0')}';
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, size: 14, color: textColor),
|
|
const SizedBox(width: 8),
|
|
],
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
timeStr,
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 12,
|
|
fontFamily: 'monospace',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
prefix,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
log.message,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 12,
|
|
fontFamily: 'monospace',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCustomUserMenu(
|
|
ControlPanelController controller, BuildContext context) {
|
|
// 获取按钮的位置和大小
|
|
final RenderBox renderBox =
|
|
_dropdownButtonKey.currentContext!.findRenderObject() as RenderBox;
|
|
final Offset offset = renderBox.localToGlobal(Offset.zero);
|
|
final Size buttonSize = renderBox.size;
|
|
|
|
// 搜索控制器
|
|
final searchController = TextEditingController();
|
|
|
|
showGeneralDialog(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
barrierLabel: "Dismiss",
|
|
barrierColor: Colors.black.withOpacity(0.5),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
pageBuilder: (context, animation, secondaryAnimation) {
|
|
// 使用 StatefulBuilder 来管理弹窗内部状态
|
|
return StatefulBuilder(
|
|
builder: (dialogContext, setDialogState) {
|
|
// 初始化过滤用户列表
|
|
List<UserInfo> filteredUsers = controller.users.toList();
|
|
|
|
void updateFilter(String value) {
|
|
setDialogState(() {
|
|
if (value.isEmpty) {
|
|
filteredUsers = controller.users.toList();
|
|
} else {
|
|
filteredUsers = controller.users
|
|
.where((user) =>
|
|
user.deviceModel
|
|
.toLowerCase()
|
|
.contains(value.toLowerCase()) ||
|
|
user.uuid.toLowerCase().contains(value.toLowerCase()))
|
|
.toList();
|
|
}
|
|
});
|
|
}
|
|
|
|
return Stack(
|
|
children: [
|
|
// 点击背景关闭
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.of(dialogContext).pop(),
|
|
child: Container(
|
|
color: Colors.transparent,
|
|
),
|
|
),
|
|
),
|
|
// 菜单内容 - 宽度与按钮相同
|
|
Positioned(
|
|
left: offset.dx,
|
|
top: offset.dy + buttonSize.height + 2,
|
|
width: buttonSize.width,
|
|
child: Material(
|
|
elevation: 8,
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.grey[850],
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// 搜索框
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: Colors.grey[700]!,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: searchController,
|
|
autofocus: true,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: '搜索设备名称或UUID...',
|
|
hintStyle: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 12,
|
|
),
|
|
prefixIcon: const Icon(
|
|
Icons.search,
|
|
size: 18,
|
|
color: Colors.grey,
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey[800],
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 8),
|
|
suffixIcon: searchController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear, size: 16),
|
|
color: Colors.grey,
|
|
onPressed: () {
|
|
searchController.clear();
|
|
updateFilter('');
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (value) {
|
|
updateFilter(value);
|
|
},
|
|
),
|
|
),
|
|
// 用户列表
|
|
Flexible(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxHeight: 300,
|
|
minHeight: 100,
|
|
),
|
|
child: Builder(
|
|
builder: (context) {
|
|
// 重新获取过滤后的列表
|
|
List<UserInfo> currentFilteredUsers;
|
|
if (searchController.text.isEmpty) {
|
|
currentFilteredUsers =
|
|
controller.users.toList();
|
|
} else {
|
|
currentFilteredUsers = controller.users
|
|
.where((user) =>
|
|
user.deviceModel
|
|
.toLowerCase()
|
|
.contains(searchController.text
|
|
.toLowerCase()) ||
|
|
user.uuid.toLowerCase().contains(
|
|
searchController.text
|
|
.toLowerCase()))
|
|
.toList();
|
|
}
|
|
|
|
if (currentFilteredUsers.isEmpty) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Text(
|
|
'未找到匹配的设备',
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: currentFilteredUsers.length,
|
|
itemBuilder: (context, index) {
|
|
final user = currentFilteredUsers[index];
|
|
final isSelected =
|
|
controller.selectedUser.value?.uuid ==
|
|
user.uuid;
|
|
|
|
return InkWell(
|
|
onTap: () {
|
|
controller.selectUser(user);
|
|
Navigator.of(dialogContext).pop();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? Colors.grey[800]
|
|
: Colors.transparent,
|
|
border: isSelected
|
|
? null
|
|
: Border(
|
|
bottom: BorderSide(
|
|
color: Colors.grey[800]!,
|
|
width: 0.5,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.devices,
|
|
size: 16,
|
|
color: isSelected
|
|
? Colors.green
|
|
: Colors.grey[500],
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.deviceModel,
|
|
style: TextStyle(
|
|
color: isSelected
|
|
? Colors.green
|
|
: Colors.white,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
Text(
|
|
user.uuid,
|
|
style: TextStyle(
|
|
color: Colors.grey[500],
|
|
fontSize: 10,
|
|
),
|
|
maxLines: 1,
|
|
overflow:
|
|
TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isSelected)
|
|
const Icon(
|
|
Icons.check_circle,
|
|
size: 16,
|
|
color: Colors.green,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|