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:
pp-bot
2026-03-24 12:53:55 +09:00
parent e715a0673b
commit 23fc6b0c86
17 changed files with 1068 additions and 59 deletions

View File

@@ -18,7 +18,7 @@ class SendRpData {
/// 发送红包请求
///
/// 对应 Gitea issue #19 + #22
/// 对应 Gitea issue #19 + #22 + #30
///
/// ## currencyType 规则(#19 bug fix
///
@@ -26,6 +26,10 @@ class SendRpData {
/// Flutter 修复:`currencyType` 由调用方传入UseCase 层根据 workspaceId 动态决定:
/// - workspaceId > 0 → 从 `/workspace/workspace/get` 取 workspace.currency
/// - workspaceId == 0 → 默认 `"PEA"`
///
/// ## mineAmount#30 MINE_RP
///
/// 地雷红包时附带 `mineAmount` 参数null 时不传。
class SendRpRequest extends ApiRequestable<SendRpData> {
final String amount;
final String currencyType;
@@ -36,6 +40,7 @@ class SendRpRequest extends ApiRequestable<SendRpData> {
final int rpNum;
final String remark;
final int msgSendTime;
final String? mineAmount;
const SendRpRequest({
required this.amount,
@@ -47,6 +52,7 @@ class SendRpRequest extends ApiRequestable<SendRpData> {
required this.rpNum,
required this.remark,
required this.msgSendTime,
this.mineAmount,
});
@override
@@ -66,6 +72,8 @@ class SendRpRequest extends ApiRequestable<SendRpData> {
'rpNum': rpNum,
'remark': remark,
'msgSendTime': msgSendTime,
if (mineAmount != null && mineAmount!.isNotEmpty)
'mineAmount': mineAmount,
};
@override
@@ -268,6 +276,107 @@ class WorkspaceData {
}
}
// ── 红包领取详情 ───────────────────────────────────────────────────────────────
/// 单条领取记录
class RpRecordItem {
final int userId;
final String nickname;
final String avatarUrl;
final String amount;
final int grabTime;
const RpRecordItem({
required this.userId,
required this.nickname,
required this.avatarUrl,
required this.amount,
required this.grabTime,
});
factory RpRecordItem.fromJson(Map<String, dynamic> json) {
return RpRecordItem(
userId: json['userId'] as int? ?? 0,
nickname: json['nickname'] as String? ?? '',
avatarUrl: json['avatarUrl'] as String? ?? '',
amount: (json['amount'] ?? '0').toString(),
grabTime: json['grabTime'] as int? ?? 0,
);
}
}
/// 红包领取详情响应
///
/// 对应 Gitea issue #29
class RpDetailData {
final String rpId;
final String rpType;
final String remark;
final String totalAmount;
final int totalNum;
final String receivedAmount;
final int receivedNum;
final int status;
final List<RpRecordItem> records;
const RpDetailData({
required this.rpId,
required this.rpType,
required this.remark,
required this.totalAmount,
required this.totalNum,
required this.receivedAmount,
required this.receivedNum,
required this.status,
required this.records,
});
factory RpDetailData.fromJson(Map<String, dynamic> json) {
final raw = json['records'];
final records = raw is List
? raw.whereType<Map<String, dynamic>>().map(RpRecordItem.fromJson).toList()
: <RpRecordItem>[];
return RpDetailData(
rpId: json['rpId'] as String? ?? '',
rpType: json['rpType'] as String? ?? '',
remark: json['remark'] as String? ?? '恭喜发财',
totalAmount: (json['totalAmount'] ?? '0').toString(),
totalNum: json['totalNum'] as int? ?? 0,
receivedAmount: (json['receivedAmount'] ?? '0').toString(),
receivedNum: json['receivedNum'] as int? ?? 0,
status: json['status'] as int? ?? 0,
records: records,
);
}
}
/// 获取红包领取详情请求
///
/// GET /payment/rp/detail?rpId=xxx
class GetRpDetailRequest extends ApiRequestable<RpDetailData> {
final String rpId;
const GetRpDetailRequest({required this.rpId});
@override
String get path => ApiPaths.rpDetail;
@override
HttpMethod get method => HttpMethod.get;
@override
Map<String, dynamic> get parameters => {'rpId': rpId};
@override
RpDetailData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return RpDetailData.fromJson(data);
}
}
// ── Workspace ─────────────────────────────────────────────────────────────────
/// 获取 Workspace 信息请求
class GetWorkspaceRequest extends ApiRequestable<WorkspaceData> {
final int workspaceId;