From 4075519075cd73ecea8c8c342ed5cb70a5b823aa Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 6 Sep 2026 21:44:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E9=A1=B5=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加导航条目和分类数据模型 - 实现导航卡片网格展示 - 支持点击卡片打开链接 - 添加主题样式(渐变色、卡片阴影) - 区分系统默认和自定义条目 - 添加分类统计信息" --- frontend/lib/data/nav_data.dart | 94 +++++++++ frontend/lib/main.dart | 315 +++++++++++++++++++++--------- frontend/lib/models/category.dart | 28 +++ frontend/lib/models/nav_item.dart | 28 +++ frontend/lib/theme/app_theme.dart | 54 +++++ frontend/pubspec.yaml | 2 +- 6 files changed, 424 insertions(+), 97 deletions(-) create mode 100644 frontend/lib/data/nav_data.dart create mode 100644 frontend/lib/models/category.dart create mode 100644 frontend/lib/models/nav_item.dart create mode 100644 frontend/lib/theme/app_theme.dart diff --git a/frontend/lib/data/nav_data.dart b/frontend/lib/data/nav_data.dart new file mode 100644 index 0000000..520977d --- /dev/null +++ b/frontend/lib/data/nav_data.dart @@ -0,0 +1,94 @@ +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/main.dart b/frontend/lib/main.dart index 244a702..868bda6 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; - +import 'models/nav_item.dart'; +import 'models/category.dart'; +import 'data/mock_data.dart'; +import 'package:web/web.dart' as web; +import 'data/nav_data.dart'; +import 'theme/app_theme.dart'; void main() { runApp(const MyApp()); } @@ -7,115 +12,233 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + title: 'Navius 导航', + theme: AppTheme.lightTheme, + home: NaviusHomePage(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } +class NaviusHomePage extends StatelessWidget { + const NaviusHomePage({super.key}); @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. + // 获取分类数据 + final categories = MockData.getCategories(); + return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: .center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(120), + child: Container( + decoration: BoxDecoration( + gradient: AppTheme.appBarGradient, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), ), - ], + ), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + const Text( + 'Navius 导航', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 4), + Text( + '${categories.length} 个分类 · ${_getTotalEntries(categories)} 个导航', + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), + ), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), + body: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + return CategorySection(category: category); + }, + ), + ); + } + // 计算总导航数 + static int _getTotalEntries(List categories) { + int total = 0; + for (var category in categories) { + total += category.entries.length; + } + return total; + } +} + +// 分类区块组件 +class CategorySection extends StatelessWidget { + final Category category; + + const CategorySection({super.key, required this.category}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 分类标题(美化版) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + // 分类名称 + Text( + category.name, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFF1E293B), + letterSpacing: 0.5, + ), + ), + const SizedBox(width: 12), + // 条目数量 + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), + decoration: BoxDecoration( + color: AppTheme.systemTagColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${category.entries.length}', + style: const TextStyle( + fontSize: 12, + color: AppTheme.systemTagTextColor, + fontWeight: FontWeight.w600, + ), + ), + ), + const Spacer(), + // 系统标签 + if (category.isSystem) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppTheme.systemTagColor, + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '系统分类', + style: TextStyle( + fontSize: 11, + color: AppTheme.systemTagTextColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + // 导航网格(4列) + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 1.0, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: category.entries.length, + itemBuilder: (context, index) { + final item = category.entries[index]; + return NavItemCard(item: item); + }, + ), + const SizedBox(height: 8), + ], + ); + } +} + +// 单个导航卡片组件 +class NavItemCard extends StatelessWidget { + final NavItem item; + + const NavItemCard({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: AppTheme.cardShadow, + ), + child: InkWell( + onTap: () { + web.window.open(item.url, '_blank'); + }, + borderRadius: BorderRadius.circular(16), + hoverColor: AppTheme.primaryColor.withOpacity(0.05), + splashColor: AppTheme.primaryColor.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 图标(增大) + Text( + item.icon, + style: const TextStyle(fontSize: 36), + ), + const SizedBox(height: 10), + // 标题 + Text( + item.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1E293B), + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + // 系统标签(改进样式) + if (item.isSystem) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppTheme.systemTagColor, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '默认', + style: TextStyle( + fontSize: 9, + color: AppTheme.systemTagTextColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), ), ); } diff --git a/frontend/lib/models/category.dart b/frontend/lib/models/category.dart new file mode 100644 index 0000000..6d7bf90 --- /dev/null +++ b/frontend/lib/models/category.dart @@ -0,0 +1,28 @@ +import 'nav_item.dart'; + +// 导航分类模型 +class Category { + final String id; // 分类唯一标识 + final String name; // 分类名称 + final bool isSystem; // 是否系统分类(true=不可修改) + final List entries; // 该分类下的导航条目 + + Category({ + required this.id, + required this.name, + required this.isSystem, + required this.entries, + }); + + // 从 JSON 创建 + factory Category.fromJson(Map json) { + return Category( + id: json['id'], + name: json['name'], + isSystem: json['isSystem'] ?? false, + entries: (json['entries'] as List) + .map((e) => NavItem.fromJson(e)) + .toList(), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/models/nav_item.dart b/frontend/lib/models/nav_item.dart new file mode 100644 index 0000000..bec7175 --- /dev/null +++ b/frontend/lib/models/nav_item.dart @@ -0,0 +1,28 @@ +// 导航条目模型 +class NavItem { + final String id; // 唯一标识 + final String title; // 显示名称 + final String url; // 链接地址 + final String icon; // 图标(emoji 或文字) + final bool isSystem; // 是否系统默认(true=不可修改) + + // 构造函数 + NavItem({ + required this.id, + required this.title, + required this.url, + required this.icon, + required this.isSystem, + }); + + // 从 JSON 创建对象(后续从后端获取数据时使用) + factory NavItem.fromJson(Map json) { + return NavItem( + id: json['id'], + title: json['title'], + url: json['url'], + icon: json['icon'] ?? '🔗', + isSystem: json['isSystem'] ?? false, + ); + } +} \ No newline at end of file diff --git a/frontend/lib/theme/app_theme.dart b/frontend/lib/theme/app_theme.dart new file mode 100644 index 0000000..eed192e --- /dev/null +++ b/frontend/lib/theme/app_theme.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // 颜色方案 + static const Color primaryColor = Color(0xFF2563EB); + static const Color secondaryColor = Color(0xFF7C3AED); + static const Color backgroundColor = Color(0xFFF8FAFC); + static const Color cardColor = Colors.white; + static const Color systemTagColor = Color(0xFFE2E8F0); + static const Color systemTagTextColor = Color(0xFF64748B); + + // 渐变背景 + static final LinearGradient appBarGradient = LinearGradient( + colors: [primaryColor, secondaryColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ); + + // 卡片阴影 + static final List cardShadow = [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ]; + + // 主题数据 + static ThemeData lightTheme = ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: primaryColor, + primary: primaryColor, + secondary: secondaryColor, + background: backgroundColor, + ), + scaffoldBackgroundColor: backgroundColor, + cardTheme: CardThemeData( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: cardColor, + shadowColor: Colors.transparent, + ), + appBarTheme: const AppBarTheme( + elevation: 0, + centerTitle: true, + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + ), + ); +} \ No newline at end of file diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 1ebd2d5..325e0c1 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - + web: ^1.0.0 dev_dependencies: flutter_test: sdk: flutter