import 'package:flutter/material.dart'; import 'package:yx_tracking_flutter/yx_tracking_flutter.dart'; import '../service/analytics_service.dart'; import '../view_model/demo_view_model.dart'; import 'widgets.dart'; class DemoPage extends StatefulWidget { const DemoPage({super.key, required this.initialConfig}); final AnalyticsConfig initialConfig; @override State createState() => _DemoPageState(); } class _DemoPageState extends State { late DemoViewModel _viewModel; final TextEditingController _mockBaseUrlController = TextEditingController(); final ScrollController _logScrollController = ScrollController(); int _lastLogCount = 0; @override void initState() { super.initState(); _viewModel = DemoViewModel( analytics: const AnalyticsService(), initialConfig: widget.initialConfig, )..init(); _mockBaseUrlController.text = _viewModel.mockBaseUrl; _viewModel.addListener(_handleViewModelChanged); } @override void dispose() { _viewModel.removeListener(_handleViewModelChanged); _viewModel.dispose(); _mockBaseUrlController.dispose(); _logScrollController.dispose(); super.dispose(); } void _handleViewModelChanged() { if (!mounted) { return; } if (_viewModel.logs.length != _lastLogCount) { _lastLogCount = _viewModel.logs.length; WidgetsBinding.instance.addPostFrameCallback((_) { if (_logScrollController.hasClients) { _logScrollController.jumpTo( _logScrollController.position.maxScrollExtent, ); } }); } setState(() {}); } Widget _buildConfigSummary() { final config = _viewModel.activeConfig; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Endpoint: ${config.endpointBaseUrl}'), Text('systemCode: ${config.systemCode}'), Text('allowInsecureHttp: ${config.allowInsecureHttp}'), Text('useIsolateStorage: ${config.useIsolateStorage}'), Text('maxEventAge: ${config.maxEventAge.inSeconds}s'), ], ); } Widget _actionButton({ required String key, required String label, required Future Function() action, bool outlined = true, bool suppressSdkLogs = false, }) { return ActionButton( label: label, outlined: outlined, running: _viewModel.isRunning(key), onPressed: _viewModel.isRunning(key) ? null : () => _viewModel.runAction( key, label, action, suppressSdkLogs: suppressSdkLogs, ), ); } Widget _buildActionButtons(List buttons) { return Wrap(spacing: 12, runSpacing: 12, children: buttons); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('YX Tracking Demo')), body: ListView( padding: const EdgeInsets.all(16), children: [ Text( '本地缓存事件数:${_viewModel.cacheCount}', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), SectionCard(title: '配置摘要', children: [_buildConfigSummary()]), SectionCard( title: '基础操作', children: [ _buildActionButtons([ _actionButton( key: 'track_demo', label: 'Track 演示事件', action: _viewModel.trackDemoEvent, outlined: false, ), _actionButton( key: 'flush', label: '立即上报', action: _viewModel.flushNow, outlined: false, ), _actionButton( key: 'refresh_config', label: '刷新配置', action: _viewModel.refreshConfig, ), ]), const SizedBox(height: 8), const Text( '说明:已对接 SDK-TEST-FLUTTER 系统;若为 HTTP 联调,请保持 allowInsecureHttp=true。', ), ], ), SectionCard( title: '压力测试', children: [ SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('压力测试详细日志'), subtitle: const Text('关闭后仅输出结果与异常'), value: _viewModel.verboseStressLogs, onChanged: (value) => _viewModel.setVerboseStressLogs(value), ), _buildActionButtons([ _actionButton( key: 'stress_seq', label: '串行 Track 1000', action: _viewModel.runStressSequential, suppressSdkLogs: !_viewModel.verboseStressLogs, ), _actionButton( key: 'stress_con', label: '并发 Track 1000', action: _viewModel.runStressConcurrent, suppressSdkLogs: !_viewModel.verboseStressLogs, ), _actionButton( key: 'continuous', label: '持续 Track + Flush (30秒)', action: _viewModel.runContinuousTrackFlush, suppressSdkLogs: !_viewModel.verboseStressLogs, ), ]), ], ), SectionCard( title: '错误模拟', children: [ TextField( controller: _mockBaseUrlController, decoration: const InputDecoration( labelText: '5xx/Mock BaseUrl(可选)', hintText: 'http://localhost:8080', ), onChanged: _viewModel.setMockBaseUrl, ), const SizedBox(height: 8), _buildActionButtons([ _actionButton( key: 'invalid_event', label: '非法事件 (缺少标签)', action: _viewModel.trackInvalidEvent, ), _actionButton( key: 'timeout', label: '模拟网络超时', action: _viewModel.simulateTimeout, ), _actionButton( key: 'server_5xx', label: '模拟服务器 5xx', action: _viewModel.simulateServerError, ), ]), ], ), SectionCard( title: '边界用例', children: [ _buildActionButtons([ _actionButton( key: 'empty_params', label: '空参数', action: _viewModel.trackEmptyParams, ), _actionButton( key: 'large_payload', label: '大载荷 (100KB)', action: _viewModel.trackLargePayload, ), _actionButton( key: 'special_chars', label: '特殊字符', action: _viewModel.trackSpecialChars, ), _actionButton( key: 'rapid_init', label: '快速初始化/销毁 (x5)', action: _viewModel.rapidInitDispose, ), _actionButton( key: 'expiration', label: '测试过期 (1秒)', action: _viewModel.testExpiration, ), ]), ], ), SectionCard( title: '状态与日志', children: [ const Text('最近事件(最多 20 条)'), const SizedBox(height: 8), SizedBox( height: 220, child: RecentEventList(items: _viewModel.recent), ), const SizedBox(height: 12), const Text('日志输出'), const SizedBox(height: 8), SizedBox( height: 160, child: LogList( logs: _viewModel.logs, controller: _logScrollController, ), ), ], ), ], ), ); } }