#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>
402 lines
11 KiB
Dart
402 lines
11 KiB
Dart
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);
|
||
}
|
||
}
|