diff --git a/assets/images/image_team_vs_bg.png b/assets/images/image_team_vs_bg.png new file mode 100644 index 0000000..a450210 Binary files /dev/null and b/assets/images/image_team_vs_bg.png differ diff --git a/lib/features/competition_teams/pages/page_competition_team_detail.dart b/lib/features/competition_teams/pages/page_competition_team_detail.dart index 6381959..dcaf37c 100644 --- a/lib/features/competition_teams/pages/page_competition_team_detail.dart +++ b/lib/features/competition_teams/pages/page_competition_team_detail.dart @@ -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)), + ), + ], + ), + ); } } diff --git a/lib/features/competition_teams/pages/page_competition_team_list.dart b/lib/features/competition_teams/pages/page_competition_team_list.dart index 336cb95..632c917 100644 --- a/lib/features/competition_teams/pages/page_competition_team_list.dart +++ b/lib/features/competition_teams/pages/page_competition_team_list.dart @@ -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')}'; +} diff --git a/lib/features/scan_qrcode/pages/page_push_test.dart b/lib/features/scan_qrcode/pages/page_push_test.dart deleted file mode 100644 index 5551031..0000000 --- a/lib/features/scan_qrcode/pages/page_push_test.dart +++ /dev/null @@ -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 createState() => _PushSteamTestWidgetState(); -} - -class _PushSteamTestWidgetState extends State - 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 _initialize() async { - try { - await _controller.initialize(); - await _controller.startPreview(); - - setState(() => _ready = true); - } catch (e) { - debugPrint('初始化失败: $e'); - } - } - - Future _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 _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), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index de8e257..3ce94ee 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -76,6 +76,10 @@ class $AssetsImagesGen { AssetGenImage get imageStart => 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 AssetGenImage get imageVs => const AssetGenImage('assets/images/image_vs.png'); @@ -95,6 +99,7 @@ class $AssetsImagesGen { imageScan, imageScanQrcode, imageStart, + imageTeamVsBg, imageVs, ]; }