Files
customer-im-client-dev/apps/im_app/lib/data/remote/red_envelope_request.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

402 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
// ── 发送红包 ──────────────────────────────────────────────────────────────────
/// 发送红包响应
class SendRpData {
final String rpId;
const SendRpData({required this.rpId});
factory SendRpData.fromJson(Map<String, dynamic> json) {
return SendRpData(
rpId: (json['rpID'] ?? json['rp_id'] ?? '').toString(),
);
}
}
/// 发送红包请求
///
/// 对应 Gitea issue #19 + #22 + #30
///
/// ## currencyType 规则(#19 bug fix
///
/// iOS 硬编码 `"PEA"` → `code=150001` 错误workspace 群使用 USDT
/// 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;
final int chatId;
final int chatType;
final String rpType;
final List<int> recipientIds;
final int rpNum;
final String remark;
final int msgSendTime;
final String? mineAmount;
const SendRpRequest({
required this.amount,
required this.currencyType,
required this.chatId,
required this.chatType,
required this.rpType,
required this.recipientIds,
required this.rpNum,
required this.remark,
required this.msgSendTime,
this.mineAmount,
});
@override
String get path => ApiPaths.rpSend;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {
'amount': amount,
'currencyType': currencyType,
'chatID': chatId,
'chatType': chatType,
'rpType': rpType,
'recipientIDs': recipientIds,
'rpNum': rpNum,
'remark': remark,
'msgSendTime': msgSendTime,
if (mineAmount != null && mineAmount!.isNotEmpty)
'mineAmount': mineAmount,
};
@override
SendRpData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return SendRpData.fromJson(data);
}
}
// ── 领取红包 ──────────────────────────────────────────────────────────────────
/// 领取红包响应
class ReceiveRpData {
final bool grabFlag;
final String amount;
final int rpStatus;
const ReceiveRpData({
required this.grabFlag,
required this.amount,
required this.rpStatus,
});
factory ReceiveRpData.fromJson(Map<String, dynamic> json) {
return ReceiveRpData(
grabFlag: json['grabFlag'] as bool? ?? false,
amount: (json['amount'] ?? '0').toString(),
rpStatus: json['rpStatus'] as int? ?? 0,
);
}
}
/// 领取红包请求
///
/// 对应 Gitea issue #21
///
/// ⚠️ 必须 JSON typed body非 form 字符串),否则 server 返回 code=30007。
/// `supportMask: true` + `supportHideTail: true` 为必填标志位。
class ReceiveRpRequest extends ApiRequestable<ReceiveRpData> {
final String rpId;
final int chatId;
final String rpType;
final int sendRpMsgId;
const ReceiveRpRequest({
required this.rpId,
required this.chatId,
required this.rpType,
required this.sendRpMsgId,
});
@override
String get path => ApiPaths.rpReceive;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {
'rpID': rpId,
'chatID': chatId,
'rpType': rpType,
'sendRpMsgID': sendRpMsgId,
'supportMask': true,
'supportHideTail': true,
};
@override
ReceiveRpData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return ReceiveRpData.fromJson(data);
}
}
// ── 游戏横幅 ──────────────────────────────────────────────────────────────────
/// 游戏横幅响应
class GameBannerData {
final String gameId;
final String gameName;
final String? appId;
final GameCurrentRound? currentRound;
final GameLastRound? lastRound;
const GameBannerData({
required this.gameId,
required this.gameName,
this.appId,
this.currentRound,
this.lastRound,
});
factory GameBannerData.fromJson(Map<String, dynamic> json) {
return GameBannerData(
gameId: json['gameId'] as String? ?? '',
gameName: json['gameName'] as String? ?? '',
appId: json['appid'] as String?,
currentRound: json['currentRound'] is Map<String, dynamic>
? GameCurrentRound.fromJson(json['currentRound'] as Map<String, dynamic>)
: null,
lastRound: json['lastCompletedRound'] is Map<String, dynamic>
? GameLastRound.fromJson(json['lastCompletedRound'] as Map<String, dynamic>)
: null,
);
}
}
class GameCurrentRound {
final String round;
final int? startTime;
final int? closureTime;
final int? drawTime;
final int? serverTime;
const GameCurrentRound({
required this.round,
this.startTime,
this.closureTime,
this.drawTime,
this.serverTime,
});
factory GameCurrentRound.fromJson(Map<String, dynamic> json) {
return GameCurrentRound(
round: json['round'] as String? ?? '',
startTime: json['startTime'] as int?,
closureTime: json['closureTime'] as int?,
drawTime: json['drawTime'] as int?,
serverTime: json['serverTime'] as int?,
);
}
}
class GameLastRound {
final String round;
final String result;
final String? simple;
const GameLastRound({
required this.round,
required this.result,
this.simple,
});
factory GameLastRound.fromJson(Map<String, dynamic> json) {
return GameLastRound(
round: json['round'] as String? ?? '',
result: json['result'] as String? ?? '',
simple: json['simple'] as String?,
);
}
}
/// 获取游戏横幅请求
class FetchBannerRequest extends ApiRequestable<GameBannerData> {
final String gameId;
const FetchBannerRequest({required this.gameId});
@override
String get path => ApiPaths.bannerGet;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {'game_id': gameId};
@override
GameBannerData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return GameBannerData.fromJson(data);
}
}
// ── Workspace ─────────────────────────────────────────────────────────────────
/// Workspace 信息响应(用于获取 currency
class WorkspaceData {
final int id;
final String currency;
final String name;
const WorkspaceData({
required this.id,
required this.currency,
required this.name,
});
factory WorkspaceData.fromJson(Map<String, dynamic> json) {
final ws = json['workspace'] as Map<String, dynamic>? ?? json;
return WorkspaceData(
id: ws['id'] as int? ?? 0,
currency: ws['currency'] as String? ?? 'PEA',
name: ws['name'] as String? ?? '',
);
}
}
// ── 红包领取详情 ───────────────────────────────────────────────────────────────
/// 单条领取记录
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;
const GetWorkspaceRequest({required this.workspaceId});
@override
String get path => ApiPaths.workspaceGet;
@override
HttpMethod get method => HttpMethod.get;
@override
Map<String, dynamic> get parameters => {'id': workspaceId.toString()};
@override
WorkspaceData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return WorkspaceData.fromJson(data);
}
}