feat: WebView/媒体/红包详情全量实现 (#25~#30)
#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>
This commit is contained in:
166
apps/im_app/lib/features/miniapp/miniapp_webview_page.dart
Normal file
166
apps/im_app/lib/features/miniapp/miniapp_webview_page.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user