Files
customer-im-client-dev/apps/im_app/lib/features/miniapp/miniapp_webview_page.dart
pp-bot 23fc6b0c86 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>
2026-03-24 12:53:55 +09:00

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,
),
),
);
}
}