Files
2026-09-04 15:22:20 +08:00

166 lines
5.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
),
),
],
),
),
);
}
}