feat: 进行功能分区方便后期维护

- 添加导航条目和分类数据模型
- 实现导航卡片网格展示
- 支持点击卡片打开链接
- 添加主题样式(渐变色、卡片阴影)
- 区分系统默认和自定义条目
- 添加分类统计信息"
This commit is contained in:
2026-09-13 22:49:02 +08:00
parent 4075519075
commit e2cc3ac30f
40 changed files with 3379 additions and 515 deletions
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/app_sizes.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
/// 搜索框。
///
/// 对应 `index3.html` 的 `.search-box`:固定 44px 高、白色底、1px 描边、
/// 12px 圆角,聚焦时边框变主色并外扩 3px 的浅色光晕。
///
/// 用 [FocusNode] 而不是 `InputDecoration` 的 `focusedBorder`:
/// 那条光晕是「向外扩散」的矩形阴影,靠边框宽度画不出来。
class LinkSearchField extends StatefulWidget {
const LinkSearchField({
super.key,
required this.controller,
required this.hintText,
});
/// 文本控制器。
final TextEditingController controller;
/// 占位文案。
final String hintText;
@override
State<LinkSearchField> createState() => _LinkSearchFieldState();
}
class _LinkSearchFieldState extends State<LinkSearchField> {
final FocusNode _focusNode = FocusNode();
bool _focused = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_handleFocusChange);
}
@override
void dispose() {
_focusNode
..removeListener(_handleFocusChange)
..dispose();
super.dispose();
}
void _handleFocusChange() {
if (_focusNode.hasFocus != _focused) {
setState(() => _focused = _focusNode.hasFocus);
}
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: AppSizes.transition,
// `.search-box { transition: border-color .2s, box-shadow .2s }`。
curve: Curves.ease,
height: AppSizes.searchHeight,
padding: const EdgeInsets.symmetric(
horizontal: AppSizes.searchPaddingHorizontal,
),
decoration: BoxDecoration(
color: AppColors.surface,
border: Border.all(
color: _focused ? AppColors.primary : AppColors.border,
),
borderRadius: BorderRadius.circular(AppSizes.searchRadius),
// 原型 `.search-box:focus` 的 box-shadow 会整体覆盖掉默认阴影,
// 聚焦时只剩这一圈光晕,不是「默认阴影 + 光晕」。
boxShadow: <BoxShadow>[
if (_focused)
const BoxShadow(color: AppColors.focusRing, spreadRadius: 3)
else
...AppColors.cardShadow,
],
),
child: TextField(
controller: widget.controller,
focusNode: _focusNode,
style: AppTextStyles.searchInput,
cursorColor: AppColors.primary,
textAlignVertical: TextAlignVertical.center,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
hintText: widget.hintText,
hintStyle: AppTextStyles.searchHint,
),
),
);
}
}