feat: init Party Room

This commit is contained in:
xkeyC
2025-11-18 23:10:04 +08:00
parent f98235f2f3
commit aaaee30368
34 changed files with 10999 additions and 146 deletions

View File

@@ -0,0 +1,190 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/provider/party_room.dart';
/// 创建房间对话框
class CreateRoomDialog extends HookConsumerWidget {
const CreateRoomDialog({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final partyRoomState = ref.watch(partyRoomProvider);
final partyRoom = ref.read(partyRoomProvider.notifier);
final selectedMainTag = useState<String?>(null);
final selectedSubTag = useState<String?>(null);
final targetMembersController = useTextEditingController(text: '6');
final hasPassword = useState(false);
final passwordController = useTextEditingController();
final socialLinksController = useTextEditingController();
final isCreating = useState(false);
return ContentDialog(
constraints: const BoxConstraints(maxWidth: 500),
title: const Text('创建房间'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: '房间类型',
child: ComboBox<String>(
placeholder: const Text('选择主标签'),
value: selectedMainTag.value,
items: partyRoomState.room.tags.map((tag) {
return ComboBoxItem(value: tag.id, child: Text(tag.name));
}).toList(),
onChanged: (value) {
selectedMainTag.value = value;
selectedSubTag.value = null;
},
),
),
const SizedBox(height: 12),
if (selectedMainTag.value != null) ...[
InfoLabel(
label: '子标签 (可选)',
child: ComboBox<String>(
placeholder: const Text('选择子标签'),
value: selectedSubTag.value,
items: [
const ComboBoxItem(value: null, child: Text('')),
...partyRoomState.room.tags.firstWhere((tag) => tag.id == selectedMainTag.value).subTags.map((
subTag,
) {
return ComboBoxItem(value: subTag.id, child: Text(subTag.name));
}),
],
onChanged: (value) {
selectedSubTag.value = value;
},
),
),
const SizedBox(height: 12),
],
InfoLabel(
label: '目标人数 (2-600)',
child: TextBox(
controller: targetMembersController,
placeholder: '输入目标人数',
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
checked: hasPassword.value,
onChanged: (value) {
hasPassword.value = value ?? false;
},
content: const Text('设置密码'),
),
],
),
if (hasPassword.value) ...[
const SizedBox(height: 8),
InfoLabel(
label: '房间密码',
child: TextBox(controller: passwordController, placeholder: '输入密码', obscureText: true),
),
],
const SizedBox(height: 12),
InfoLabel(
label: '社交链接 (可选)',
child: TextBox(controller: socialLinksController, placeholder: 'https://discord.gg/xxxxx', maxLines: 1),
),
],
),
),
actions: [
FilledButton(
onPressed: isCreating.value
? null
: () async {
final mainTagId = selectedMainTag.value;
if (mainTagId == null || mainTagId.isEmpty) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('提示'),
content: const Text('请选择房间类型'),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
return;
}
final targetMembers = int.tryParse(targetMembersController.text);
if (targetMembers == null || targetMembers < 2 || targetMembers > 600) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('提示'),
content: const Text('目标人数必须在 2-600 之间'),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
return;
}
if (hasPassword.value && passwordController.text.trim().isEmpty) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('提示'),
content: const Text('请输入密码'),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
return;
}
final socialLinks = socialLinksController.text
.split('\n')
.where((link) => link.trim().isNotEmpty && link.trim().startsWith('http'))
.toList();
isCreating.value = true;
try {
await partyRoom.createRoom(
mainTagId: mainTagId,
subTagId: selectedSubTag.value,
targetMembers: targetMembers,
hasPassword: hasPassword.value,
password: hasPassword.value ? passwordController.text : null,
socialLinks: socialLinks.isEmpty ? null : socialLinks,
);
if (context.mounted) {
Navigator.pop(context);
}
} catch (e) {
isCreating.value = false;
if (context.mounted) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('创建失败'),
content: Text(e.toString()),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
}
}
},
child: isCreating.value
? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2))
: const Text('创建'),
),
Button(onPressed: isCreating.value ? null : () => Navigator.pop(context), child: const Text('取消')),
],
);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
/// 连接服务器页面
class PartyRoomConnectPage extends HookConsumerWidget {
const PartyRoomConnectPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiModel = ref.read(partyRoomUIModelProvider.notifier);
final uiState = ref.watch(partyRoomUIModelProvider);
return ScaffoldPage(
padding: EdgeInsets.zero,
content: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black.withValues(alpha: 0.3), Colors.black.withValues(alpha: 0.6)],
),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Logo 或图标
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1E3A5F).withValues(alpha: 0.6),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: const Color(0xFF4A9EFF).withValues(alpha: 0.3), blurRadius: 30, spreadRadius: 5),
],
),
child: const Icon(FluentIcons.group, size: 64, color: Color(0xFF4A9EFF)),
),
const SizedBox(height: 32),
// 标题
const Text(
'组队大厅',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0), letterSpacing: 2),
),
const SizedBox(height: 12),
// 副标题
Text('正在连接服务器...', style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.7))),
const SizedBox(height: 32),
// 加载动画
const SizedBox(width: 40, height: 40, child: ProgressRing(strokeWidth: 3)),
const SizedBox(height: 32),
if (uiState.errorMessage != null) ...[
Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF3D1E1E).withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFF6B6B), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(FluentIcons.error_badge, color: Color(0xFFFF6B6B), size: 16),
SizedBox(width: 8),
Text(
'连接失败',
style: TextStyle(color: Color(0xFFFF6B6B), fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 8),
Text(uiState.errorMessage!, style: const TextStyle(color: Color(0xFFE0E0E0))),
const SizedBox(height: 12),
FilledButton(
onPressed: () async {
await uiModel.connectToServer();
},
child: const Text('重试'),
),
],
),
),
],
],
),
),
),
);
}
}

