针对 docs/opendesign/warm-notes-design-spec.md 全面审查的修复: ## 🔴 阻断级修复(商用合规) - 下载真实 Quicksand/Nunito 字体文件(原 0 字节) - 添加 OFL.txt 许可证文件,履行 SIL Open Font License 分发义务 ## 🟠 设计 Token 偏差 - AppRadius: 删除非规范的 xs=8px,所有引用迁移至 sm=10px - AppColors.moodColors: 对齐 spec §3.6 - happy #FFD93D → secondary #81B29A - calm #81B29A → tertiary #F2CC8F - sad #7B9CC4 → #5B7DB1 - thinking #B8A9C9(淡紫,spec 无)→ #8B7E74 - AppShadows: blurRadius/alpha 精确对齐 spec §1 (12/20/32 + 0.06/0.08/0.12) - DesignTokens: 补 spacing40 + 新增 safe-top/safe-bottom/tab-height/touch-min 常量 ## 🟠 首页 §3.4 完全重构 - 新增问候语头部(xx好,小暖 + accent 色高亮名字) - 新增 streak-badge pill 徽章(tertiary-soft + #B8860B 暖金) - 心情选择器卡片背景从 primaryContainer 改为 surface(spec 规定 #FFFFFF) - 心情卡片圆角 lg(22) → md(16) 对齐 spec - 新增 today-card 渐变卡片 + 浮动右下圆形写按钮 - 新增 quick-stats 三栏统计(本月日记/连续天数/总日记数) - 移除 AppBar 多余的贴纸/模板按钮,搜索按钮改路由到 /search - HomeBloc 扩展 monthCount/totalCount 字段 - 日记卡片:72×72 预览图 + 标签摘要 + 心情圆点 ## 🟠 路由 §3.12 + §3.13 拆分 - 新建 DiscoverPage (features/discover/views/discover_page.dart) - 搜索框(跳转 /search) - 每日推荐渐变卡片 - 热门话题横向 chips(前 3 个 accent 高亮) - 精选模板 2 列网格 - 达人日记列表 - /discover 路由从指向 SearchPage 改为 DiscoverPage - 新增 /search 路由(全屏无 Tab)指向 SearchPage ## 🟠 Tab 栏 §2.2 重构 - 高度从 64px 改为 56+bottomPadding(含 safe-bottom,约 90px) - 中心按钮从 CircularNotchedRectangle 凹槽改为 margin-top:-16px 凸起 - FAB 尺寸从默认改为 48×48 spec 规格 - FAB 图标从 edit_rounded 改为 add_rounded(spec §2.2) - 删除未使用的 _navItems 旧常量 ## 🟡 登录页圆角统一 - 移除 3 处 InputBorder 显式 mdBorder(16px) 覆盖 - 全局主题 smBorder(10px) 生效,对齐 spec - 提交按钮圆角改为 pill(spec §2.6 Primary 按钮) ## 验证 - flutter analyze: 0 errors (剩余 40 个 warning/info 全为预存) - flutter test: 84/85 通过(widget smoke test 预存失败,与本次无关)
486 lines
16 KiB
Dart
486 lines
16 KiB
Dart
// 发现页 — 严格对齐 spec §3.12 discover.html
|
||
//
|
||
// 视觉层级(从上到下):
|
||
// 1. 搜索框 (pill 形状)
|
||
// 2. 每日推荐卡片 inspiration-card (accent→tertiary 渐变)
|
||
// 3. 热门话题 hot-topics (横向滚动 chips)
|
||
// 4. 精选模板 featured-templates (2 列网格)
|
||
// 5. 达人日记 expert-diaries (纵向列表)
|
||
//
|
||
// 注意:本页是发现/灵感浏览,区别于 /search(主动搜索)
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
|
||
import '../../../core/constants/design_tokens.dart';
|
||
import '../../../core/theme/app_colors.dart';
|
||
import '../../../core/theme/app_radius.dart';
|
||
import '../../../core/theme/app_shadows.dart';
|
||
import '../../../core/theme/app_typography.dart';
|
||
|
||
class DiscoverPage extends StatelessWidget {
|
||
const DiscoverPage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
final bg = isDark ? AppColors.bgDark : AppColors.bgLight;
|
||
|
||
return Scaffold(
|
||
backgroundColor: bg,
|
||
body: SafeArea(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: DesignTokens.spacing12),
|
||
_SearchBar(onTap: () => context.push('/search')),
|
||
const SizedBox(height: DesignTokens.spacing20),
|
||
const _InspirationCard(
|
||
title: '今日推荐:图书馆的午后时光',
|
||
author: '小暖 · 5月31日',
|
||
emoji: '📚',
|
||
),
|
||
const SizedBox(height: DesignTokens.spacing24),
|
||
_SectionTitle(title: '热门话题'),
|
||
const SizedBox(height: DesignTokens.spacing12),
|
||
const _HotTopicsChips(),
|
||
const SizedBox(height: DesignTokens.spacing24),
|
||
_SectionTitle(title: '精选模板'),
|
||
const SizedBox(height: DesignTokens.spacing12),
|
||
const _FeaturedTemplatesGrid(),
|
||
const SizedBox(height: DesignTokens.spacing24),
|
||
_SectionTitle(title: '达人日记'),
|
||
const SizedBox(height: DesignTokens.spacing12),
|
||
const _ExpertDiariesList(),
|
||
const SizedBox(height: DesignTokens.spacing24),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 1. 搜索框(点击跳转 /search)
|
||
class _SearchBar extends StatelessWidget {
|
||
const _SearchBar({required this.onTap});
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return InkWell(
|
||
onTap: onTap,
|
||
borderRadius: AppRadius.pillBorder,
|
||
child: Container(
|
||
height: 48,
|
||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing16),
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.surface,
|
||
borderRadius: AppRadius.pillBorder,
|
||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.search_rounded, size: 20, color: theme.colorScheme.onSurfaceVariant),
|
||
const SizedBox(width: DesignTokens.spacing12),
|
||
Text(
|
||
'搜索日记、模板、话题...',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 2. 每日推荐卡片(渐变背景)
|
||
class _InspirationCard extends StatelessWidget {
|
||
const _InspirationCard({
|
||
required this.title,
|
||
required this.author,
|
||
required this.emoji,
|
||
});
|
||
|
||
final String title;
|
||
final String author;
|
||
final String emoji;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(DesignTokens.spacing20),
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [AppColors.accent, AppColors.tertiary],
|
||
),
|
||
borderRadius: AppRadius.lgBorder,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppColors.accent.withValues(alpha: 0.2),
|
||
offset: const Offset(0, 4),
|
||
blurRadius: 14,
|
||
),
|
||
],
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 装饰圆
|
||
Positioned(
|
||
right: -20,
|
||
top: -20,
|
||
child: Container(
|
||
width: 120,
|
||
height: 120,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: Colors.white.withValues(alpha: 0.12),
|
||
),
|
||
),
|
||
),
|
||
Positioned(
|
||
left: -10,
|
||
bottom: -20,
|
||
child: Container(
|
||
width: 80,
|
||
height: 80,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: Colors.white.withValues(alpha: 0.08),
|
||
),
|
||
),
|
||
),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'今日推荐',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white.withValues(alpha: 0.85),
|
||
letterSpacing: 0.5,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.spacing12),
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 80,
|
||
height: 80,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.2),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(emoji, style: const TextStyle(fontSize: 36)),
|
||
),
|
||
const SizedBox(width: DesignTokens.spacing16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: const TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.w700,
|
||
color: Colors.white,
|
||
height: 1.25,
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
author,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.white.withValues(alpha: 0.75),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SectionTitle extends StatelessWidget {
|
||
const _SectionTitle({required this.title});
|
||
final String title;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontFamily: AppTypography.displayFont,
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.w700,
|
||
color: theme.colorScheme.onSurface,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 3. 热门话题(横向滚动 chips)
|
||
class _HotTopicsChips extends StatelessWidget {
|
||
const _HotTopicsChips();
|
||
|
||
static const _topics = [
|
||
'#期末备考', '#读书笔记', '#旅行手账', '#美食日记',
|
||
'#校园生活', '#自我成长', '#心情日记', '#手写摘抄',
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return SizedBox(
|
||
height: 44,
|
||
child: ListView.separated(
|
||
scrollDirection: Axis.horizontal,
|
||
itemCount: _topics.length,
|
||
separatorBuilder: (_, __) => const SizedBox(width: DesignTokens.spacing8),
|
||
itemBuilder: (context, index) {
|
||
final isHot = index < 3;
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: isHot ? theme.colorScheme.primary : theme.colorScheme.surface,
|
||
borderRadius: AppRadius.pillBorder,
|
||
border: isHot ? null : Border.all(color: theme.colorScheme.outlineVariant),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
_topics[index],
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: isHot ? theme.colorScheme.onPrimary : theme.colorScheme.onSurface,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 4. 精选模板(2 列网格)
|
||
class _FeaturedTemplatesGrid extends StatelessWidget {
|
||
const _FeaturedTemplatesGrid();
|
||
|
||
static const _templates = [
|
||
('📖', '每日心情日记', '2.3k 人使用', AppColors.secondarySoftLight),
|
||
('🎓', '期末复习计划', '1.8k 人使用', AppColors.tertiarySoftLight),
|
||
('🌿', '植物观察日记', '956 人使用', AppColors.roseSoftLight),
|
||
('✈️', '旅行手账本', '742 人使用', AppColors.secondarySoftLight),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
mainAxisSpacing: DesignTokens.spacing12,
|
||
crossAxisSpacing: DesignTokens.spacing12,
|
||
childAspectRatio: 0.85,
|
||
),
|
||
itemCount: _templates.length,
|
||
itemBuilder: (context, index) {
|
||
final t = _templates[index];
|
||
return _TemplateCard(emoji: t.$1, name: t.$2, usage: t.$3, bg: t.$4);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _TemplateCard extends StatelessWidget {
|
||
const _TemplateCard({
|
||
required this.emoji,
|
||
required this.name,
|
||
required this.usage,
|
||
required this.bg,
|
||
});
|
||
|
||
final String emoji;
|
||
final String name;
|
||
final String usage;
|
||
final Color bg;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Material(
|
||
color: theme.colorScheme.surface,
|
||
borderRadius: AppRadius.mdBorder,
|
||
child: InkWell(
|
||
onTap: () => context.push('/templates'),
|
||
borderRadius: AppRadius.mdBorder,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: AppRadius.mdBorder,
|
||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||
),
|
||
padding: const EdgeInsets.all(DesignTokens.spacing12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
height: 96,
|
||
decoration: BoxDecoration(
|
||
color: bg,
|
||
borderRadius: AppRadius.smBorder,
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(emoji, style: const TextStyle(fontSize: 32)),
|
||
),
|
||
const SizedBox(height: DesignTokens.spacing8),
|
||
Text(
|
||
name,
|
||
style: TextStyle(
|
||
fontFamily: AppTypography.displayFont,
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: theme.colorScheme.onSurface,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
usage,
|
||
style: TextStyle(fontSize: 11, color: theme.colorScheme.onSurfaceVariant),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 5. 达人日记(纵向列表)
|
||
class _ExpertDiariesList extends StatelessWidget {
|
||
const _ExpertDiariesList();
|
||
|
||
static const _experts = [
|
||
('🌸', '小桃子', '春日漫步手账', '记录春天的每一朵花开...', '342 赞'),
|
||
('☕', '咖啡少年', '咖啡馆日记', '今天尝试了一家新店...', '218 赞'),
|
||
('📝', '学习达人', '考研倒计时30天', '坚持就是胜利...', '556 赞'),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Column(
|
||
children: _experts.map((e) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: DesignTokens.spacing12),
|
||
padding: const EdgeInsets.all(DesignTokens.spacing16),
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.surface,
|
||
borderRadius: AppRadius.mdBorder,
|
||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||
boxShadow: AppShadows.soft(context),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.surfaceWarmLight,
|
||
shape: BoxShape.circle,
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(e.$1, style: const TextStyle(fontSize: 20)),
|
||
),
|
||
const SizedBox(width: DesignTokens.spacing12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
e.$2,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.spacing8),
|
||
Text(
|
||
'·',
|
||
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
||
),
|
||
const SizedBox(width: DesignTokens.spacing8),
|
||
Expanded(
|
||
child: Text(
|
||
e.$3,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
fontFamily: AppTypography.displayFont,
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
color: theme.colorScheme.onSurface,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
e.$4,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
height: 1.5,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.spacing8),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.favorite_rounded, size: 14, color: AppColors.rose),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
e.$5,
|
||
style: TextStyle(fontSize: 11, color: theme.colorScheme.onSurfaceVariant),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
}
|