From e2cc3ac30f2218693d7c8a8de92d2acbe6d3ed62 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 13 Sep 2026 22:49:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=9B=E8=A1=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=88=86=E5=8C=BA=E6=96=B9=E4=BE=BF=E5=90=8E=E6=9C=9F=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加导航条目和分类数据模型 - 实现导航卡片网格展示 - 支持点击卡片打开链接 - 添加主题样式(渐变色、卡片阴影) - 区分系统默认和自定义条目 - 添加分类统计信息" --- frontend/README.md | 43 +- frontend/lib/app.dart | 29 + .../lib/core/constants/app_breakpoints.dart | 24 + frontend/lib/core/constants/app_sizes.dart | 162 ++++++ frontend/lib/core/services/url_opener.dart | 8 + .../lib/core/services/web_url_opener.dart | 16 + frontend/lib/core/theme/app_colors.dart | 76 +++ frontend/lib/core/theme/app_text_styles.dart | 105 ++++ frontend/lib/core/theme/app_theme.dart | 31 ++ frontend/lib/core/utils/text.dart | 11 + frontend/lib/data/nav_data.dart | 94 ---- .../features/home/data/home_repository.dart | 13 + .../features/home/data/mock_home_data.dart | 83 +++ .../home/data/mock_home_repository.dart | 14 + .../home/domain/models/home_data.dart | 59 ++ .../home/domain/models/link_item.dart | 63 +++ .../home/domain/models/link_section.dart | 75 +++ .../features/home/domain/models/nav_node.dart | 54 ++ .../home/presentation/pages/home_page.dart | 184 +++++++ .../widgets/back_to_top_button.dart | 70 +++ .../home/presentation/widgets/link_card.dart | 108 ++++ .../widgets/link_search_field.dart | 97 ++++ .../widgets/link_section_view.dart | 93 ++++ .../presentation/widgets/sidebar_nav.dart | 465 ++++++++++++++++ .../home/presentation/widgets/top_bar.dart | 116 ++++ frontend/lib/main.dart | 246 +-------- frontend/lib/models/category.dart | 28 - frontend/lib/models/nav_item.dart | 28 - frontend/lib/theme/app_theme.dart | 54 -- frontend/pubspec.lock | 8 + .../core/constants/app_breakpoints_test.dart | 32 ++ .../home/data/mock_home_repository_test.dart | 111 ++++ .../home/presentation/home_page_test.dart | 322 +++++++++++ .../home/presentation/link_card_test.dart | 102 ++++ frontend/test/helpers/fake_url_opener.dart | 12 + frontend/test/widget_test.dart | 30 - frontend/web/index.html | 43 +- frontend/web/manifest.json | 11 +- index.html | 258 +++++++++ index3.html | 516 ++++++++++++++++++ 40 files changed, 3379 insertions(+), 515 deletions(-) create mode 100644 frontend/lib/app.dart create mode 100644 frontend/lib/core/constants/app_breakpoints.dart create mode 100644 frontend/lib/core/constants/app_sizes.dart create mode 100644 frontend/lib/core/services/url_opener.dart create mode 100644 frontend/lib/core/services/web_url_opener.dart create mode 100644 frontend/lib/core/theme/app_colors.dart create mode 100644 frontend/lib/core/theme/app_text_styles.dart create mode 100644 frontend/lib/core/theme/app_theme.dart create mode 100644 frontend/lib/core/utils/text.dart delete mode 100644 frontend/lib/data/nav_data.dart create mode 100644 frontend/lib/features/home/data/home_repository.dart create mode 100644 frontend/lib/features/home/data/mock_home_data.dart create mode 100644 frontend/lib/features/home/data/mock_home_repository.dart create mode 100644 frontend/lib/features/home/domain/models/home_data.dart create mode 100644 frontend/lib/features/home/domain/models/link_item.dart create mode 100644 frontend/lib/features/home/domain/models/link_section.dart create mode 100644 frontend/lib/features/home/domain/models/nav_node.dart create mode 100644 frontend/lib/features/home/presentation/pages/home_page.dart create mode 100644 frontend/lib/features/home/presentation/widgets/back_to_top_button.dart create mode 100644 frontend/lib/features/home/presentation/widgets/link_card.dart create mode 100644 frontend/lib/features/home/presentation/widgets/link_search_field.dart create mode 100644 frontend/lib/features/home/presentation/widgets/link_section_view.dart create mode 100644 frontend/lib/features/home/presentation/widgets/sidebar_nav.dart create mode 100644 frontend/lib/features/home/presentation/widgets/top_bar.dart delete mode 100644 frontend/lib/models/category.dart delete mode 100644 frontend/lib/models/nav_item.dart delete mode 100644 frontend/lib/theme/app_theme.dart create mode 100644 frontend/test/core/constants/app_breakpoints_test.dart create mode 100644 frontend/test/features/home/data/mock_home_repository_test.dart create mode 100644 frontend/test/features/home/presentation/home_page_test.dart create mode 100644 frontend/test/features/home/presentation/link_card_test.dart create mode 100644 frontend/test/helpers/fake_url_opener.dart delete mode 100644 frontend/test/widget_test.dart create mode 100644 index.html create mode 100644 index3.html diff --git a/frontend/README.md b/frontend/README.md index 92d1efb..f62de1f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,17 +1,38 @@ -# frontend +# Navius 前端(Flutter Web) -A new Flutter project. +导航站前端,使用 Flutter 构建。当前目标平台只有 Web,数据全部来自本地 mock, +不发起网络请求;后端(Rust)尚未接入。 -## Getting Started +界面基准是仓库根目录的 `index3.html`:可折叠的层级侧栏 + 居中的顶部控制栏 +(折叠按钮 + 搜索框)+ 分组卡片网格 + 右下角圆形回到顶部。 -This project is a starting point for a Flutter application. +## 常用命令 -A few resources to get you started if this is your first Flutter project: +```bash +flutter pub get # 拉取依赖 +flutter run -d chrome # 本地运行 +flutter analyze # 静态分析 +flutter test # 单元测试与组件测试 +flutter build web # 构建产物输出到 build/web +``` -- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) -- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) +## 目录约定 -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +- `lib/core/`:与业务无关的基础设施——主题、尺寸常量、断点计算、打开链接的能力。 +- `lib/features/<功能>/`:按功能划分,内部再分 `data`(数据来源与仓库)、`domain`(模型与规则)、`presentation`(页面与组件)。 +- `test/`:目录结构与 `lib/` 保持一致。 + +依赖方向固定为 `features/ → core/`:`core` 不引用任何功能代码, +功能之间也不互相引用。 + +## 接后端的位置 + +页面只依赖仓库接口,不关心数据来自 mock 还是 Rust 后端: + +- 首页:`lib/features/home/data/home_repository.dart` + +接入时新增一个实现类,并把 `fetchHomeData` 的返回类型改成 `Future` 即可, +`domain/` 与 `presentation/` 基本不需要改动。 + +完整的目录树、设计令牌映射、响应式规则与实现取舍见 +[docs/frontend-architecture.md](../docs/frontend-architecture.md)。 diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart new file mode 100644 index 0000000..ad74862 --- /dev/null +++ b/frontend/lib/app.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'core/services/web_url_opener.dart'; +import 'core/theme/app_theme.dart'; +import 'features/home/data/mock_home_repository.dart'; +import 'features/home/presentation/pages/home_page.dart'; + +/// 应用根组件。 +/// +/// 只负责三件事:配置全局主题、装配依赖、指定首页。 +/// 这里是「具体实现」接起来的地方,所以只有它需要知道 +/// [MockHomeRepository] 与 [WebUrlOpener] 的存在。 +class NaviusApp extends StatelessWidget { + const NaviusApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + // 与 HomeData.siteName 保持一致;接入后端后这一层由接口返回的数据驱动。 + title: 'Suxing Nav', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + home: const HomePage( + repository: MockHomeRepository(), + urlOpener: WebUrlOpener(), + ), + ); + } +} diff --git a/frontend/lib/core/constants/app_breakpoints.dart b/frontend/lib/core/constants/app_breakpoints.dart new file mode 100644 index 0000000..0c112c6 --- /dev/null +++ b/frontend/lib/core/constants/app_breakpoints.dart @@ -0,0 +1,24 @@ +/// 网格列数计算。 +/// +/// `index3.html` 的 `.grid` 用的是 CSS Grid 的 +/// `repeat(auto-fill, minmax(220px, 1fr))`;Flutter 没有等价写法, +/// 这里把它的规则换算成「给定可用宽度能放几列」,写成纯函数便于单测。 +/// +/// 原型没有媒体查询,所以这里也不做断点分支——宽度由调用方传入。 +abstract final class AppBreakpoints { + /// 给定可用宽度与最小单元宽度,算出网格能放几列。 + /// + /// 等价于 CSS:`列数 = floor((可用宽度 + 间距) / (最小单元 + 间距))`。 + static int gridColumnsFor( + double availableWidth, { + double minItemWidth = 220, + double gap = 16, + }) { + if (availableWidth <= minItemWidth) { + return 1; + } + final int columns = + ((availableWidth + gap) / (minItemWidth + gap)).floor(); + return columns < 1 ? 1 : columns; + } +} diff --git a/frontend/lib/core/constants/app_sizes.dart b/frontend/lib/core/constants/app_sizes.dart new file mode 100644 index 0000000..7477c52 --- /dev/null +++ b/frontend/lib/core/constants/app_sizes.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; + +/// 尺寸与动效常量。 +/// +/// 数值逐条对应 `index3.html` 的 CSS 变量与规则,组件里不再出现魔法数字。 +abstract final class AppSizes { + // ---- 侧栏 ---- + + /// 侧栏展开宽度(`--sidebar-width`,230px)。 + static const double sidebarWidth = 230; + + /// 侧栏折叠宽度(`--sidebar-collapsed-width`,64px)。 + static const double sidebarCollapsedWidth = 64; + + /// 侧栏内边距(`padding: 20px 12px 12px 12px`)。 + static const double sidebarPaddingTop = 20; + static const double sidebarPaddingHorizontal = 12; + static const double sidebarPaddingBottom = 12; + + /// 折叠后的左右内边距(`padding: 20px 8px 12px 8px`)。 + static const double sidebarCollapsedPaddingHorizontal = 8; + + /// logo 区域高度与下方间距(`.sidebar-header`,`height:40px; margin-bottom:24px`)。 + static const double sidebarHeaderHeight = 40; + static const double sidebarHeaderGap = 24; + + /// 侧栏展开 / 折叠的动画(`width .3s cubic-bezier(.4, 0, .2, 1)`)。 + /// + /// 该贝塞尔曲线就是 Material 的 standard easing,Flutter 里对应 + /// [Curves.fastOutSlowIn]。 + static const Duration sidebarTransition = Duration(milliseconds: 300); + + // ---- 导航项 ---- + + /// 导航项圆角(`border-radius: 8px`)。 + static const double navItemRadius = 8; + + /// 导航项内边距(`.nav-link`,`padding: 11px 16px`)。 + static const double navItemPaddingVertical = 11; + static const double navItemPaddingHorizontal = 16; + + /// 导航项之间的间距(`.nav-item-wrapper`,`margin-bottom: 4px`)。 + static const double navItemGap = 4; + + /// 折叠状态下导航项的内边距(`padding: 12px 0`)。 + static const double navItemCollapsedPaddingVertical = 12; + + /// 子菜单项内边距与字号(`.sub-menu .nav-link`,`padding: 8px 16px`)。 + static const double subNavItemPaddingVertical = 8; + static const double subNavItemPaddingHorizontal = 16; + + /// 子菜单相对父级的缩进(`.sub-menu`,`padding-left: 16px`)。 + static const double subMenuIndent = 16; + + /// 子菜单展开 / 收起的动画时长(`transition: .3s`)。 + static const Duration subMenuTransition = Duration(milliseconds: 300); + + /// 折叠箭头的点击热区宽度。 + static const double navArrowBoxWidth = 18; + + // ---- 侧栏底部 ---- + + /// 底部分隔线与内边距(`.sidebar-footer`)。 + static const double sidebarFooterPaddingTop = 12; + static const double sidebarFooterMarginTop = 8; + + /// 提交按钮内边距(`.submit-btn`,`padding: 10px 16px`)。 + static const double submitButtonPaddingVertical = 10; + static const double submitButtonPaddingHorizontal = 16; + + // ---- 主内容区 ---- + + /// 主内容区内边距(`padding: 24px 40px 40px 40px`)。 + static const double contentPaddingTop = 24; + static const double contentPaddingHorizontal = 40; + static const double contentPaddingBottom = 40; + + // ---- 顶部控制栏 ---- + + /// 顶栏最大宽度与垂直间距(`.top-bar`,`max-width: 750px; margin-bottom: 36px`)。 + static const double topBarMaxWidth = 750; + static const double topBarGap = 16; + static const double topBarMarginBottom = 36; + + /// 折叠按钮尺寸与圆角(`.toggle-btn`,44×44,圆角 12px)。 + static const double toggleButtonSize = 44; + static const double toggleButtonRadius = 12; + + // ---- 搜索框 ---- + + /// 搜索框高度与内边距(`.search-box`,`height:44px; padding: 0 20px`)。 + static const double searchHeight = 44; + static const double searchPaddingHorizontal = 20; + + /// 搜索框圆角(`border-radius: 12px`)。 + static const double searchRadius = 12; + + /// 聚焦光晕宽度(`0 0 0 3px`)。 + static const double focusRingWidth = 3; + + // ---- 分组与卡片 ---- + + /// 分组标题与网格之间的间距(`margin-bottom: 16px`)。 + static const double sectionTitleGap = 16; + + /// 标题前的竖条尺寸(`width: 4px; height: 16px`)。 + static const double sectionAccentWidth = 4; + static const double sectionAccentHeight = 16; + + /// 竖条与标题文字的间距(`margin-right: 8px`)。 + static const double sectionAccentGap = 8; + + /// 竖条圆角(`border-radius: 2px`)。 + static const double sectionAccentRadius = 2; + + /// 分组之间的间距(`.grid` 的 `margin-bottom: 36px`)。 + static const double sectionGap = 36; + + /// 网格间距(`gap: 16px`)。 + static const double gridGap = 16; + + /// 网格单元最小宽度(`minmax(220px, 1fr)`)。 + static const double gridMinItemWidth = 220; + + /// 卡片圆角(`border-radius: 10px`)。 + static const double cardRadius = 10; + + /// 卡片内边距(`padding: 14px 16px`)。 + static const double cardPaddingVertical = 14; + static const double cardPaddingHorizontal = 16; + + /// 卡片悬停时上移的距离(`translateY(-2px)`)。 + static const double cardHoverLift = 2; + + /// 卡片图标尺寸(`.card-icon`,36px)。 + static const double cardIconSize = 36; + + /// 卡片图标圆角(`border-radius: 8px`)。 + static const double cardIconRadius = 8; + + /// 图标与文字之间的间距(`margin-right: 12px`)。 + static const double cardIconGap = 12; + + /// 卡片标题与描述的间距(`margin-top: 2px`)。 + static const double cardTitleGap = 2; + + // ---- 回到顶部 ---- + + /// 按钮尺寸(`.back-to-top`,44×44,圆角 50%)。 + static const double backToTopSize = 44; + + /// 距视口右下角的距离(`bottom: 30px; right: 30px`)。 + static const double backToTopOffset = 30; + + /// 悬停时上移距离(`translateY(-3px)`)。 + static const double backToTopHoverLift = 3; + + // ---- 通用 ---- + + /// 通用过渡时长(CSS 里的 `transition: 0.2s`)。 + static const Duration transition = Duration(milliseconds: 200); +} diff --git a/frontend/lib/core/services/url_opener.dart b/frontend/lib/core/services/url_opener.dart new file mode 100644 index 0000000..ab840bd --- /dev/null +++ b/frontend/lib/core/services/url_opener.dart @@ -0,0 +1,8 @@ +/// 打开外部链接的能力抽象。 +/// +/// 界面层只依赖这个接口:一来把「打开链接」这种平台相关行为挡在 UI 之外, +/// 二来测试里可以注入假实现,避免真的弹出浏览器。 +abstract interface class UrlOpener { + /// 打开 [url]。 + void open(String url); +} diff --git a/frontend/lib/core/services/web_url_opener.dart b/frontend/lib/core/services/web_url_opener.dart new file mode 100644 index 0000000..28b80eb --- /dev/null +++ b/frontend/lib/core/services/web_url_opener.dart @@ -0,0 +1,16 @@ +import 'package:web/web.dart' as web; + +import 'url_opener.dart'; + +/// [UrlOpener] 的 Web 实现:在新标签页打开链接。 +/// +/// 当前项目只面向 Web,因此这是唯一的实现; +/// 以后要支持其它平台,只需新增实现类,界面代码无需改动。 +class WebUrlOpener implements UrlOpener { + const WebUrlOpener(); + + @override + void open(String url) { + web.window.open(url, '_blank'); + } +} diff --git a/frontend/lib/core/theme/app_colors.dart b/frontend/lib/core/theme/app_colors.dart new file mode 100644 index 0000000..c80ab70 --- /dev/null +++ b/frontend/lib/core/theme/app_colors.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +/// 全局配色。 +/// +/// 取值逐条对应 `index.html` 里 `:root` 的 CSS 变量, +/// 组件只引用这里的语义名,不再出现十六进制字面量。 +/// 将来调色只需要改这个文件。 +abstract final class AppColors { + // ---- 基础色 ---- + + /// 页面底色(`--bg-color`)。 + static const Color background = Color(0xFFF7F9FB); + + /// 卡片 / 侧栏底色(`--card-bg`)。 + static const Color surface = Color(0xFFFFFFFF); + + /// 主色(`--primary-color`):logo、导航选中态、图标文字、焦点边框。 + static const Color primary = Color(0xFF3B82F6); + + /// 主要文字(`--text-main`):标题与卡片标题。 + static const Color textPrimary = Color(0xFF1E293B); + + /// 次要文字(`--text-sub`):描述、未选中的导航项。 + static const Color textSecondary = Color(0xFF64748B); + + /// 描边(`--border-color`):侧栏右边界、卡片边框、搜索框边框。 + static const Color border = Color(0xFFE2E8F0); + + /// 导航项悬停 / 选中底色(`.nav-item:hover`)。 + static const Color primaryFaint = Color(0xFFEFF6FF); + + /// 导航项悬停底色(`.nav-link:hover`,比选中态更浅)。 + static const Color navItemHover = Color(0xFFF8FAFC); + + /// 提交按钮悬停底色(`.submit-btn:hover`),与卡片图标底色同值。 + static const Color navItemHoverDeep = Color(0xFFF1F5F9); + + /// 折叠箭头颜色(`.arrow-icon`)。 + static const Color navArrow = Color(0xFF94A3B8); + + /// 卡片图标底色(`.card-icon`)。 + static const Color iconSurface = Color(0xFFF1F5F9); + + /// 搜索框聚焦时的外发光(`0 0 0 3px rgba(59,130,246,.15)`)。 + static const Color focusRing = Color(0x263B82F6); + + // ---- 阴影 ---- + + /// 卡片默认阴影(`--shadow`)。 + static const List cardShadow = [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 6, + offset: Offset(0, 4), + ), + BoxShadow( + color: Color(0x08000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ]; + + /// 卡片悬停阴影(`--shadow-hover`)。 + static const List cardShadowHover = [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 15, + offset: Offset(0, 10), + ), + BoxShadow( + color: Color(0x0A000000), + blurRadius: 6, + offset: Offset(0, 4), + ), + ]; +} diff --git a/frontend/lib/core/theme/app_text_styles.dart b/frontend/lib/core/theme/app_text_styles.dart new file mode 100644 index 0000000..192264c --- /dev/null +++ b/frontend/lib/core/theme/app_text_styles.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import 'app_colors.dart'; + +/// 语义化文字样式。 +/// +/// 字号与字重对应 `index.html` 的 CSS;字体族不显式指定, +/// 沿用 HTML 里的系统字体栈(`-apple-system, Segoe UI, ...`)。 +/// 颜色留空的样式由调用方按状态着色(例如导航项的选中 / 未选中)。 +abstract final class AppTextStyles { + /// 侧栏 logo 文字(`.logo`,18px / 700)。 + static const TextStyle logo = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ); + + /// 侧栏折叠后的 logo 图标(`.logo-icon`,20px / 800)。 + static const TextStyle logoIcon = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: AppColors.primary, + ); + + /// 侧栏一级导航项(`.nav-link`,14px / 500)。 + static const TextStyle navItem = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ); + + /// 侧栏二级导航项(`.sub-menu .nav-link`,13px / 500)。 + static const TextStyle subNavItem = TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ); + + /// 折叠箭头(`.arrow-icon`,14px)。 + static const TextStyle navArrow = TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.navArrow, + fontFamily: 'monospace', + height: 1, + ); + + /// 提交按钮文字(`.submit-btn`,14px / 500)。 + static const TextStyle submitButton = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ); + + /// 分组标题(`.section-title`,16px / 600)。 + static const TextStyle sectionTitle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ); + + /// 搜索框正文(`.search-box`,15px)。 + static const TextStyle searchInput = TextStyle( + fontSize: 15, + color: AppColors.textPrimary, + ); + + /// 搜索框占位文字。 + static const TextStyle searchHint = TextStyle( + fontSize: 15, + color: AppColors.textSecondary, + ); + + /// 卡片标题(`.card-title`,14px / 600)。 + static const TextStyle cardTitle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ); + + /// 卡片描述(`.card-desc`,12px)。 + static const TextStyle cardDesc = TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ); + + /// 卡片图标里的首字母(`.card-icon`)。 + /// + /// 原型没有给 .card-icon 设 font-size,它继承 body 的浏览器默认字号 16px。 + static const TextStyle cardIcon = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ); + + /// 空状态主文案。 + static const TextStyle emptyTitle = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ); + + /// 空状态补充说明。 + static const TextStyle emptyMessage = TextStyle( + fontSize: 13, + color: AppColors.textSecondary, + ); +} diff --git a/frontend/lib/core/theme/app_theme.dart b/frontend/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..2832e03 --- /dev/null +++ b/frontend/lib/core/theme/app_theme.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'app_colors.dart'; + +/// 全局主题。 +abstract final class AppTheme { + /// 浅色主题(当前唯一主题,取值来自 index.html 的设计变量)。 + static ThemeData get light { + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + primary: AppColors.primary, + surface: AppColors.surface, + ), + scaffoldBackgroundColor: AppColors.background, + dividerColor: AppColors.border, + iconTheme: const IconThemeData(color: AppColors.textSecondary, size: 20), + // 搜索框自己绘制边框与聚焦光晕,这里只负责光标颜色。 + textSelectionTheme: const TextSelectionThemeData( + cursorColor: AppColors.primary, + selectionColor: AppColors.focusRing, + selectionHandleColor: AppColors.primary, + ), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: AppColors.textPrimary, + contentTextStyle: TextStyle(color: AppColors.surface, fontSize: 14), + ), + ); + } +} diff --git a/frontend/lib/core/utils/text.dart b/frontend/lib/core/utils/text.dart new file mode 100644 index 0000000..301a9f4 --- /dev/null +++ b/frontend/lib/core/utils/text.dart @@ -0,0 +1,11 @@ +/// 取字符串的第一个字符。 +/// +/// 用 `runes` 而不是 `substring(0, 1)`:中文与 emoji 不会被截成半个字符。 +/// 侧栏折叠后的短标签、卡片左上角的字母图标都依赖这条规则, +/// 放在这里保证三处(logo、导航、卡片)表现一致。 +String firstChar(String text, {String fallback = '·'}) { + if (text.isEmpty) { + return fallback; + } + return String.fromCharCode(text.runes.first); +} diff --git a/frontend/lib/data/nav_data.dart b/frontend/lib/data/nav_data.dart deleted file mode 100644 index 520977d..0000000 --- a/frontend/lib/data/nav_data.dart +++ /dev/null @@ -1,94 +0,0 @@ -import '../models/nav_item.dart'; -import '../models/category.dart'; - -class NavData { - // 获取所有分类数据 - static List getCategories() { - return [ - Category( - id: 'default', - name: '常用导航', - isSystem: true, - entries: [ - NavItem( - id: 'github', - title: 'GitHub', - url: 'https://github.com', - icon: '💻', - isSystem: true, - ), - NavItem( - id: 'google', - title: 'Google', - url: 'https://www.google.com', - icon: '🔍', - isSystem: true, - ), - NavItem( - id: 'stackoverflow', - title: 'Stack Overflow', - url: 'https://stackoverflow.com', - icon: '📚', - isSystem: true, - ), - NavItem( - id: 'bilibili', - title: 'B站', - url: 'https://www.bilibili.com', - icon: '📺', - isSystem: true, - ), - ], - ), - Category( - id: 'dev', - name: '开发工具', - isSystem: false, - entries: [ - NavItem( - id: 'rust_doc', - title: 'Rust 文档', - url: 'https://doc.rust-lang.org', - icon: '🦀', - isSystem: false, - ), - NavItem( - id: 'flutter_doc', - title: 'Flutter 文档', - url: 'https://docs.flutter.dev', - icon: '📱', - isSystem: false, - ), - NavItem( - id: 'dartpad', - title: 'DartPad', - url: 'https://dartpad.dev', - icon: '🎯', - isSystem: false, - ), - ], - ), - Category( - id: 'ai', - name: 'AI 工具', - isSystem: false, - entries: [ - NavItem( - id: 'chatgpt', - title: 'ChatGPT', - url: 'https://chat.openai.com', - icon: '🤖', - isSystem: false, - ), - NavItem( - id: 'claude', - title: 'Claude', - url: 'https://claude.ai', - icon: '🧠', - isSystem: false, - ), - ], - ), - ]; - } -} \ No newline at end of file diff --git a/frontend/lib/features/home/data/home_repository.dart b/frontend/lib/features/home/data/home_repository.dart new file mode 100644 index 0000000..7c31956 --- /dev/null +++ b/frontend/lib/features/home/data/home_repository.dart @@ -0,0 +1,13 @@ +import '../domain/models/home_data.dart'; + +/// 首页数据仓库契约。 +/// +/// 界面层只认这个接口,不关心数据来自本地 mock 还是 Rust 后端。 +/// +/// 目前是同步返回:mock 数据是恒定常量,加一层 Future 只会凭空多出 +/// loading / error 分支。等真正走网络请求时,把返回值改成 +/// `Future`,改动范围仅限实现类与页面的一次数据加载。 +abstract interface class HomeRepository { + /// 读取首页需要的全部数据。 + HomeData fetchHomeData(); +} diff --git a/frontend/lib/features/home/data/mock_home_data.dart b/frontend/lib/features/home/data/mock_home_data.dart new file mode 100644 index 0000000..05d8ce7 --- /dev/null +++ b/frontend/lib/features/home/data/mock_home_data.dart @@ -0,0 +1,83 @@ +import '../domain/models/home_data.dart'; +import '../domain/models/link_item.dart'; +import '../domain/models/link_section.dart'; +import '../domain/models/nav_node.dart'; + +/// 侧栏导航树,与 `index3.html` 的 `