View File

@@ -0,0 +1,689 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as partroom;
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
import 'package:starcitizen_doctor/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
import 'package:starcitizen_doctor/widgets/src/cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
/// 房间详情页面 (Discord 样式)
class PartyRoomDetailPage extends ConsumerStatefulWidget {
const PartyRoomDetailPage({super.key});
@override
ConsumerState<PartyRoomDetailPage> createState() => _PartyRoomDetailPageState();
}
class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
final ScrollController _scrollController = ScrollController();
int _lastEventCount = 0;
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
}
@override
Widget build(BuildContext context) {
final partyRoomState = ref.watch(partyRoomProvider);
final partyRoom = ref.read(partyRoomProvider.notifier);
final room = partyRoomState.room.currentRoom;
final members = partyRoomState.room.members;
final isOwner = partyRoomState.room.isOwner;
final events = partyRoomState.room.recentEvents;
// 检测消息数量变化,触发滚动
if (events.length != _lastEventCount) {
_lastEventCount = events.length;
if (events.isNotEmpty) {
_scrollToBottom();
}
}
return ScaffoldPage(
padding: EdgeInsets.zero,
content: Row(
children: [
// 左侧成员列表 (类似 Discord 侧边栏)
Container(
width: 240,
decoration: BoxDecoration(
color: Color(0xFF232428).withValues(alpha: .3),
border: Border(right: BorderSide(color: Colors.black.withValues(alpha: 0.3), width: 1)),
),
child: Column(
children: [
// 房间信息头部
_buildRoomHeader(context, room, members, isOwner, partyRoom),
const Divider(
style: DividerThemeData(thickness: 1, decoration: BoxDecoration(color: Color(0xFF1E1F22))),
),
// 成员列表
Expanded(child: _buildMembersSidebar(context, ref, members, isOwner, partyRoom)),
],
),
),
// 右侧消息区域
Expanded(
child: Column(
children: [
// 消息列表
Expanded(child: _buildMessageList(context, events, _scrollController, ref)),
// 信号发送按钮
_buildSignalSender(context, ref, partyRoom, room),
],
),
),
],
),
);
}
// 房间信息头部
Widget _buildRoomHeader(BuildContext context, dynamic room, List members, bool isOwner, PartyRoom partyRoom) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(FluentIcons.room, size: 16, color: Color(0xFFB5BAC1)),
const SizedBox(width: 8),
Expanded(
child: Text(
room?.ownerGameId ?? '房间',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(FluentIcons.group, size: 12, color: Color(0xFF80848E)),
const SizedBox(width: 4),
Text(
'${members.length}/${room?.targetMembers ?? 0} 成员',
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
),
],
),
if (isOwner) ...[
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: Button(
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
title: const Text('确认解散'),
content: const Text('确定要解散房间吗?所有成员将被移出。'),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
FilledButton(
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
child: const Text('解散', style: TextStyle(color: Colors.white)),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (confirmed == true) {
ref.read(partyRoomUIModelProvider.notifier).dismissRoom();
}
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((state) {
if (state.isHovered || state.isPressed) {
return const Color(0xFFB3261E);
}
return const Color(0xFFDA373C);
}),
),
child: const Text('解散房间', style: TextStyle(color: Colors.white)),
),
),
] else ...[
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: Button(
onPressed: () async {
await partyRoom.leaveRoom();
},
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFF404249))),
child: const Text('离开房间', style: TextStyle(color: Color(0xFFB5BAC1))),
),
),
],
],
),
);
}
IconData _getSocialIcon(String link) {
if (link.contains('qq.com')) return FontAwesomeIcons.qq;
if (link.contains('discord')) return FontAwesomeIcons.discord;
if (link.contains('kook')) return FluentIcons.chat;
return FluentIcons.link;
}
String _getSocialName(String link) {
if (link.contains('discord')) return 'Discord';
if (link.contains('kook')) return 'KOOK';
if (link.contains('qq')) return 'QQ';
return '链接';
}
// 成员侧边栏
Widget _buildMembersSidebar(BuildContext context, WidgetRef ref, List members, bool isOwner, PartyRoom partyRoom) {
if (members.isEmpty) {
return Center(
child: Text('暂无成员', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12)),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return _buildMemberItem(context, ref, member, isOwner, partyRoom);
},
);
}
Widget _buildMemberItem(BuildContext context, WidgetRef ref, RoomMember member, bool isOwner, PartyRoom partyRoom) {
final avatarUrl = member.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${member.avatarUrl}' : null;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
child: HoverButton(
onPressed: isOwner && !member.isOwner ? () => _showMemberContextMenu(context, member, partyRoom) : null,
builder: (context, states) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: states.isHovered ? const Color(0xFF404249) : Colors.transparent,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
// 头像
makeUserAvatar(member.handleName, avatarUrl: avatarUrl, size: 32),
const SizedBox(width: 8),
// 名称和状态
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
member.handleName.isNotEmpty ? member.handleName : member.gameUserId,
style: TextStyle(
fontSize: 13,
color: member.isOwner ? const Color(0xFFFAA81A) : const Color(0xFFDBDEE1),
fontWeight: member.isOwner ? FontWeight.bold : FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
),
),
if (member.isOwner) ...[
const SizedBox(width: 4),
const Icon(FluentIcons.crown, size: 10, color: Color(0xFFFAA81A)),
],
],
),
if (member.status.currentLocation.isNotEmpty)
Text(
member.status.currentLocation,
style: const TextStyle(fontSize: 10, color: Color(0xFF80848E)),
overflow: TextOverflow.ellipsis,
),
],
),
),
// 状态指示器
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF23A559), // 在线绿色
shape: BoxShape.circle,
),
),
],
),
);
},
),
);
}
Widget makeUserAvatar(String memberName, {String? avatarUrl, double size = 32}) {
return SizedBox(
width: size,
height: size,
child: avatarUrl == null
? CircleAvatar(
radius: 16,
backgroundColor: const Color(0xFF5865F2),
child: Text(
memberName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(100),
child: CacheNetImage(url: avatarUrl),
),
);
}
void _showMemberContextMenu(BuildContext context, dynamic member, PartyRoom partyRoom) async {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: Text(member.handleName.isNotEmpty ? member.handleName : member.gameUserId),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
onPressed: () async {
Navigator.pop(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
title: const Text('转移房主'),
content: Text(
'确定要将房主转移给 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?',
),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
FilledButton(child: const Text('转移'), onPressed: () => Navigator.pop(context, true)),
],
),
);
if (confirmed == true) {
await partyRoom.transferOwnership(member.gameUserId);
}
},
child: const Text('转移房主'),
),
const SizedBox(height: 8),
Button(
onPressed: () async {
Navigator.pop(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
title: const Text('踢出成员'),
content: Text('确定要踢出 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?'),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
FilledButton(
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
child: const Text('踢出'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (confirmed == true) {
await partyRoom.kickMember(member.gameUserId);
}
},
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
child: const Text('踢出成员', style: TextStyle(color: Colors.white)),
),
],
),
actions: [Button(child: const Text('关闭'), onPressed: () => Navigator.pop(context))],
),
);
}
// 消息列表
Widget _buildMessageList(BuildContext context, List events, ScrollController scrollController, WidgetRef ref) {
final partyRoomState = ref.watch(partyRoomProvider);
final room = partyRoomState.room.currentRoom;
final hasSocialLinks = room != null && room.socialLinks.isNotEmpty;
// 计算总项数:社交链接消息(如果有)+ 事件消息
final totalItems = (hasSocialLinks ? 1 : 0) + events.length;
if (totalItems == 0) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(FluentIcons.chat, size: 64, color: Color(0xFF404249)),
const SizedBox(height: 16),
Text('暂无消息', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14)),
const SizedBox(height: 4),
Text('发送一条信号开始对话吧!', style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12)),
],
),
);
}
return ListView.builder(
controller: scrollController,
padding: const EdgeInsets.all(16),
itemCount: totalItems,
itemBuilder: (context, index) {
// 第一条消息显示社交链接(如果有)
if (hasSocialLinks && index == 0) {
return _buildSocialLinksMessage(room);
}
// 其他消息显示事件
final eventIndex = hasSocialLinks ? index - 1 : index;
final event = events[eventIndex];
return _buildMessageItem(event, ref);
},
);
}
// 社交链接系统消息
Widget _buildSocialLinksMessage(dynamic room) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF2B2D31),
border: Border.all(color: const Color(0xFF5865F2).withValues(alpha: 0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: const BoxDecoration(color: Color(0xFF5865F2), shape: BoxShape.circle),
child: const Icon(FluentIcons.info, size: 14, color: Colors.white),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'该房间包含第三方社交连接,点击加入一起开黑吧~',
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: room.socialLinks.map<Widget>((link) {
return HyperlinkButton(
onPressed: () => launchUrlString(link),
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.isHovered) return const Color(0xFF4752C4);
return const Color(0xFF5865F2);
}),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getSocialIcon(link), size: 16, color: Colors.white),
const SizedBox(width: 8),
Text(
_getSocialName(link),
style: const TextStyle(fontSize: 13, color: Colors.white, fontWeight: FontWeight.w500),
),
],
),
);
}).toList(),
),
],
),
);
}
Widget _buildMessageItem(dynamic event, WidgetRef ref) {
final roomEvent = event as partroom.RoomEvent;
final isSignal = roomEvent.type == partroom.RoomEventType.SIGNAL_BROADCAST;
final userName = _getEventUserName(roomEvent);
final avatarUrl = _getEventAvatarUrl(roomEvent);
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
makeUserAvatar(userName, avatarUrl: avatarUrl, size: 28),
const SizedBox(width: 12),
// 消息内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
userName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isSignal ? Colors.white : const Color(0xFF80848E),
),
),
const SizedBox(width: 8),
Text(
_formatTime(roomEvent.timestamp),
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
),
],
),
const SizedBox(height: 4),
Text(
_getEventText(roomEvent, ref),
style: TextStyle(
fontSize: 14,
color: isSignal ? const Color(0xFFDBDEE1) : const Color(0xFF949BA4),
fontStyle: isSignal ? FontStyle.normal : FontStyle.italic,
),
),
],
),
),
],
),
);
}
String _getEventUserName(partroom.RoomEvent event) {
switch (event.type) {
case partroom.RoomEventType.SIGNAL_BROADCAST:
return event.signalSender.isNotEmpty ? event.signalSender : '未知用户';
case partroom.RoomEventType.MEMBER_JOINED:
case partroom.RoomEventType.MEMBER_LEFT:
case partroom.RoomEventType.MEMBER_KICKED:
return event.hasMember() && event.member.handleName.isNotEmpty
? event.member.handleName
: event.hasMember()
? event.member.gameUserId
: '未知用户';
case partroom.RoomEventType.OWNER_CHANGED:
return event.hasMember() && event.member.handleName.isNotEmpty ? event.member.handleName : '新房主';
default:
return '系统';
}
}
String? _getEventAvatarUrl(partroom.RoomEvent event) {
if (event.type == partroom.RoomEventType.SIGNAL_BROADCAST ||
event.type == partroom.RoomEventType.MEMBER_JOINED ||
event.type == partroom.RoomEventType.MEMBER_LEFT ||
event.type == partroom.RoomEventType.MEMBER_KICKED ||
event.type == partroom.RoomEventType.OWNER_CHANGED) {
if (event.hasMember() && event.member.avatarUrl.isNotEmpty) {
return '${URLConf.rsiAvatarBaseUrl}${event.member.avatarUrl}';
}
}
return null;
}
// 信号发送器
Widget _buildSignalSender(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room) {
final partyRoomState = ref.watch(partyRoomProvider);
final signalTypes = partyRoomState.room.signalTypes.where((s) => !s.isSpecial).toList();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF2B2D31).withValues(alpha: .4),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.3))),
),
child: Row(
children: [
const Spacer(),
DropDownButton(
leading: const Icon(FluentIcons.send, size: 16),
title: Text(signalTypes.isEmpty ? '加载中...' : '发送信号'),
disabled: signalTypes.isEmpty || room == null,
items: signalTypes.map((signal) {
return MenuFlyoutItem(
leading: const Icon(FluentIcons.radio_bullet, size: 16),
text: Text(signal.name.isNotEmpty ? signal.name : signal.id),
onPressed: () => _sendSignal(context, ref, partyRoom, room, signal),
);
}).toList(),
),
],
),
);
}
Future<void> _sendSignal(
BuildContext context,
WidgetRef ref,
PartyRoom partyRoom,
dynamic room,
dynamic signal,
) async {
if (room == null) return;
try {
await partyRoom.sendSignal(signal.id);
// 发送成功后,显示在消息列表中
if (context.mounted) {
// 信号已发送,会通过事件流更新到消息列表
}
} catch (e) {
// 显示错误提示
if (context.mounted) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('发送失败'),
content: Text(e.toString()),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
}
}
}
String _getEventText(partroom.RoomEvent event, WidgetRef ref) {
final partyRoomState = ref.read(partyRoomProvider);
final signalTypes = partyRoomState.room.signalTypes;
switch (event.type) {
case partroom.RoomEventType.SIGNAL_BROADCAST:
// 从 signalTypes 提取信号名称
final signalType = signalTypes.where((s) => s.id == event.signalId).firstOrNull;
// 显示信号ID和参数
if (event.signalId.isNotEmpty) {
if (event.signalParams.isNotEmpty) {
final params = event.signalParams;
return "signalId: ${event.signalId}params:$params";
}
}
return signalType?.name ?? event.signalId;
case partroom.RoomEventType.MEMBER_JOINED:
return '加入了房间';
case partroom.RoomEventType.MEMBER_LEFT:
return '离开了房间';
case partroom.RoomEventType.OWNER_CHANGED:
return '成为了新房主';
case partroom.RoomEventType.ROOM_UPDATED:
return '房间信息已更新';
case partroom.RoomEventType.MEMBER_STATUS_UPDATED:
if (event.hasMember()) {
final member = event.member;
final name = member.handleName.isNotEmpty ? member.handleName : member.gameUserId;
if (member.hasStatus() && member.status.currentLocation.isNotEmpty) {
return '$name 更新了状态: ${member.status.currentLocation}';
}
return '$name 更新了状态';
}
return '成员状态已更新';
case partroom.RoomEventType.ROOM_DISMISSED:
return '房间已解散';
case partroom.RoomEventType.MEMBER_KICKED:
return '被踢出房间';
default:
return '未知事件';
}
}
String _formatTime(dynamic timestamp) {
try {
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt() * 1000);
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inMinutes < 1) {
return '刚刚';
} else if (diff.inMinutes < 60) {
return '${diff.inMinutes} 分钟前';
} else if (diff.inHours < 24) {
return '${diff.inHours} 小时前';
} else {
return '${diff.inDays} 天前';
}
} catch (e) {
return '';
}
}
}

