feat: 进行功能分区方便后期维护
- 添加导航条目和分类数据模型 - 实现导航卡片网格展示 - 支持点击卡片打开链接 - 添加主题样式(渐变色、卡片阴影) - 区分系统默认和自定义条目 - 添加分类统计信息"
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/constants/app_sizes.dart';
|
||||
import '../../../../core/services/url_opener.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../data/home_repository.dart';
|
||||
import '../../domain/models/home_data.dart';
|
||||
import '../../domain/models/link_item.dart';
|
||||
import '../../domain/models/link_section.dart';
|
||||
import '../widgets/back_to_top_button.dart';
|
||||
import '../widgets/link_section_view.dart';
|
||||
import '../widgets/sidebar_nav.dart';
|
||||
import '../widgets/top_bar.dart';
|
||||
|
||||
/// 首页:可折叠侧栏 + 顶部控制栏 + 分组卡片网格。
|
||||
///
|
||||
/// 完全按项目根目录的 `index3.html` 还原:结构、样式、交互都以那份文件为准,
|
||||
/// 不额外增加原型里没有的行为。原型里没有的东西也照样不实现——
|
||||
/// 搜索框不参与过滤(原型没有对应脚本)、`active` 固定写在第一个导航条目上、
|
||||
/// 没有对应锚点的导航项点击后不会有任何反应。
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({
|
||||
super.key,
|
||||
required this.repository,
|
||||
required this.urlOpener,
|
||||
});
|
||||
|
||||
/// 数据来源(当前是 mock 实现)。
|
||||
final HomeRepository repository;
|
||||
|
||||
/// 打开链接的能力(当前是 Web 实现)。
|
||||
final UrlOpener urlOpener;
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
/// 首页数据只在初始化时取一次。
|
||||
late final HomeData _data = widget.repository.fetchHomeData();
|
||||
|
||||
/// 有对应锚点的分区,用于「点导航滚动过去」。
|
||||
late final Map<String, GlobalKey> _sectionKeys = <String, GlobalKey>{
|
||||
for (final LinkSection section in _data.sections) section.id: GlobalKey(),
|
||||
};
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
/// 已展开的分组。原型初始全部收起。
|
||||
final Set<String> _openGroupIds = <String>{};
|
||||
|
||||
/// 侧栏是否折叠。原型初始是展开的(body 上没有 sidebar-collapsed)。
|
||||
bool _collapsed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ---- 交互 ----
|
||||
|
||||
/// `#toggleBtn`:切换 `sidebar-collapsed`。
|
||||
void _toggleSidebar() {
|
||||
setState(() => _collapsed = !_collapsed);
|
||||
}
|
||||
|
||||
/// `.has-sub` 的点击:折叠时先展开侧栏,再切换该分组的 open。
|
||||
void _toggleGroup(String groupId) {
|
||||
setState(() {
|
||||
if (_collapsed) {
|
||||
_collapsed = false;
|
||||
_openGroupIds.add(groupId);
|
||||
return;
|
||||
}
|
||||
if (!_openGroupIds.remove(groupId)) {
|
||||
_openGroupIds.add(groupId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 点导航叶子:滚到对应锚点。
|
||||
///
|
||||
/// 原型里 `#icon`、`#stock`、`#dev`、`#api` 没有对应元素,
|
||||
/// 对应 `<a href="#...">` 点了不会有任何反应,这里保持一致。
|
||||
void _goToSection(String sectionId) {
|
||||
final BuildContext? sectionContext = _sectionKeys[sectionId]?.currentContext;
|
||||
if (sectionContext == null) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
Scrollable.ensureVisible(
|
||||
sectionContext,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openItem(LinkItem item) => widget.urlOpener.open(item.url);
|
||||
|
||||
void _backToTop() {
|
||||
_scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 布局 ----
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
SidebarNav(
|
||||
siteName: _data.siteName,
|
||||
navNodes: _data.navNodes,
|
||||
// 原型把 active 写死在第一个条目上,切换页面也不变。
|
||||
activeSectionId: _data.activeSectionId,
|
||||
collapsed: _collapsed,
|
||||
openGroupIds: _openGroupIds,
|
||||
onToggleGroup: _toggleGroup,
|
||||
onSectionSelected: _goToSection,
|
||||
// 原型指向不存在的 #submit,点了没有任何反应。
|
||||
onSubmitSite: () {},
|
||||
// 补上这行,将页面的切换折叠方法传进去
|
||||
onToggleCollapse: _toggleSidebar,
|
||||
),
|
||||
Expanded(child: _buildMain()),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: AppSizes.backToTopOffset,
|
||||
bottom: AppSizes.backToTopOffset,
|
||||
child: BackToTopButton(onTap: _backToTop),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMain() {
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSizes.contentPaddingHorizontal,
|
||||
AppSizes.contentPaddingTop,
|
||||
AppSizes.contentPaddingHorizontal,
|
||||
AppSizes.contentPaddingBottom,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
TopBar(
|
||||
searchController: _searchController,
|
||||
searchHint: _data.searchHint,
|
||||
sidebarCollapsed: _collapsed,
|
||||
onToggleSidebar: _toggleSidebar,
|
||||
),
|
||||
for (final LinkSection section in _data.sections)
|
||||
Padding(
|
||||
// 原型里每个 .grid 都带 margin-bottom: 36px。
|
||||
padding: const EdgeInsets.only(bottom: AppSizes.sectionGap),
|
||||
child: LinkSectionView(
|
||||
key: _sectionKeys[section.id],
|
||||
section: section,
|
||||
onOpen: _openItem,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user