#25 MiniAppWebViewPage + MiniAppRouter - webview_flutter 加载 {apiBaseUrl}/miniapp/{appId}/index.html?gameId=...&token=... - MiniAppFloatButton 接收 chatId/chatType,默认打开 WebView - BannerState 新增 appId 字段,由 GameBannerData.appId 填充 #26 open_filex 文件打开 - FileMessageBubble 下载完成后调用 OpenFilex.open(localPath) - 打开失败时 SnackBar 提示 #27 audioplayers 音频播放 - AudioPlaybackService(Notifier):单例 AudioPlayer,togglePlay/pause/seek - AudioMessageBubble 接入:播放态图标切换、进度 mm:ss 显示 #28 video_player + chewie 视频全屏 - VideoPlayerPage:本地文件 / HTTP 双模,chewie 控制栏 - VideoMessageBubble 默认 onTap → push VideoPlayerPage #29 红包领取排行榜详情页 - GET /payment/rp/detail → RpDetailData + RpRecordItem DTO - GetRpDetailUseCase + getRpDetailUseCaseProvider - RedEnvelopeDetailSheet:汇总行 + 领取排行列表,头像/昵称/金额/时间 #30 MINE_RP 地雷红包发包 UI - _RpType 新增 mine(MINE_RP),显示地雷金额输入框 - SendRpRequest.parameters 携带 mineAmount - RedEnvelopeBubble:非活跃状态直接打开详情,活跃状态领取后打开排行榜 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
4.5 KiB
Dart
167 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
|
|
import 'package:im_app/app/di/network_provider.dart';
|
|
import 'package:im_app/core/foundation/config.dart';
|
|
|
|
/// 小程序 WebView 页
|
|
///
|
|
/// 对应 Gitea issue #25 / iOS ChatRoomMiniAppFloatButton → WebView
|
|
///
|
|
/// ## URL 构造
|
|
///
|
|
/// `{apiBaseUrl}/miniapp/{appId}/index.html?gameId={gameId}&token={token}&chatId={chatId}&chatType={chatType}`
|
|
///
|
|
/// ## 使用
|
|
///
|
|
/// ```dart
|
|
/// MiniAppRouter.open(
|
|
/// context, ref,
|
|
/// gameId: state.gameId,
|
|
/// appId: state.appId,
|
|
/// chatId: chatId,
|
|
/// chatType: chatType,
|
|
/// );
|
|
/// ```
|
|
class MiniAppWebViewPage extends ConsumerStatefulWidget {
|
|
const MiniAppWebViewPage({
|
|
super.key,
|
|
required this.gameId,
|
|
required this.appId,
|
|
required this.chatId,
|
|
required this.chatType,
|
|
this.title = '游戏',
|
|
});
|
|
|
|
final String gameId;
|
|
final String appId;
|
|
final int chatId;
|
|
final int chatType;
|
|
final String title;
|
|
|
|
@override
|
|
ConsumerState<MiniAppWebViewPage> createState() => _MiniAppWebViewPageState();
|
|
}
|
|
|
|
class _MiniAppWebViewPageState extends ConsumerState<MiniAppWebViewPage> {
|
|
late final WebViewController _controller;
|
|
bool _isLoading = true;
|
|
String? _errorMsg;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initController();
|
|
}
|
|
|
|
void _initController() {
|
|
final token = ref.read(apiConfigProvider).token ?? '';
|
|
final url = _buildUrl(token);
|
|
|
|
_controller = WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setBackgroundColor(Colors.black)
|
|
..setNavigationDelegate(
|
|
NavigationDelegate(
|
|
onPageStarted: (_) {
|
|
if (mounted) setState(() { _isLoading = true; _errorMsg = null; });
|
|
},
|
|
onPageFinished: (_) {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
},
|
|
onWebResourceError: (error) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMsg = '加载失败:${error.description}';
|
|
});
|
|
}
|
|
},
|
|
),
|
|
)
|
|
..loadRequest(Uri.parse(url));
|
|
}
|
|
|
|
String _buildUrl(String token) {
|
|
var base = AppConfig.apiBaseUrl;
|
|
if (base.endsWith('/')) base = base.substring(0, base.length - 1);
|
|
final appId = widget.appId.isNotEmpty ? widget.appId : widget.gameId;
|
|
return '$base/miniapp/$appId/index.html'
|
|
'?gameId=${Uri.encodeComponent(widget.gameId)}'
|
|
'&token=${Uri.encodeComponent(token)}'
|
|
'&chatId=${widget.chatId}'
|
|
'&chatType=${widget.chatType}';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.title),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _initController,
|
|
),
|
|
],
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
WebViewWidget(controller: _controller),
|
|
if (_isLoading)
|
|
const Center(child: CircularProgressIndicator()),
|
|
if (_errorMsg != null)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.error_outline, size: 48, color: Colors.grey),
|
|
const SizedBox(height: 12),
|
|
Text(_errorMsg!, textAlign: TextAlign.center),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _initController,
|
|
child: const Text('重试'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 小程序路由入口
|
|
///
|
|
/// 通过 [Navigator] push [MiniAppWebViewPage],保留在当前 ProviderScope 中。
|
|
class MiniAppRouter {
|
|
MiniAppRouter._();
|
|
|
|
static void open(
|
|
BuildContext context,
|
|
WidgetRef ref, {
|
|
required String gameId,
|
|
String appId = '',
|
|
required int chatId,
|
|
required int chatType,
|
|
String title = '游戏',
|
|
}) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => MiniAppWebViewPage(
|
|
gameId: gameId,
|
|
appId: appId.isNotEmpty ? appId : gameId,
|
|
chatId: chatId,
|
|
chatType: chatType,
|
|
title: title,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|