还原参赛队伍列表页、详情页

This commit is contained in:
2026-07-28 15:46:23 +08:00
parent 9e72bc522e
commit 6793ca53af
5 changed files with 157 additions and 307 deletions
@@ -7,6 +7,8 @@ import 'package:recording_tool/features/competition_teams/model/model_competitio
import 'package:recording_tool/features/competition_teams/widgets/widget_manual_winner_dialog.dart';
import 'package:recording_tool/features/events/request_model/request_model_event.dart';
import 'package:recording_tool/features/events/server/server_events.dart';
import 'package:recording_tool/gen/assets.gen.dart';
import 'package:recording_tool/shared/widgets/app_bar.dart';
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
@@ -79,14 +81,14 @@ class _CompetitionTeamDetailPageState
final matchups = _matchups;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text(_title)),
backgroundColor: const Color(0xFFF2F4F7),
appBar: AppPageBar(title: _title),
body: matchups.isEmpty
? const AppEmptyView(message: '暂无对阵信息')
: ListView.separated(
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
padding: EdgeInsets.only(bottom: 36.h),
itemCount: matchups.length,
separatorBuilder: (_, _) => SizedBox(height: 22.h),
separatorBuilder: (_, _) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final raw = matchups[index];
final matchup = _toCompetitionMatchup(raw, index);
@@ -95,8 +97,6 @@ class _CompetitionTeamDetailPageState
}
return _MatchupCard(
matchup: matchup,
index: index,
matchTitle: raw.matchTitle?.trim() ?? '',
onManualProcess: () => _handleManualProcess(raw, index),
);
},
@@ -175,91 +175,74 @@ class _CompetitionTeamDetailPageState
}
class _MatchupCard extends StatelessWidget {
const _MatchupCard({
required this.matchup,
required this.index,
required this.matchTitle,
required this.onManualProcess,
});
const _MatchupCard({required this.matchup, required this.onManualProcess});
/// 背景图 image_team_vs_bg.png 的原始宽高比(1920 x 318)。
static const double _vsBgAspectRatio = 1920 / 318;
final CompetitionMatchup matchup;
final int index;
final String matchTitle;
final VoidCallback onManualProcess;
@override
Widget build(BuildContext context) {
final title = matchTitle.isEmpty ? '${index + 1}' : matchTitle;
return Container(
key: ValueKey('competition-matchup-${matchup.id}'),
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFC7CCD4)),
borderRadius: BorderRadius.circular(12.r),
),
color: Colors.white,
padding: EdgeInsets.fromLTRB(12.w, 4.h, 12.w, 24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF7A828E),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
key: ValueKey('manual-process-${matchup.id}'),
onPressed: onManualProcess,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'人工处理',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF078AF2),
),
),
TextButton(
key: ValueKey('manual-process-${matchup.id}'),
onPressed: onManualProcess,
child: Text(
'人工处理',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF078AF2),
),
),
),
],
),
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: _TeamPanel(
team: matchup.teamA,
alignment: CrossAxisAlignment.start,
winner: matchup.winnerTeamId == matchup.teamA.id,
accentColor: const Color(0xFFFF6B75),
AspectRatio(
aspectRatio: _vsBgAspectRatio,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(Assets.images.imageTeamVsBg.path),
fit: BoxFit.fill,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
'VS',
style: TextStyle(
fontSize: 21.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF303640),
child: Row(
children: [
Expanded(
child: _TeamPanel(
team: matchup.teamA,
isLeft: true,
winner: matchup.winnerTeamId == matchup.teamA.id,
accentColor: const Color(0xFFFF6B75),
),
),
),
Expanded(
child: _TeamPanel(
team: matchup.teamB,
isLeft: false,
winner: matchup.winnerTeamId == matchup.teamB.id,
accentColor: const Color(0xFF12A6C8),
),
),
],
),
Expanded(
child: _TeamPanel(
team: matchup.teamB,
alignment: CrossAxisAlignment.end,
textAlign: TextAlign.end,
winner: matchup.winnerTeamId == matchup.teamB.id,
accentColor: const Color(0xFF20BFA9),
),
),
],
),
),
],
),
@@ -270,83 +253,72 @@ class _MatchupCard extends StatelessWidget {
class _TeamPanel extends StatelessWidget {
const _TeamPanel({
required this.team,
required this.alignment,
required this.isLeft,
required this.winner,
required this.accentColor,
this.textAlign = TextAlign.start,
});
final CompetitionTeam team;
final CrossAxisAlignment alignment;
final TextAlign textAlign;
/// 左半区(红色面板,右对齐、略偏上);右半区(蓝色面板,左对齐、略偏下)。
final bool isLeft;
final bool winner;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: alignment,
final textAlign = isLeft ? TextAlign.end : TextAlign.start;
final nameRow = Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: winner
? accentColor.withValues(alpha: 0.14)
: Colors.transparent,
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
team.name,
textAlign: textAlign,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF282E37),
),
),
),
if (winner) ...[
SizedBox(width: 5.w),
Icon(Icons.emoji_events, size: 17.r, color: accentColor),
],
],
),
),
SizedBox(height: 10.h),
Text(
team.playerNames,
textAlign: textAlign,
style: TextStyle(
fontSize: 15.sp,
height: 1.45,
color: const Color(0xFF4E5662),
Flexible(
child: Text(
team.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF282E37),
),
),
),
if (winner) ...[
SizedBox(height: 8.h),
Container(
SizedBox(width: 4.w),
Icon(
Icons.emoji_events,
key: ValueKey('winner-${team.id}'),
padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 3.h),
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(10.r),
),
child: Text(
'胜方',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
size: 14.r,
color: accentColor,
),
],
],
);
return Padding(
// 中线两侧留出 VS 图案空间,外侧避开斜切边缘。
padding: isLeft
? EdgeInsets.only(left: 16.w, right: 30.w, bottom: 10.h)
: EdgeInsets.only(left: 30.w, right: 16.w, top: 10.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: isLeft
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
nameRow,
SizedBox(height: 4.h),
Text(
team.playerNames,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
style: TextStyle(fontSize: 13.sp, color: const Color(0xFF4E5662)),
),
],
),
);
}
}
@@ -34,7 +34,7 @@ class _CompetitionTeamListPageState
Widget build(BuildContext context) {
final state = ref.watch(competitionTeamsProvider);
return Scaffold(
backgroundColor: Colors.white,
backgroundColor: const Color(0xFFF5F6F8),
appBar: AppBar(title: const Text('参赛队伍')),
body: SafeArea(
top: false,
@@ -56,8 +56,8 @@ class _CompetitionTeamListPageState
onRefresh: ref.read(competitionTeamsProvider.notifier).refresh,
onLoadMore: ref.read(competitionTeamsProvider.notifier).loadMore,
enablePullUp: state.hasMore,
padding: EdgeInsets.fromLTRB(20.w, 18.h, 20.w, 28.h),
separator: SizedBox(height: 14.h),
padding: EdgeInsets.fromLTRB(10.w, 10.h, 10.w, 24.h),
separator: SizedBox(height: 8.h),
empty: const AppEmptyView(message: '暂无参赛队伍'),
itemBuilder: (context, item, index) {
return _CompetitionScheduleCard(
@@ -97,33 +97,26 @@ class _CompetitionScheduleCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheduleText = item.scheduleTime.isEmpty ? '时间待定' : item.scheduleTime;
return Material(
key: ValueKey('competition-team-item-${item.itemId}'),
color: Colors.white,
borderRadius: BorderRadius.circular(14.r),
borderRadius: BorderRadius.circular(6.r),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14.r),
borderRadius: BorderRadius.circular(6.r),
child: Container(
constraints: BoxConstraints(minHeight: 142.h),
padding: EdgeInsets.all(16.r),
constraints: BoxConstraints(minHeight: 86.h),
padding: EdgeInsets.fromLTRB(12.w, 10.h, 14.w, 10.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: const Color(0xFFD7DBE2)),
boxShadow: const [
BoxShadow(
color: Color(0x0F1A2230),
blurRadius: 16,
offset: Offset(0, 6),
),
],
color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
@@ -131,23 +124,21 @@ class _CompetitionScheduleCard extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 19.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF20242B),
fontSize: 13.sp,
height: 1.2,
color: const Color(0xFF30343A),
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 18.h),
_InfoLine(
icon: Icons.schedule_outlined,
text: scheduleText,
),
SizedBox(height: 6.h),
_MetaText(_formatScheduleTime(item)),
],
),
),
SizedBox(width: 14.w),
SizedBox(width: 12.w),
Icon(
Icons.chevron_right,
size: 28.r,
size: 20.r,
color: const Color(0xFF9AA3AF),
),
],
@@ -158,27 +149,40 @@ class _CompetitionScheduleCard extends StatelessWidget {
}
}
class _InfoLine extends StatelessWidget {
const _InfoLine({required this.icon, required this.text});
class _MetaText extends StatelessWidget {
const _MetaText(this.text);
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, size: 18.r, color: const Color(0xFF7B8491)),
SizedBox(width: 8.w),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF525B68)),
),
),
],
return Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.sp,
height: 1.2,
color: const Color(0xFF6E747D),
fontWeight: FontWeight.w400,
),
);
}
}
String _formatScheduleTime(CompetitionTeamListItem item) {
final start = DateTime.tryParse(item.matchStartTime);
final end = DateTime.tryParse(item.matchEndTime);
if (start == null && end == null) {
return item.scheduleTime.isEmpty ? '时间待定' : item.scheduleTime;
}
if (start != null && end != null) {
return '${start.month}${start.day}${_formatClock(start)}-${_formatClock(end)}';
}
final value = start ?? end!;
return '${value.month}${value.day}${_formatClock(value)}';
}
String _formatClock(DateTime value) {
return '${value.hour}:${value.minute.toString().padLeft(2, '0')}';
}
@@ -1,131 +0,0 @@
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:recording_tool/features/scan_qrcode/utils/rtmp_stream_target.dart';
import 'package:recording_tool/shared/widgets/app_bar.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
class PushSteamTestWidget extends StatefulWidget {
const PushSteamTestWidget({
super.key,
this.rtmpUrl =
'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组',
});
final String rtmpUrl;
@override
State<PushSteamTestWidget> createState() => _PushSteamTestWidgetState();
}
class _PushSteamTestWidgetState extends State<PushSteamTestWidget>
with WidgetsBindingObserver {
late final ApiVideoLiveStreamController _controller;
bool _ready = false;
bool _isStreaming = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = ApiVideoLiveStreamController(
initialAudioConfig: AudioConfig(bitrate: 128000),
initialVideoConfig: VideoConfig.withDefaultBitrate(
resolution: Resolution.RESOLUTION_1080,
fps: 30,
),
onConnectionSuccess: () => {
debugPrint('推流成功'),
setState(() => _isStreaming = true),
},
onConnectionFailed: (reason) {
setState(() => _isStreaming = false);
debugPrint('推流失败: $reason');
},
onDisconnection: () => {
debugPrint('推流断开'),
setState(() => _isStreaming = false),
},
);
WidgetsBinding.instance.addPostFrameCallback((_) => _initialize());
}
Future<void> _initialize() async {
try {
await _controller.initialize();
await _controller.startPreview();
setState(() => _ready = true);
} catch (e) {
debugPrint('初始化失败: $e');
}
}
Future<void> _startPush() async {
try {
final target = RtmpStreamTarget.parse(widget.rtmpUrl);
await _controller.startStreaming(
streamKey: target.streamKey,
url: target.url,
);
} on FormatException catch (e) {
debugPrint('推流地址错误: ${e.message}');
AppToast.show(e.message);
} on PlatformException catch (e) {
final message = e.message ?? e.code;
debugPrint('推流失败: ${e.code} $message');
AppToast.show('推流失败: $message');
} catch (e) {
debugPrint('推流失败: $e');
AppToast.showError(e);
}
}
Future<void> _stopPush() => _controller.stopStreaming();
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
_controller.stop();
} else if (state == AppLifecycleState.resumed) {
_controller.startPreview();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: myAppBar(context: context),
body: Stack(
children: [
if (_ready)
ApiVideoCameraPreview(controller: _controller, fit: BoxFit.cover),
Positioned(
bottom: 32,
left: 0,
right: 0,
child: Center(
child: FloatingActionButton(
backgroundColor: _isStreaming ? Colors.red : Colors.green,
onPressed: _ready
? (_isStreaming ? _stopPush : _startPush)
: null,
child: Icon(_isStreaming ? Icons.stop : Icons.circle),
),
),
),
],
),
);
}
}