ix_developer_tools.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. import 'dart:convert';
  2. import 'package:archive/archive.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:get/get.dart';
  5. import 'package:dio/dio.dart' as dio;
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import '../../app/constants/keys.dart';
  8. import '../crypto.dart';
  9. import '../log/logger.dart';
  10. import 'simple_log_card.dart';
  11. import 'simple_api_card.dart';
  12. /// 简化版开发者工具 - 只包含控制台日志和API监控
  13. class IXDeveloperTools {
  14. static final IXDeveloperTools _instance = IXDeveloperTools._internal();
  15. factory IXDeveloperTools() => _instance;
  16. IXDeveloperTools._internal();
  17. final List<ConsoleLogEntry> _logs = [];
  18. final List<ApiRequestInfo> _apiRequests = [];
  19. static const int maxLogs = 1000;
  20. static const int maxApiRequests = 500;
  21. /// 添加控制台日志
  22. static void addLog(String message, String tag) {
  23. final entry = ConsoleLogEntry(
  24. timestamp: DateTime.now(),
  25. message: message,
  26. tag: tag,
  27. );
  28. _instance._logs.insert(0, entry);
  29. if (_instance._logs.length > maxLogs) {
  30. _instance._logs.removeRange(maxLogs, _instance._logs.length);
  31. }
  32. }
  33. /// 添加API请求信息
  34. static void addApiRequest(ApiRequestInfo request) {
  35. _instance._apiRequests.insert(0, request);
  36. if (_instance._apiRequests.length > maxApiRequests) {
  37. _instance._apiRequests.removeRange(
  38. maxApiRequests,
  39. _instance._apiRequests.length,
  40. );
  41. }
  42. }
  43. /// 获取所有日志
  44. static List<ConsoleLogEntry> get logs => List.unmodifiable(_instance._logs);
  45. /// 获取所有API请求
  46. static List<ApiRequestInfo> get apiRequests =>
  47. List.unmodifiable(_instance._apiRequests);
  48. /// 清除所有日志
  49. static void clearLogs() {
  50. _instance._logs.clear();
  51. }
  52. /// 清除所有API请求
  53. static void clearApiRequests() {
  54. _instance._apiRequests.clear();
  55. }
  56. /// 显示开发者工具
  57. static void show() {
  58. try {
  59. Get.to(
  60. () => const SimpleDeveloperToolsScreen(),
  61. transition: Transition.cupertino,
  62. );
  63. } catch (e) {
  64. log('IXDeveloperTools', 'Unable to open Developer Tools: $e');
  65. }
  66. }
  67. }
  68. /// 控制台日志条目
  69. class ConsoleLogEntry {
  70. final DateTime timestamp;
  71. final String message;
  72. final String tag;
  73. ConsoleLogEntry({
  74. required this.timestamp,
  75. required this.message,
  76. required this.tag,
  77. });
  78. String get formattedTime {
  79. return '${timestamp.hour.toString().padLeft(2, '0')}:'
  80. '${timestamp.minute.toString().padLeft(2, '0')}:'
  81. '${timestamp.second.toString().padLeft(2, '0')}.'
  82. '${timestamp.millisecond.toString().padLeft(3, '0')}';
  83. }
  84. }
  85. /// API请求信息
  86. class ApiRequestInfo {
  87. final String id;
  88. final DateTime timestamp;
  89. final String method;
  90. final String url;
  91. final Map<String, dynamic>? headers;
  92. final dynamic requestData;
  93. final int? statusCode;
  94. final dynamic responseData;
  95. final String? error;
  96. final Duration? duration;
  97. ApiRequestInfo({
  98. required this.id,
  99. required this.timestamp,
  100. required this.method,
  101. required this.url,
  102. this.headers,
  103. this.requestData,
  104. this.statusCode,
  105. this.responseData,
  106. this.error,
  107. this.duration,
  108. });
  109. String get statusText {
  110. if (error != null) return 'ERROR';
  111. if (statusCode == null) return 'PENDING';
  112. if (statusCode! >= 200 && statusCode! < 300) return 'SUCCESS';
  113. if (statusCode! >= 400) return 'ERROR';
  114. return 'UNKNOWN';
  115. }
  116. Color get statusColor {
  117. switch (statusText) {
  118. case 'SUCCESS':
  119. return Colors.green;
  120. case 'ERROR':
  121. return Colors.red;
  122. case 'PENDING':
  123. return Colors.orange;
  124. default:
  125. return Colors.grey;
  126. }
  127. }
  128. }
  129. /// 简化版API监控拦截器
  130. class SimpleApiMonitorInterceptor extends dio.Interceptor {
  131. final Map<String, DateTime> _requestStartTimes = {};
  132. @override
  133. void onRequest(
  134. dio.RequestOptions options,
  135. dio.RequestInterceptorHandler handler,
  136. ) {
  137. final requestId = _generateRequestId();
  138. _requestStartTimes[requestId] = DateTime.now();
  139. final request = ApiRequestInfo(
  140. id: requestId,
  141. timestamp: DateTime.now(),
  142. method: options.method,
  143. url: options.uri.toString(),
  144. headers: options.headers,
  145. requestData: options.data is FormData
  146. ? options.data.fields
  147. : _decryptData(options.data),
  148. );
  149. IXDeveloperTools.addApiRequest(request);
  150. handler.next(options);
  151. }
  152. @override
  153. void onResponse(
  154. dio.Response response,
  155. dio.ResponseInterceptorHandler handler,
  156. ) {
  157. final requestId = _findRequestId(response.requestOptions.uri.toString());
  158. if (requestId != null) {
  159. final startTime = _requestStartTimes.remove(requestId);
  160. final duration = startTime != null
  161. ? DateTime.now().difference(startTime)
  162. : null;
  163. final updatedRequest = ApiRequestInfo(
  164. id: requestId,
  165. timestamp: DateTime.now(),
  166. method: response.requestOptions.method,
  167. url: response.requestOptions.uri.toString(),
  168. headers: response.requestOptions.headers,
  169. requestData: response.requestOptions.data is FormData
  170. ? response.requestOptions.data.fields
  171. : _decryptData(response.requestOptions.data),
  172. statusCode: response.statusCode,
  173. responseData: _decryptData(response.data),
  174. duration: duration,
  175. );
  176. _updateApiRequest(updatedRequest);
  177. }
  178. handler.next(response);
  179. }
  180. @override
  181. void onError(dio.DioException err, dio.ErrorInterceptorHandler handler) {
  182. final requestId = _findRequestId(err.requestOptions.uri.toString());
  183. if (requestId != null) {
  184. final startTime = _requestStartTimes.remove(requestId);
  185. final duration = startTime != null
  186. ? DateTime.now().difference(startTime)
  187. : null;
  188. final updatedRequest = ApiRequestInfo(
  189. id: requestId,
  190. timestamp: DateTime.now(),
  191. method: err.requestOptions.method,
  192. url: err.requestOptions.uri.toString(),
  193. headers: err.requestOptions.headers,
  194. requestData: err.requestOptions.data is FormData
  195. ? err.requestOptions.data.fields
  196. : _decryptData(err.requestOptions.data),
  197. statusCode: err.response?.statusCode,
  198. error: err.message,
  199. duration: duration,
  200. );
  201. _updateApiRequest(updatedRequest);
  202. }
  203. handler.next(err);
  204. }
  205. dynamic _decryptData(dynamic encryptedData) {
  206. try {
  207. // 1. Base64 编码
  208. final base64Data = base64Encode(encryptedData);
  209. // 2. AES 解密
  210. final decryptedBytes = Crypto.decryptBytes(
  211. base64Data,
  212. Keys.aesKey,
  213. Keys.aesIv,
  214. );
  215. // 3. GZip 解压
  216. final gzipDecoder = GZipDecoder();
  217. final decompressedData = gzipDecoder.decodeBytes(decryptedBytes);
  218. // 4. UTF-8 解码
  219. final data = utf8.decode(decompressedData);
  220. // string to json
  221. return jsonDecode(data);
  222. } catch (e) {
  223. return encryptedData;
  224. }
  225. }
  226. String _generateRequestId() {
  227. return '${DateTime.now().millisecondsSinceEpoch}_${IXDeveloperTools.apiRequests.length}';
  228. }
  229. String? _findRequestId(String url) {
  230. try {
  231. final request = IXDeveloperTools.apiRequests.firstWhere(
  232. (r) => r.url == url && r.statusCode == null,
  233. );
  234. return request.id;
  235. } catch (e) {
  236. return null;
  237. }
  238. }
  239. void _updateApiRequest(ApiRequestInfo updatedRequest) {
  240. final requests = IXDeveloperTools._instance._apiRequests;
  241. final index = requests.indexWhere((r) => r.id == updatedRequest.id);
  242. if (index != -1) {
  243. requests[index] = updatedRequest;
  244. }
  245. }
  246. }
  247. class SimpleNoSignApiMonitorInterceptor extends dio.Interceptor {
  248. final Map<String, DateTime> _requestStartTimes = {};
  249. @override
  250. void onRequest(
  251. dio.RequestOptions options,
  252. dio.RequestInterceptorHandler handler,
  253. ) {
  254. final requestId = _generateRequestId();
  255. _requestStartTimes[requestId] = DateTime.now();
  256. final request = ApiRequestInfo(
  257. id: requestId,
  258. timestamp: DateTime.now(),
  259. method: options.method,
  260. url: options.uri.toString(),
  261. headers: options.headers,
  262. requestData: options.data,
  263. );
  264. IXDeveloperTools.addApiRequest(request);
  265. handler.next(options);
  266. }
  267. @override
  268. void onResponse(
  269. dio.Response response,
  270. dio.ResponseInterceptorHandler handler,
  271. ) {
  272. final requestId = _findRequestId(response.requestOptions.uri.toString());
  273. if (requestId != null) {
  274. final startTime = _requestStartTimes.remove(requestId);
  275. final duration = startTime != null
  276. ? DateTime.now().difference(startTime)
  277. : null;
  278. final updatedRequest = ApiRequestInfo(
  279. id: requestId,
  280. timestamp: DateTime.now(),
  281. method: response.requestOptions.method,
  282. url: response.requestOptions.uri.toString(),
  283. headers: response.requestOptions.headers,
  284. requestData: response.requestOptions.data,
  285. statusCode: response.statusCode,
  286. responseData: response.data,
  287. duration: duration,
  288. );
  289. _updateApiRequest(updatedRequest);
  290. }
  291. handler.next(response);
  292. }
  293. @override
  294. void onError(dio.DioException err, dio.ErrorInterceptorHandler handler) {
  295. final requestId = _findRequestId(err.requestOptions.uri.toString());
  296. if (requestId != null) {
  297. final startTime = _requestStartTimes.remove(requestId);
  298. final duration = startTime != null
  299. ? DateTime.now().difference(startTime)
  300. : null;
  301. final updatedRequest = ApiRequestInfo(
  302. id: requestId,
  303. timestamp: DateTime.now(),
  304. method: err.requestOptions.method,
  305. url: err.requestOptions.uri.toString(),
  306. headers: err.requestOptions.headers,
  307. requestData: err.requestOptions.data,
  308. statusCode: err.response?.statusCode,
  309. error: err.message,
  310. duration: duration,
  311. );
  312. _updateApiRequest(updatedRequest);
  313. }
  314. handler.next(err);
  315. }
  316. String _generateRequestId() {
  317. return '${DateTime.now().millisecondsSinceEpoch}_${IXDeveloperTools.apiRequests.length}';
  318. }
  319. String? _findRequestId(String url) {
  320. try {
  321. final request = IXDeveloperTools.apiRequests.firstWhere(
  322. (r) => r.url == url && r.statusCode == null,
  323. );
  324. return request.id;
  325. } catch (e) {
  326. return null;
  327. }
  328. }
  329. void _updateApiRequest(ApiRequestInfo updatedRequest) {
  330. final requests = IXDeveloperTools._instance._apiRequests;
  331. final index = requests.indexWhere((r) => r.id == updatedRequest.id);
  332. if (index != -1) {
  333. requests[index] = updatedRequest;
  334. }
  335. }
  336. }
  337. /// 简化版开发者工具主界面
  338. class SimpleDeveloperToolsScreen extends StatefulWidget {
  339. const SimpleDeveloperToolsScreen({super.key});
  340. @override
  341. State<SimpleDeveloperToolsScreen> createState() =>
  342. _SimpleDeveloperToolsScreenState();
  343. }
  344. class _SimpleDeveloperToolsScreenState extends State<SimpleDeveloperToolsScreen>
  345. with SingleTickerProviderStateMixin {
  346. late TabController _tabController;
  347. @override
  348. void initState() {
  349. super.initState();
  350. _tabController = TabController(length: 2, vsync: this);
  351. _tabController.index = 1;
  352. }
  353. @override
  354. void dispose() {
  355. _tabController.dispose();
  356. super.dispose();
  357. }
  358. @override
  359. Widget build(BuildContext context) {
  360. return Scaffold(
  361. backgroundColor: Get.reactiveTheme.scaffoldBackgroundColor,
  362. appBar: AppBar(
  363. elevation: 0,
  364. iconTheme: IconThemeData(
  365. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  366. ),
  367. title: Text(
  368. '开发者工具',
  369. style: TextStyle(
  370. fontWeight: FontWeight.w600,
  371. fontSize: 17,
  372. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  373. ),
  374. ),
  375. bottom: PreferredSize(
  376. preferredSize: const Size.fromHeight(44),
  377. child: Container(
  378. margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
  379. decoration: BoxDecoration(
  380. color: Colors.grey[100],
  381. borderRadius: BorderRadius.circular(8),
  382. ),
  383. child: TabBar(
  384. controller: _tabController,
  385. indicator: BoxDecoration(
  386. color: Colors.black87,
  387. borderRadius: BorderRadius.circular(6),
  388. ),
  389. indicatorSize: TabBarIndicatorSize.tab,
  390. dividerColor: Colors.transparent,
  391. labelColor: Colors.white,
  392. unselectedLabelColor: Colors.grey[600],
  393. labelStyle: const TextStyle(
  394. fontWeight: FontWeight.w500,
  395. fontSize: 13,
  396. ),
  397. unselectedLabelStyle: const TextStyle(
  398. fontWeight: FontWeight.w400,
  399. fontSize: 13,
  400. ),
  401. indicatorPadding: const EdgeInsets.all(3),
  402. tabs: const [
  403. Tab(text: '日志'),
  404. Tab(text: 'API'),
  405. ],
  406. ),
  407. ),
  408. ),
  409. ),
  410. body: TabBarView(
  411. controller: _tabController,
  412. children: const [SimpleConsoleLogTab(), SimpleApiMonitorTab()],
  413. ),
  414. );
  415. }
  416. }
  417. /// 简化版开发者工具对话框
  418. class SimpleDeveloperToolsDialog extends StatelessWidget {
  419. const SimpleDeveloperToolsDialog({super.key});
  420. @override
  421. Widget build(BuildContext context) {
  422. return Dialog(
  423. backgroundColor: Colors.transparent,
  424. elevation: 0,
  425. child: Container(
  426. width: Get.width * 0.95,
  427. height: Get.height * 0.9,
  428. decoration: BoxDecoration(
  429. color: Colors.white,
  430. borderRadius: BorderRadius.circular(12),
  431. ),
  432. child: ClipRRect(
  433. borderRadius: BorderRadius.circular(12),
  434. child: const SimpleDeveloperToolsScreen(),
  435. ),
  436. ),
  437. );
  438. }
  439. }
  440. /// 简化版控制台日志标签页
  441. class SimpleConsoleLogTab extends StatefulWidget {
  442. const SimpleConsoleLogTab({super.key});
  443. @override
  444. State<SimpleConsoleLogTab> createState() => _SimpleConsoleLogTabState();
  445. }
  446. class _SimpleConsoleLogTabState extends State<SimpleConsoleLogTab> {
  447. final TextEditingController _searchController = TextEditingController();
  448. List<ConsoleLogEntry> _filteredLogs = [];
  449. @override
  450. void initState() {
  451. super.initState();
  452. _updateLogs();
  453. }
  454. void _updateLogs() {
  455. setState(() {
  456. final allLogs = IXDeveloperTools.logs;
  457. if (_searchController.text.isEmpty) {
  458. _filteredLogs = allLogs;
  459. } else {
  460. _filteredLogs = allLogs
  461. .where(
  462. (log) =>
  463. log.message.toLowerCase().contains(
  464. _searchController.text.toLowerCase(),
  465. ) ||
  466. log.tag.toLowerCase().contains(
  467. _searchController.text.toLowerCase(),
  468. ),
  469. )
  470. .toList();
  471. }
  472. });
  473. }
  474. @override
  475. Widget build(BuildContext context) {
  476. return Column(
  477. children: [
  478. // 搜索和操作栏
  479. Container(
  480. padding: const EdgeInsets.all(12),
  481. child: Row(
  482. children: [
  483. // 搜索框
  484. Expanded(
  485. child: Container(
  486. height: 36,
  487. decoration: BoxDecoration(
  488. color: Colors.grey[100],
  489. borderRadius: BorderRadius.circular(8),
  490. ),
  491. child: TextField(
  492. controller: _searchController,
  493. style: const TextStyle(fontSize: 13),
  494. decoration: InputDecoration(
  495. hintText: '搜索...',
  496. hintStyle: TextStyle(
  497. color: Colors.grey[400],
  498. fontSize: 13,
  499. ),
  500. prefixIcon: Icon(
  501. Icons.search,
  502. color: Colors.grey[400],
  503. size: 18,
  504. ),
  505. border: InputBorder.none,
  506. isDense: true,
  507. contentPadding: const EdgeInsets.symmetric(
  508. vertical: 8,
  509. horizontal: 0,
  510. ),
  511. ),
  512. onChanged: (_) => _updateLogs(),
  513. ),
  514. ),
  515. ),
  516. const SizedBox(width: 8),
  517. // 刷新按钮
  518. _buildIconButton(Icons.refresh, _updateLogs),
  519. const SizedBox(width: 6),
  520. // 清除按钮
  521. _buildIconButton(Icons.delete_outline, () {
  522. IXDeveloperTools.clearLogs();
  523. _updateLogs();
  524. }),
  525. ],
  526. ),
  527. ),
  528. // 日志列表
  529. Expanded(
  530. child: _filteredLogs.isEmpty
  531. ? Center(
  532. child: Column(
  533. mainAxisSize: MainAxisSize.min,
  534. children: [
  535. Icon(
  536. Icons.inbox_outlined,
  537. size: 48,
  538. color: Colors.grey[300],
  539. ),
  540. const SizedBox(height: 12),
  541. Text(
  542. '暂无日志',
  543. style: TextStyle(fontSize: 14, color: Colors.grey[400]),
  544. ),
  545. ],
  546. ),
  547. )
  548. : ListView.separated(
  549. padding: const EdgeInsets.symmetric(horizontal: 12),
  550. itemCount: _filteredLogs.length,
  551. separatorBuilder: (_, __) => const SizedBox(height: 1),
  552. itemBuilder: (context, index) {
  553. final log = _filteredLogs[index];
  554. return SimpleLogCard(log: log);
  555. },
  556. ),
  557. ),
  558. ],
  559. );
  560. }
  561. Widget _buildIconButton(IconData icon, VoidCallback onTap) {
  562. return InkWell(
  563. onTap: onTap,
  564. borderRadius: BorderRadius.circular(8),
  565. child: Container(
  566. width: 36,
  567. height: 36,
  568. decoration: BoxDecoration(
  569. color: Colors.grey[100],
  570. borderRadius: BorderRadius.circular(8),
  571. ),
  572. child: Icon(icon, size: 18, color: Colors.grey[600]),
  573. ),
  574. );
  575. }
  576. }
  577. /// 简化版API监控标签页
  578. class SimpleApiMonitorTab extends StatefulWidget {
  579. const SimpleApiMonitorTab({super.key});
  580. @override
  581. State<SimpleApiMonitorTab> createState() => _SimpleApiMonitorTabState();
  582. }
  583. class _SimpleApiMonitorTabState extends State<SimpleApiMonitorTab> {
  584. @override
  585. Widget build(BuildContext context) {
  586. final requests = IXDeveloperTools.apiRequests;
  587. return Column(
  588. children: [
  589. // 操作栏
  590. Container(
  591. padding: const EdgeInsets.all(12),
  592. child: Row(
  593. children: [
  594. // 统计信息
  595. Text(
  596. '${requests.length} 个请求',
  597. style: TextStyle(fontSize: 13, color: Colors.grey[600]),
  598. ),
  599. const Spacer(),
  600. // 刷新按钮
  601. _buildIconButton(Icons.refresh, () => setState(() {})),
  602. const SizedBox(width: 6),
  603. // 清除按钮
  604. _buildIconButton(Icons.delete_outline, () {
  605. IXDeveloperTools.clearApiRequests();
  606. setState(() {});
  607. }),
  608. ],
  609. ),
  610. ),
  611. // API请求列表
  612. Expanded(
  613. child: requests.isEmpty
  614. ? Center(
  615. child: Column(
  616. mainAxisSize: MainAxisSize.min,
  617. children: [
  618. Icon(
  619. Icons.cloud_off_outlined,
  620. size: 48,
  621. color: Colors.grey[300],
  622. ),
  623. const SizedBox(height: 12),
  624. Text(
  625. '暂无请求',
  626. style: TextStyle(fontSize: 14, color: Colors.grey[400]),
  627. ),
  628. ],
  629. ),
  630. )
  631. : ListView.separated(
  632. padding: const EdgeInsets.symmetric(horizontal: 12),
  633. itemCount: requests.length,
  634. separatorBuilder: (_, __) => const SizedBox(height: 1),
  635. itemBuilder: (context, index) {
  636. final request = requests[index];
  637. return SimpleApiCard(request: request);
  638. },
  639. ),
  640. ),
  641. ],
  642. );
  643. }
  644. Widget _buildIconButton(IconData icon, VoidCallback onTap) {
  645. return InkWell(
  646. onTap: onTap,
  647. borderRadius: BorderRadius.circular(8),
  648. child: Container(
  649. width: 36,
  650. height: 36,
  651. decoration: BoxDecoration(
  652. color: Colors.grey[100],
  653. borderRadius: BorderRadius.circular(8),
  654. ),
  655. child: Icon(icon, size: 18, color: Colors.grey[600]),
  656. ),
  657. );
  658. }
  659. }