166 lines
5.5 KiB
Dart
166 lines
5.5 KiB
Dart
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<ClassPage> createState() => _ClassPageState();
|
||
}
|
||
|
||
class _ClassPageState extends State<ClassPage> {
|
||
static const Color _titleColor = Color(0xFF1D2B47);
|
||
static const Color _subtitleColor = Color(0xFF495A7A);
|
||
static const Color _primary = Color(0xFF3D7DF5);
|
||
|
||
static const List<String> _tabs = ['班级动态', '积分排行'];
|
||
|
||
int _currentTab = 0;
|
||
|
||
/// 懒加载标记(对标 Vue 的 loadedTabs):首次切到才创建对应子页
|
||
final List<bool> _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),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |