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

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
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

@@ -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/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/request_model/request_model_event.dart';
import 'package:recording_tool/features/events/server/server_events.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_empty_view.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart';
@@ -79,14 +81,14 @@ class _CompetitionTeamDetailPageState
final matchups = _matchups; final matchups = _matchups;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: const Color(0xFFF2F4F7),
appBar: AppBar(title: Text(_title)), appBar: AppPageBar(title: _title),
body: matchups.isEmpty body: matchups.isEmpty
? const AppEmptyView(message: '暂无对阵信息') ? const AppEmptyView(message: '暂无对阵信息')
: ListView.separated( : ListView.separated(
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h), padding: EdgeInsets.only(bottom: 36.h),
itemCount: matchups.length, itemCount: matchups.length,
separatorBuilder: (_, _) => SizedBox(height: 22.h), separatorBuilder: (_, _) => SizedBox(height: 12.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final raw = matchups[index]; final raw = matchups[index];
final matchup = _toCompetitionMatchup(raw, index); final matchup = _toCompetitionMatchup(raw, index);
@@ -95,8 +97,6 @@ class _CompetitionTeamDetailPageState
} }
return _MatchupCard( return _MatchupCard(
matchup: matchup, matchup: matchup,
index: index,
matchTitle: raw.matchTitle?.trim() ?? '',
onManualProcess: () => _handleManualProcess(raw, index), onManualProcess: () => _handleManualProcess(raw, index),
); );
}, },
@@ -175,91 +175,74 @@ class _CompetitionTeamDetailPageState
} }
class _MatchupCard extends StatelessWidget { class _MatchupCard extends StatelessWidget {
const _MatchupCard({ const _MatchupCard({required this.matchup, required this.onManualProcess});
required this.matchup,
required this.index, /// 背景图 image_team_vs_bg.png 的原始宽高比(1920 x 318)。
required this.matchTitle, static const double _vsBgAspectRatio = 1920 / 318;
required this.onManualProcess,
});
final CompetitionMatchup matchup; final CompetitionMatchup matchup;
final int index;
final String matchTitle;
final VoidCallback onManualProcess; final VoidCallback onManualProcess;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final title = matchTitle.isEmpty ? '${index + 1}' : matchTitle;
return Container( return Container(
key: ValueKey('competition-matchup-${matchup.id}'), key: ValueKey('competition-matchup-${matchup.id}'),
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h), color: Colors.white,
decoration: BoxDecoration( padding: EdgeInsets.fromLTRB(12.w, 4.h, 12.w, 24.h),
color: Colors.white,
border: Border.all(color: const Color(0xFFC7CCD4)),
borderRadius: BorderRadius.circular(12.r),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Align(
children: [ alignment: Alignment.centerRight,
Expanded( child: TextButton(
child: Text( key: ValueKey('manual-process-${matchup.id}'),
title, onPressed: onManualProcess,
maxLines: 1, style: TextButton.styleFrom(
overflow: TextOverflow.ellipsis, padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
style: TextStyle( minimumSize: Size.zero,
fontSize: 14.sp, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: const Color(0xFF7A828E), ),
), 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), SizedBox(height: 8.h),
Row( AspectRatio(
crossAxisAlignment: CrossAxisAlignment.center, aspectRatio: _vsBgAspectRatio,
children: [ child: DecoratedBox(
Expanded( decoration: BoxDecoration(
child: _TeamPanel( image: DecorationImage(
team: matchup.teamA, image: AssetImage(Assets.images.imageTeamVsBg.path),
alignment: CrossAxisAlignment.start, fit: BoxFit.fill,
winner: matchup.winnerTeamId == matchup.teamA.id,
accentColor: const Color(0xFFFF6B75),
), ),
), ),
Padding( child: Row(
padding: EdgeInsets.symmetric(horizontal: 12.w), children: [
child: Text( Expanded(
'VS', child: _TeamPanel(
style: TextStyle( team: matchup.teamA,
fontSize: 21.sp, isLeft: true,
fontWeight: FontWeight.w700, winner: matchup.winnerTeamId == matchup.teamA.id,
color: const Color(0xFF303640), 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 { class _TeamPanel extends StatelessWidget {
const _TeamPanel({ const _TeamPanel({
required this.team, required this.team,
required this.alignment, required this.isLeft,
required this.winner, required this.winner,
required this.accentColor, required this.accentColor,
this.textAlign = TextAlign.start,
}); });
final CompetitionTeam team; final CompetitionTeam team;
final CrossAxisAlignment alignment;
final TextAlign textAlign; /// 左半区(红色面板,右对齐、略偏上);右半区(蓝色面板,左对齐、略偏下)。
final bool isLeft;
final bool winner; final bool winner;
final Color accentColor; final Color accentColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( final textAlign = isLeft ? TextAlign.end : TextAlign.start;
crossAxisAlignment: alignment,
final nameRow = Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
AnimatedContainer( Flexible(
duration: const Duration(milliseconds: 180), child: Text(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), team.name,
decoration: BoxDecoration( maxLines: 1,
color: winner overflow: TextOverflow.ellipsis,
? accentColor.withValues(alpha: 0.14) textAlign: textAlign,
: Colors.transparent, style: TextStyle(
borderRadius: BorderRadius.circular(8.r), fontSize: 15.sp,
), fontWeight: FontWeight.w600,
child: Row( color: const Color(0xFF282E37),
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),
), ),
), ),
if (winner) ...[ if (winner) ...[
SizedBox(height: 8.h), SizedBox(width: 4.w),
Container( Icon(
Icons.emoji_events,
key: ValueKey('winner-${team.id}'), key: ValueKey('winner-${team.id}'),
padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 3.h), size: 14.r,
decoration: BoxDecoration( color: accentColor,
color: accentColor,
borderRadius: BorderRadius.circular(10.r),
),
child: Text(
'胜方',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
), ),
], ],
], ],
); );
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) { Widget build(BuildContext context) {
final state = ref.watch(competitionTeamsProvider); final state = ref.watch(competitionTeamsProvider);
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: const Color(0xFFF5F6F8),
appBar: AppBar(title: const Text('参赛队伍')), appBar: AppBar(title: const Text('参赛队伍')),
body: SafeArea( body: SafeArea(
top: false, top: false,
@@ -56,8 +56,8 @@ class _CompetitionTeamListPageState
onRefresh: ref.read(competitionTeamsProvider.notifier).refresh, onRefresh: ref.read(competitionTeamsProvider.notifier).refresh,
onLoadMore: ref.read(competitionTeamsProvider.notifier).loadMore, onLoadMore: ref.read(competitionTeamsProvider.notifier).loadMore,
enablePullUp: state.hasMore, enablePullUp: state.hasMore,
padding: EdgeInsets.fromLTRB(20.w, 18.h, 20.w, 28.h), padding: EdgeInsets.fromLTRB(10.w, 10.h, 10.w, 24.h),
separator: SizedBox(height: 14.h), separator: SizedBox(height: 8.h),
empty: const AppEmptyView(message: '暂无参赛队伍'), empty: const AppEmptyView(message: '暂无参赛队伍'),
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return _CompetitionScheduleCard( return _CompetitionScheduleCard(
@@ -97,33 +97,26 @@ class _CompetitionScheduleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheduleText = item.scheduleTime.isEmpty ? '时间待定' : item.scheduleTime;
return Material( return Material(
key: ValueKey('competition-team-item-${item.itemId}'), key: ValueKey('competition-team-item-${item.itemId}'),
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14.r), borderRadius: BorderRadius.circular(6.r),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(14.r), borderRadius: BorderRadius.circular(6.r),
child: Container( child: Container(
constraints: BoxConstraints(minHeight: 142.h), constraints: BoxConstraints(minHeight: 86.h),
padding: EdgeInsets.all(16.r), padding: EdgeInsets.fromLTRB(12.w, 10.h, 14.w, 10.h),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.r), color: Colors.white,
border: Border.all(color: const Color(0xFFD7DBE2)), borderRadius: BorderRadius.circular(6.r),
boxShadow: const [
BoxShadow(
color: Color(0x0F1A2230),
blurRadius: 16,
offset: Offset(0, 6),
),
],
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
@@ -131,23 +124,21 @@ class _CompetitionScheduleCard extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 19.sp, fontSize: 13.sp,
fontWeight: FontWeight.w600, height: 1.2,
color: const Color(0xFF20242B), color: const Color(0xFF30343A),
fontWeight: FontWeight.w700,
), ),
), ),
SizedBox(height: 18.h), SizedBox(height: 6.h),
_InfoLine( _MetaText(_formatScheduleTime(item)),
icon: Icons.schedule_outlined,
text: scheduleText,
),
], ],
), ),
), ),
SizedBox(width: 14.w), SizedBox(width: 12.w),
Icon( Icon(
Icons.chevron_right, Icons.chevron_right,
size: 28.r, size: 20.r,
color: const Color(0xFF9AA3AF), color: const Color(0xFF9AA3AF),
), ),
], ],
@@ -158,27 +149,40 @@ class _CompetitionScheduleCard extends StatelessWidget {
} }
} }
class _InfoLine extends StatelessWidget { class _MetaText extends StatelessWidget {
const _InfoLine({required this.icon, required this.text}); const _MetaText(this.text);
final IconData icon;
final String text; final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Text(
children: [ text,
Icon(icon, size: 18.r, color: const Color(0xFF7B8491)), maxLines: 1,
SizedBox(width: 8.w), overflow: TextOverflow.ellipsis,
Expanded( style: TextStyle(
child: Text( fontSize: 10.sp,
text, height: 1.2,
maxLines: 1, color: const Color(0xFF6E747D),
overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w400,
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF525B68)), ),
),
),
],
); );
} }
} }
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),
),
),
),
],
),
);
}
}
+5
View File
@@ -76,6 +76,10 @@ class $AssetsImagesGen {
AssetGenImage get imageStart => AssetGenImage get imageStart =>
const AssetGenImage('assets/images/image_start.png'); const AssetGenImage('assets/images/image_start.png');
/// File path: assets/images/image_team_vs_bg.png
AssetGenImage get imageTeamVsBg =>
const AssetGenImage('assets/images/image_team_vs_bg.png');
/// File path: assets/images/image_vs.png /// File path: assets/images/image_vs.png
AssetGenImage get imageVs => AssetGenImage get imageVs =>
const AssetGenImage('assets/images/image_vs.png'); const AssetGenImage('assets/images/image_vs.png');
@@ -95,6 +99,7 @@ class $AssetsImagesGen {
imageScan, imageScan,
imageScanQrcode, imageScanQrcode,
imageStart, imageStart,
imageTeamVsBg,
imageVs, imageVs,
]; ];
} }