262 lines
8.4 KiB
Dart
262 lines
8.4 KiB
Dart
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<DemoPage> createState() => _DemoPageState();
|
||
}
|
||
|
||
class _DemoPageState extends State<DemoPage> {
|
||
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: <Widget>[
|
||
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<void> 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<Widget> 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: <Widget>[
|
||
Text(
|
||
'本地缓存事件数:${_viewModel.cacheCount}',
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 8),
|
||
SectionCard(title: '配置摘要', children: <Widget>[_buildConfigSummary()]),
|
||
SectionCard(
|
||
title: '基础操作',
|
||
children: <Widget>[
|
||
_buildActionButtons(<Widget>[
|
||
_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: <Widget>[
|
||
SwitchListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
title: const Text('压力测试详细日志'),
|
||
subtitle: const Text('关闭后仅输出结果与异常'),
|
||
value: _viewModel.verboseStressLogs,
|
||
onChanged: (value) => _viewModel.setVerboseStressLogs(value),
|
||
),
|
||
_buildActionButtons(<Widget>[
|
||
_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: <Widget>[
|
||
TextField(
|
||
controller: _mockBaseUrlController,
|
||
decoration: const InputDecoration(
|
||
labelText: '5xx/Mock BaseUrl(可选)',
|
||
hintText: 'http://localhost:8080',
|
||
),
|
||
onChanged: _viewModel.setMockBaseUrl,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_buildActionButtons(<Widget>[
|
||
_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: <Widget>[
|
||
_buildActionButtons(<Widget>[
|
||
_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: <Widget>[
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|