import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:new_learn/core/utils/img_url.dart'; import 'package:new_learn/features/class/class_update_view.dart'; import 'package:new_learn/features/class/point_rank_view.dart'; /// ============================================================ /// 【ClassPage】「班级」tab 页 —— 对标 Vue pages/tabbar/class/class.vue /// /// 一、Vue 源结构(壳 + 两个子页): /// NavBar(透明、无返回)的 title 插槽 = 顶部 tab「班级动态 / 积分排行」 /// content-body = ClassUpdate(tab0) + PointRank(tab1),v-show 切换 /// /// 二、换算规则(本工程统一):Vue 的 `N rpx` → Flutter 的 `(N/2).w/.h/.sp/.r` /// 例:间距 20rpx → 10.w;卡片圆角 30rpx → 15.r;字号 28rpx → 14.sp /// /// 三、对应关系: /// v-show 切换保留状态 → IndexedStack(保持两子页状态) /// v-if 懒加载 → _loadedTabs 首次切到才 build /// switchTab 后 handleShow → TODO(业务逻辑): 切回时刷新子页数据 /// /// 四、底部 Tabbar 由 MainTabPage 承载,此处不重复实现。 /// ============================================================ class ClassPage extends StatefulWidget { const ClassPage({super.key}); @override State createState() => _ClassPageState(); } class _ClassPageState extends State { static const Color _titleColor = Color(0xFF1D2B47); static const Color _subtitleColor = Color(0xFF495A7A); static const Color _primary = Color(0xFF3D7DF5); static const List _tabs = ['班级动态', '积分排行']; int _currentTab = 0; /// 懒加载标记(对标 Vue 的 loadedTabs):首次切到才创建对应子页 final List _loadedTabs = [true, false]; void _switchTab(int index) { if (_currentTab == index) return; setState(() { _loadedTabs[index] = true; _currentTab = index; }); // TODO(业务逻辑): 对应 Vue switchTab 的 nextTick 后调用子组件 handleShow 刷新。 } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // 页面底色(class-container 的 background-color: #F5F8FA) const Positioned.fill(child: ColoredBox(color: Color(0xFFF5F8FA))), // 顶部背景图:background-size 100% 512rpx → 高 256.h Positioned( top: 0, left: 0, right: 0, height: 256.h, child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-03/class-header-background-small.png', ), fit: BoxFit.fill, width: double.infinity, ), ), // 积分排行左上角装饰图(仅 tab1 显示;280rpx → 140.w) if (_currentTab == 1) Positioned( top: 0, left: 0, width: 140.w, height: 140.h, child: IgnorePointer( child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-07-31/img_points1785468064641.png', ), fit: BoxFit.cover, ), ), ), SafeArea( child: Column( children: [ _buildTabBar(), Expanded( child: IndexedStack( index: _currentTab, children: [ if (_loadedTabs[0]) const ClassUpdateView() else const SizedBox.shrink(), if (_loadedTabs[1]) const PointRankView() else const SizedBox.shrink(), ], ), ), ], ), ), ], ), ); } /// 顶部 tab 栏(对标 NavBar title 插槽) Widget _buildTabBar() { return SizedBox( height: 44.h, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < _tabs.length; i++) ...[ if (i > 0) SizedBox(width: 20.w), // 两个 tab 各 margin 20rpx _buildTabItem(i), ], ], ), ); } Widget _buildTabItem(int index) { final active = _currentTab == index; return GestureDetector( onTap: () => _switchTab(index), behavior: HitTestBehavior.opaque, child: SizedBox( height: 44.h, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _tabs[index], style: TextStyle( fontSize: active ? 16.sp : 14.sp, fontWeight: active ? FontWeight.w700 : FontWeight.w400, color: active ? _titleColor : _subtitleColor, ), ), SizedBox(height: 6.h), // 下划线指示(36rpx × 6rpx) Container( width: 18.w, height: 3.h, decoration: BoxDecoration( color: active ? _primary : Colors.transparent, borderRadius: BorderRadius.circular(2.r), ), ), ], ), ), ); } }