- 添加导航条目和分类数据模型 - 实现导航卡片网格展示 - 支持点击卡片打开链接 - 添加主题样式(渐变色、卡片阴影) - 区分系统默认和自定义条目 - 添加分类统计信息"
99 lines
3.0 KiB
Dart
99 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../../core/constants/app_sizes.dart';
|
|
import '../../../../core/theme/app_colors.dart';
|
|
import '../../../../core/theme/app_text_styles.dart';
|
|
|
|
/// 搜索框。
|
|
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 GestureDetector(
|
|
// 输入框只占中间一行的高度,点上下留白也让它聚焦——
|
|
// 原型里整个 44px 高的盒子就是一个 <input>。
|
|
onTap: _focusNode.requestFocus,
|
|
child: AnimatedContainer(
|
|
duration: AppSizes.transition,
|
|
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),
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|