feat: 实现导航页基础功能

- 添加导航条目和分类数据模型
- 实现导航卡片网格展示
- 支持点击卡片打开链接
- 添加主题样式(渐变色、卡片阴影)
- 区分系统默认和自定义条目
- 添加分类统计信息"
This commit is contained in:
2026-09-19 21:53:15 +08:00
parent e2cc3ac30f
commit 1c98340fc5
42 changed files with 6054 additions and 140 deletions
@@ -5,12 +5,6 @@ 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,
@@ -18,10 +12,7 @@ class LinkSearchField extends StatefulWidget {
required this.hintText,
});
/// 文本控制器。
final TextEditingController controller;
/// 占位文案。
final String hintText;
@override
@@ -54,42 +45,52 @@ class _LinkSearchFieldState extends State<LinkSearchField> {
@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,
return GestureDetector(
// 输入框只占中间一行的高度,点上下留白也让它聚焦——
// 原型里整个 44px 高的盒子就是一个 <input>。
onTap: _focusNode.requestFocus,
child: AnimatedContainer(
duration: AppSizes.transition,
curve: Curves.ease,
height: AppSizes.searchHeight,
padding: const EdgeInsets.symmetric(
horizontal: AppSizes.searchPaddingHorizontal,
),
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,
decoration: BoxDecoration(
color: AppColors.surface,
border: Border.all(
color: _focused ? AppColors.primary : AppColors.border,
),
borderRadius: BorderRadius.circular(AppSizes.searchRadius),
boxShadow: <BoxShadow>[
if (_focused)
const BoxShadow(color: AppColors.focusRing, spreadRadius: 3)
else
...AppColors.cardShadow,
],
),
// 垂直居中不要靠固定的 contentPadding。
// 那个写法只在「字体行高 + 上下 padding 正好等于内容区高度」时成立,
// 换一个字体(浏览器里的系统字体行高通常更高)就会被挤偏。
// 这里让 TextField 按自身行高确定高度,再用 Align 把这一行放进
// 44px 的内容区里居中——和浏览器渲染单行 <input> 的方式一致,
// 与字体行高无关。
child: Align(
alignment: Alignment.centerLeft,
child: TextField(
controller: widget.controller,
focusNode: _focusNode,
style: AppTextStyles.searchInput,
cursorColor: AppColors.primary,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
hintText: widget.hintText,
hintStyle: AppTextStyles.searchHint,
),
),
),
),
);