View File

@@ -0,0 +1,371 @@
import 'dart:ui';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
import 'package:starcitizen_doctor/ui/party_room/widgets/create_room_dialog.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
/// 房间列表页面
class PartyRoomListPage extends HookConsumerWidget {
const PartyRoomListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiModel = ref.read(partyRoomUIModelProvider.notifier);
final uiState = ref.watch(partyRoomUIModelProvider);
final partyRoomState = ref.watch(partyRoomProvider);
final partyRoom = ref.read(partyRoomProvider.notifier);
final searchController = useTextEditingController();
final scrollController = useScrollController();
useEffect(() {
// 初次加载房间列表
Future.microtask(() => uiModel.loadRoomList());
return null;
}, []);
// 无限滑动监听
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) {
// 距离底部200px时开始加载
final totalPages = (uiState.totalRooms / uiState.pageSize).ceil();
if (!uiState.isLoading && uiState.currentPage < totalPages && uiState.errorMessage == null) {
uiModel.loadMoreRooms();
}
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [uiState.isLoading, uiState.currentPage, uiState.totalRooms]);
return ScaffoldPage(
padding: EdgeInsets.zero,
content: Column(
children: [
// 筛选栏
Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextBox(
controller: searchController,
placeholder: '搜索房主名称...',
prefix: const Padding(padding: EdgeInsets.only(left: 8), child: Icon(FluentIcons.search)),
onSubmitted: (value) {
uiModel.loadRoomList(searchName: value, page: 1);
},
),
),
const SizedBox(width: 12),
_buildTagFilter(context, ref, uiState, partyRoomState),
const SizedBox(width: 12),
IconButton(icon: const Icon(FluentIcons.refresh), onPressed: () => uiModel.refreshRoomList()),
const SizedBox(width: 12),
FilledButton(
onPressed: () => _showCreateRoomDialog(context, ref),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [Icon(FluentIcons.add, size: 16), SizedBox(width: 8), Text('创建房间')],
),
),
],
),
),
// 房间列表
Expanded(child: _buildRoomList(context, ref, uiState, partyRoom, scrollController)),
],
),
);
}
Widget _buildTagFilter(
BuildContext context,
WidgetRef ref,
PartyRoomUIState uiState,
PartyRoomFullState partyRoomState,
) {
final tags = partyRoomState.room.tags;
return ComboBox<String>(
placeholder: const Text('选择标签'),
value: uiState.selectedMainTagId,
items: [
const ComboBoxItem(value: null, child: Text('全部标签')),
...tags.map((tag) => ComboBoxItem(value: tag.id, child: Text(tag.name))),
],
onChanged: (value) {
ref.read(partyRoomUIModelProvider.notifier).setSelectedMainTagId(value);
},
);
}
Widget _buildRoomList(
BuildContext context,
WidgetRef ref,
PartyRoomUIState uiState,
PartyRoom partyRoom,
ScrollController scrollController,
) {
if (uiState.isLoading && uiState.roomListItems.isEmpty) {
return const Center(child: ProgressRing());
}
if (uiState.errorMessage != null && uiState.roomListItems.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(FluentIcons.error, size: 48, color: Color(0xFFFF6B6B)),
const SizedBox(height: 16),
Text(uiState.errorMessage!, style: const TextStyle(color: Color(0xFFE0E0E0))),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
ref.read(partyRoomUIModelProvider.notifier).refreshRoomList();
},
child: const Text('重试'),
),
],
),
);
}
if (uiState.roomListItems.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FluentIcons.room, size: 48, color: Colors.grey.withValues(alpha: 0.6)),
const SizedBox(height: 16),
Text('暂无房间', style: TextStyle(color: Colors.white.withValues(alpha: 0.7))),
const SizedBox(height: 8),
Text('成为第一个创建房间的人吧!', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))),
const SizedBox(height: 16),
FilledButton(onPressed: () => _showCreateRoomDialog(context, ref), child: const Text('创建房间')),
],
),
);
}
final totalPages = (uiState.totalRooms / uiState.pageSize).ceil();
final hasMore = uiState.currentPage < totalPages;
return MasonryGridView.count(
controller: scrollController,
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: uiState.roomListItems.length + (hasMore || uiState.isLoading ? 1 : 0),
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
// 显示加载更多指示器
if (index == uiState.roomListItems.length) {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: uiState.isLoading
? const ProgressRing()
: Text('已加载全部房间', style: TextStyle(color: Colors.white.withValues(alpha: 0.5))),
),
);
}
final room = uiState.roomListItems[index];
return _buildRoomCard(context, ref, partyRoom, room, index);
},
);
}
Widget _buildRoomCard(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room, int index) {
final avatarUrl = room.ownerAvatar.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${room.ownerAvatar}' : '';
return GridItemAnimator(
index: index,
child: GestureDetector(
onTap: () => _joinRoom(context, ref, partyRoom, room),
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
// 背景图片
if (avatarUrl.isNotEmpty)
Positioned.fill(
child: CacheNetImage(url: avatarUrl, fit: BoxFit.cover),
),
// 黑色遮罩
Positioned.fill(
child: Container(decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.6))),
),
// 模糊效果
Positioned.fill(
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15.0, sigmaY: 15.0),
child: Container(color: Colors.transparent),
),
),
),
// 内容
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 头像和房主信息
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: const Color(0xFF4A9EFF).withValues(alpha: 0.5),
backgroundImage: avatarUrl.isNotEmpty ? NetworkImage(avatarUrl) : null,
child: avatarUrl.isEmpty ? const Icon(FluentIcons.contact, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
room.ownerHandleName.isNotEmpty ? room.ownerHandleName : room.ownerGameId,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
if (room.hasPassword) ...[
const SizedBox(width: 4),
Icon(FluentIcons.lock, size: 12, color: Colors.white.withValues(alpha: 0.7)),
],
],
),
const SizedBox(height: 2),
Row(
children: [
Icon(FluentIcons.group, size: 11, color: Colors.white.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text(
'${room.currentMembers}/${room.targetMembers}',
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: 0.7)),
),
],
),
],
),
),
],
),
const SizedBox(height: 12),
// 标签和时间
Wrap(
spacing: 6,
runSpacing: 6,
children: [
if (room.mainTagId.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF4A9EFF).withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
child: Text(
room.mainTagId,
style: const TextStyle(fontSize: 11, color: Color(0xFF4A9EFF)),
),
),
if (room.socialLinks.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FluentIcons.link, size: 10, color: Colors.green.withValues(alpha: 0.8)),
const SizedBox(width: 4),
Text(
'${room.socialLinks.length}',
style: TextStyle(fontSize: 11, color: Colors.green.withValues(alpha: 0.9)),
),
],
),
),
],
),
],
),
),
],
),
),
),
),
);
}
Future<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async {
await showDialog(context: context, builder: (context) => const CreateRoomDialog());
}
Future<void> _joinRoom(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room) async {
String? password;
if (room.hasPassword) {
password = await showDialog<String>(
context: context,
builder: (context) {
final passwordController = TextEditingController();
return ContentDialog(
title: const Text('输入房间密码'),
content: TextBox(controller: passwordController, placeholder: '请输入密码', obscureText: true),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context)),
FilledButton(child: const Text('加入'), onPressed: () => Navigator.pop(context, passwordController.text)),
],
);
},
);
if (password == null) return;
}
try {
await partyRoom.joinRoom(room.roomUuid, password: password);
} catch (e) {
if (context.mounted) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('加入失败'),
content: Text(e.toString()),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
}
}
}
}

View File

@@ -0,0 +1,364 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
/// 注册页面
class PartyRoomRegisterPage extends HookConsumerWidget {
const PartyRoomRegisterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiModel = ref.read(partyRoomUIModelProvider.notifier);
final uiState = ref.watch(partyRoomUIModelProvider);
final gameIdController = useTextEditingController();
final currentStep = useState(0);
return ScaffoldPage(
padding: EdgeInsets.zero,
content: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * .6,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Expanded(
child: Text(
'注册账号',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
),
],
),
const SizedBox(height: 24),
if (uiState.errorMessage != null) ...[
InfoBar(
title: const Text('错误'),
content: Text(uiState.errorMessage!),
severity: InfoBarSeverity.error,
onClose: () => uiModel.clearError(),
),
const SizedBox(height: 16),
],
// 步骤指示器
Row(
children: [
_buildStepIndicator(
context,
number: 1,
title: '输入游戏ID',
isActive: currentStep.value == 0,
isCompleted: currentStep.value > 0,
),
const Expanded(child: Divider()),
_buildStepIndicator(
context,
number: 2,
title: '验证RSI账号',
isActive: currentStep.value == 1,
isCompleted: currentStep.value > 1,
),
const Expanded(child: Divider()),
_buildStepIndicator(
context,
number: 3,
title: '完成注册',
isActive: currentStep.value == 2,
isCompleted: false,
),
],
),
const SizedBox(height: 24),
if (currentStep.value == 0) ..._buildStep1(context, uiModel, uiState, gameIdController, currentStep),
if (currentStep.value == 1) ..._buildStep2(context, uiModel, uiState, gameIdController, currentStep),
if (currentStep.value == 2) ..._buildStep3(context, uiModel, uiState, currentStep),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
InfoBar(
title: const Text('关于账号验证'),
content: const Text('接下来,您需要在 RSI 账号简介中添加验证码以证明账号所有权,验证通过后,您可以移除该验证码。'),
severity: InfoBarSeverity.info,
),
],
),
),
),
);
}
static Widget _buildStepIndicator(
BuildContext context, {
required int number,
required String title,
required bool isActive,
required bool isCompleted,
}) {
return Column(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isCompleted
? const Color(0xFF4CAF50)
: isActive
? const Color(0xFF4A9EFF)
: Colors.grey.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: Center(
child: isCompleted
? const Icon(FluentIcons.check_mark, size: 16, color: Colors.white)
: Text(
'$number',
style: TextStyle(
color: isActive ? Colors.white : Colors.grey.withValues(alpha: 0.7),
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 11,
color: isActive ? const Color(0xFF4A9EFF) : Colors.grey.withValues(alpha: 0.7),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
],
);
}
static List<Widget> _buildStep1(
BuildContext context,
PartyRoomUIModel uiModel,
PartyRoomUIState uiState,
TextEditingController gameIdController,
ValueNotifier<int> currentStep,
) {
return [
const Text(
'步骤 1: 输入您的游戏ID',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 12),
Text(
'请输入您在星际公民中的游戏IDHandle'
'这是您在游戏中使用的唯一标识符。',
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.6)),
),
const SizedBox(height: 16),
TextBox(
controller: gameIdController,
placeholder: '例如: Citizen123',
enabled: !uiState.isLoading,
onSubmitted: (value) async {
if (value.trim().isEmpty) return;
await _requestVerificationCode(uiModel, uiState, value.trim(), currentStep);
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Button(
onPressed: () {
launchUrlString('https://robertsspaceindustries.com/en/account/dashboard');
},
child: const Text('查看我的游戏ID'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: uiState.isLoading
? null
: () async {
final gameId = gameIdController.text.trim();
if (gameId.isEmpty) {
await showDialog(
context: context,
builder: (context) => ContentDialog(
title: const Text('提示'),
content: const Text('请输入游戏ID'),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
),
);
return;
}
await _requestVerificationCode(uiModel, uiState, gameId, currentStep);
},
child: uiState.isLoading
? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2))
: const Text('下一步'),
),
],
),
];
}
static Future<void> _requestVerificationCode(
PartyRoomUIModel uiModel,
PartyRoomUIState uiState,
String gameId,
ValueNotifier<int> currentStep,
) async {
try {
await uiModel.requestPreRegister(gameId);
currentStep.value = 1;
} catch (e) {
// 错误已在 state 中设置
}
}
static List<Widget> _buildStep2(
BuildContext context,
PartyRoomUIModel uiModel,
PartyRoomUIState uiState,
TextEditingController gameIdController,
ValueNotifier<int> currentStep,
) {
return [
const Text(
'步骤 2: 验证 RSI 账号',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 12),
Text('请按照以下步骤完成账号验证:', style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.6))),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1E3A5F).withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF4A9EFF).withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'1. 复制以下验证码:',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 8),
Row(
children: [
SelectableText(
'SCB:${uiState.preRegisterCode}',
style: const TextStyle(
fontSize: 16,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
color: Color(0xFF4A9EFF),
),
),
SizedBox(width: 12),
Button(
child: Icon(FluentIcons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: 'SCB:${uiState.preRegisterCode}'));
},
),
],
),
const SizedBox(height: 16),
const Text(
'2. 访问您的 RSI 账号资设置页',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 8),
Button(
onPressed: () {
launchUrlString('https://robertsspaceindustries.com/en/account/profile');
},
child: const Text('打开资料页'),
),
const SizedBox(height: 16),
const Text(
'3. 编辑您的个人简介,将验证码添加到简介中',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 8),
Text(
'在简介的任意位置添加验证码即可验证码30分钟内有效',
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5)),
),
],
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Button(
onPressed: () {
currentStep.value = 0;
},
child: const Text('上一步'),
),
FilledButton(
onPressed: uiState.isLoading
? null
: () async {
await _completeRegistration(uiModel, currentStep);
},
child: uiState.isLoading
? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2))
: const Text('我已添加,验证并注册'),
),
],
),
];
}
static Future<void> _completeRegistration(PartyRoomUIModel uiModel, ValueNotifier<int> currentStep) async {
try {
await uiModel.completeRegister();
currentStep.value = 2;
} catch (e) {
// 错误已在 state 中设置
}
}
static List<Widget> _buildStep3(
BuildContext context,
PartyRoomUIModel uiModel,
PartyRoomUIState uiState,
ValueNotifier<int> currentStep,
) {
return [
Center(
child: Column(
children: [
const Icon(FluentIcons.completed_solid, size: 64, color: Color(0xFF4CAF50)),
const SizedBox(height: 16),
const Text(
'注册成功!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)),
),
const SizedBox(height: 8),
Text('您已成功注册组队大厅,现在可以开始使用了', style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.6))),
],
),
),
];
}
}