本次提交包含多项核心更新: 1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等 2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果 3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线 4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题 5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航 6. 新增收藏与时间线数据合并导入功能 7. 修复多处UI样式问题,统一主题颜色使用规范 8. 新增日历同步与跨平台触觉反馈依赖库 9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
38 KiB
宽屏适配 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为闲言APP添加宽屏/横屏分屏适配,三个主Tab页面支持左右双面板布局
Architecture: 混合方案 — AppShell提供AdaptiveSplitView框架,各Tab通过RightPanelRegistry注册自己的详情面板。宽屏(≥900px)时底部导航栏变为垂直导航栏,内容区左右分屏。窄屏(<900px)保持现有行为。
Tech Stack: Flutter 3.x, Riverpod, GoRouter, liquid_glass_widgets, dual_screen(可选)
File Structure
新增文件
| 文件 | 职责 |
|---|---|
lib/core/providers/split_view_provider.dart |
SplitView状态Provider(分割比例/面板内容/导航栏位置/分屏开关) |
lib/core/layout/adaptive_split_view.dart |
AdaptiveSplitView核心分屏组件(左右面板+分割线+手势隔离) |
lib/core/layout/split_divider.dart |
SplitDivider可拖拽分割线(hover/拖拽/触觉反馈) |
lib/core/layout/adaptive_nav_bar.dart |
AdaptiveNavBar垂直/水平导航栏(4种停靠位置) |
lib/core/layout/right_panel_registry.dart |
RightPanelRegistry右侧面板注册表 |
lib/core/layout/overview_dashboard.dart |
OverviewDashboard概览仪表盘(空状态页面) |
lib/features/home/presentation/panels/sentence_detail_panel.dart |
句子详情面板(从Sheet提取内容复用) |
lib/features/home/presentation/panels/home_dashboard.dart |
首页概览仪表盘 |
lib/features/discover/presentation/panels/chat_flow_panel.dart |
会话流详情面板 |
lib/features/discover/presentation/panels/discover_dashboard.dart |
发现页概览仪表盘 |
lib/features/mine/profile/presentation/panels/profile_dashboard.dart |
我的概览仪表盘 |
lib/features/mine/settings/presentation/panels/settings_panels.dart |
设置项面板集合 |
修改文件
| 文件 | 修改内容 |
|---|---|
lib/core/layout/app_shell.dart |
集成AdaptiveLayoutSwitcher,宽屏用分屏布局 |
lib/features/home/presentation/home_page.dart |
宽屏点击判断 |
lib/features/discover/presentation/pages/home/discover_page.dart |
宽屏点击判断 |
lib/features/mine/profile/presentation/profile_page.dart |
宽屏点击判断 |
lib/features/mine/settings/presentation/general/general_settings_sections.dart |
新增导航栏位置/分屏设置项 |
pubspec.yaml |
添加dual_screen依赖 |
Phase 1: 核心框架(SplitView + Provider + Divider)
Task 1: SplitView Provider
Files:
-
Create:
lib/core/providers/split_view_provider.dart -
Step 1: 创建split_view_provider.dart
/// ============================================================
/// 闲言APP — 宽屏分屏状态管理
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 管理分屏比例、右侧面板内容、导航栏位置、分屏开关
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/storage/app_kv_store.dart';
/// 导航栏停靠位置
enum NavBarPosition {
left,
right,
top,
bottom;
String get label => switch (this) {
left => '左侧',
right => '右侧',
top => '顶部',
bottom => '底部',
};
String get emoji => switch (this) {
left => '⬅️',
right => '➡️',
top => '⬆️',
bottom => '⬇️',
};
}
/// 分屏比例选项
class SplitRatioOption {
const SplitRatioOption(this.value, this.label);
final double value;
final String label;
static const List<SplitRatioOption> options = [
SplitRatioOption(0.30, '30:70'),
SplitRatioOption(0.35, '35:65'),
SplitRatioOption(0.40, '40:60'),
SplitRatioOption(0.45, '45:55'),
SplitRatioOption(0.50, '50:50'),
SplitRatioOption(0.55, '55:45'),
SplitRatioOption(0.60, '60:40'),
];
}
const String _kSplitRatio = 'split_view_ratio';
const String _kNavBarPosition = 'nav_bar_position';
const String _kSplitViewEnabled = 'split_view_enabled';
/// 分割比例
final splitRatioProvider = StateProvider<double>((ref) {
return KvStorage.getDouble(_kSplitRatio) ?? 0.4;
});
/// 右侧面板内容标识
final rightPanelContentProvider = StateProvider<String?>((ref) => null);
/// 右侧面板参数
final rightPanelArgsProvider = StateProvider<Map<String, dynamic>?>((ref) => null);
/// 导航栏位置
final navBarPositionProvider = StateProvider<NavBarPosition>((ref) {
final index = KvStorage.getInt(_kNavBarPosition) ?? 0;
return NavBarPosition.values[index];
});
/// 分屏功能开关
final splitViewEnabledProvider = StateProvider<bool>((ref) {
return KvStorage.getBool(_kSplitViewEnabled) ?? true;
});
/// 首页右侧面板
final homeRightPanelProvider = StateProvider<String?>((ref) => null);
/// 发现页右侧面板
final discoverRightPanelProvider = StateProvider<String?>((ref) => null);
/// 我的页面右侧面板
final profileRightPanelProvider = StateProvider<String?>((ref) => null);
/// 当前Tab索引
final currentTabProvider = StateProvider<int>((ref) => 0);
/// 当前活跃的右侧面板(根据当前Tab自动选择)
final activeRightPanelProvider = Provider<String?>((ref) {
final tab = ref.watch(currentTabProvider);
return switch (tab) {
0 => ref.watch(homeRightPanelProvider),
1 => ref.watch(discoverRightPanelProvider),
2 => ref.watch(profileRightPanelProvider),
_ => null,
};
});
/// SplitView设置变更Notifier
class SplitViewSettingsNotifier extends Notifier<void> {
@override
void build() {}
void setSplitRatio(double value) {
KvStorage.setDouble(_kSplitRatio, value);
ref.read(splitRatioProvider.notifier).state = value;
}
void setNavBarPosition(NavBarPosition position) {
KvStorage.setInt(_kNavBarPosition, position.index);
ref.read(navBarPositionProvider.notifier).state = position;
}
void setSplitViewEnabled(bool enabled) {
KvStorage.setBool(_kSplitViewEnabled, enabled);
ref.read(splitViewEnabledProvider.notifier).state = enabled;
}
}
final splitViewSettingsProvider =
NotifierProvider<SplitViewSettingsNotifier, void>(
SplitViewSettingsNotifier.new,
);
- Step 2: 验证编译
Run: flutter analyze lib/core/providers/split_view_provider.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/providers/split_view_provider.dart
git commit -m "feat: add SplitView providers for widescreen adaptation"
Task 2: SplitDivider 可拖拽分割线
Files:
-
Create:
lib/core/layout/split_divider.dart -
Step 1: 创建split_divider.dart
/// ============================================================
/// 闲言APP — 可拖拽分割线
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 宽屏分屏的分割线组件,支持拖拽调整比例、hover高亮、触觉反馈
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
class SplitDivider extends StatefulWidget {
const SplitDivider({
required this.onPositionChanged,
this.currentPosition = 0.4,
this.minPosition = 0.2,
this.maxPosition = 0.7,
this.isVertical = true,
super.key,
});
final ValueChanged<double> onPositionChanged;
final double currentPosition;
final double minPosition;
final double maxPosition;
final bool isVertical;
@override
State<SplitDivider> createState() => _SplitDividerState();
}
class _SplitDividerState extends State<SplitDivider> {
bool _isHovering = false;
bool _isDragging = false;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final dividerColor = ext.textHint.withValues(alpha: 0.15);
final handleColor = _isDragging
? ext.accent.withValues(alpha: 0.8)
: _isHovering
? ext.accent.withValues(alpha: 0.5)
: ext.textHint.withValues(alpha: 0.3);
return MouseRegion(
onEnter: (_) => setState(() => _isHovering = true),
onExit: (_) => setState(() => _isHovering = false),
cursor: SystemMouseCursors.resizeColumn,
child: GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
behavior: HitTestBehavior.translucent,
child: Container(
width: 17,
alignment: Alignment.center,
child: Container(
width: 1,
color: dividerColor,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
width: _isDragging ? 6 : _isHovering ? 4 : 4,
height: 32,
decoration: BoxDecoration(
color: handleColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
),
);
}
void _onDragStart(DragStartDetails details) {
setState(() => _isDragging = true);
HapticFeedback.selectionClick();
}
void _onDragUpdate(DragUpdateDetails details) {
final box = context.findRenderObject() as RenderBox;
final parent = box.parent as RenderBox;
final totalWidth = parent.size.width;
if (totalWidth <= 0) return;
final localX = details.globalPosition.dx - parent.localToGlobal(Offset.zero).dx;
final newPosition = (localX / totalWidth).clamp(widget.minPosition, widget.maxPosition);
widget.onPositionChanged(newPosition);
}
void _onDragEnd(DragEndDetails details) {
setState(() => _isDragging = false);
}
}
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/split_divider.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/layout/split_divider.dart
git commit -m "feat: add SplitDivider with drag/hover/haptic support"
Task 3: AdaptiveSplitView 核心分屏组件
Files:
-
Create:
lib/core/layout/adaptive_split_view.dart -
Step 1: 创建adaptive_split_view.dart
/// ============================================================
/// 闲言APP — 自适应分屏组件
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 宽屏时左右分屏布局,支持可拖拽分割线、手势隔离、动画过渡
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/split_view_provider.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import 'split_divider.dart';
/// 分屏断点:宽度 >= 900px 进入分屏模式
const double kSplitViewBreakpoint = 900.0;
class AdaptiveSplitView extends ConsumerStatefulWidget {
const AdaptiveSplitView({
required this.leftPanel,
required this.rightPanel,
this.minLeftWidth = 320,
this.minRightWidth = 400,
super.key,
});
final Widget leftPanel;
final Widget rightPanel;
final double minLeftWidth;
final double minRightWidth;
@override
ConsumerState<AdaptiveSplitView> createState() => _AdaptiveSplitViewState();
}
class _AdaptiveSplitViewState extends ConsumerState<AdaptiveSplitView>
with SingleTickerProviderStateMixin {
late AnimationController _panelAnimationController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
bool _wasSplitView = false;
@override
void initState() {
super.initState();
_panelAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _panelAnimationController,
curve: Curves.easeInOutCubic,
));
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _panelAnimationController,
curve: Curves.easeIn,
),
);
}
@override
void dispose() {
_panelAnimationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.sizeOf(context).width;
final isSplitView = screenWidth >= kSplitViewBreakpoint &&
ref.watch(splitViewEnabledProvider);
final splitRatio = ref.watch(splitRatioProvider);
final rightPanelContent = ref.watch(activeRightPanelProvider);
final hasRightContent = rightPanelContent != null;
// 动画控制:宽屏+有内容 → 滑入,否则 → 滑出
if (isSplitView && hasRightContent) {
_panelAnimationController.forward();
} else {
_panelAnimationController.reverse();
}
// 窄屏:直接返回左面板
if (!isSplitView) {
if (_wasSplitView) {
_wasSplitView = false;
}
return widget.leftPanel;
}
_wasSplitView = true;
final leftWidth = screenWidth * splitRatio;
final rightWidth = screenWidth * (1 - splitRatio);
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: leftWidth.clamp(widget.minLeftWidth, screenWidth - widget.minRightWidth - 17),
child: NotificationListener<ScrollNotification>(
onNotification: (_) => true,
child: widget.leftPanel,
),
),
SplitDivider(
currentPosition: splitRatio,
onPositionChanged: (newRatio) {
ref.read(splitViewSettingsProvider.notifier).setSplitRatio(newRatio);
},
minPosition: widget.minLeftWidth / screenWidth,
maxPosition: (screenWidth - widget.minRightWidth - 17) / screenWidth,
),
SizedBox(
width: (screenWidth - leftWidth - 17).clamp(widget.minRightWidth, screenWidth - widget.minLeftWidth - 17),
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: NotificationListener<ScrollNotification>(
onNotification: (_) => true,
child: widget.rightPanel,
),
),
),
),
],
);
}
}
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/adaptive_split_view.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/layout/adaptive_split_view.dart
git commit -m "feat: add AdaptiveSplitView with animation and gesture isolation"
Task 4: RightPanelRegistry 右侧面板注册表
Files:
-
Create:
lib/core/layout/right_panel_registry.dart -
Step 1: 创建right_panel_registry.dart
/// ============================================================
/// 闲言APP — 右侧面板注册表
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 管理右侧面板的注册与构建,各Tab页面通过注册表提供面板内容
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter/widgets.dart';
typedef RightPanelBuilder = Widget Function(
BuildContext context,
Map<String, dynamic>? args,
);
class RightPanelRegistry {
RightPanelRegistry._();
static final Map<String, RightPanelBuilder> _builders = {};
/// 注册面板构建器
static void register(String panelId, RightPanelBuilder builder) {
_builders[panelId] = builder;
}
/// 批量注册
static void registerAll(Map<String, RightPanelBuilder> entries) {
_builders.addAll(entries);
}
/// 构建面板Widget
static Widget build(String panelId, BuildContext context, {Map<String, dynamic>? args}) {
final builder = _builders[panelId];
if (builder == null) {
return _buildPlaceholder(context, panelId);
}
return builder(context, args);
}
/// 是否已注册
static bool hasPanel(String panelId) => _builders.containsKey(panelId);
/// 获取所有已注册的panelId
static List<String> get registeredIds => _builders.keys.toList();
/// 未注册面板的占位Widget
static Widget _buildPlaceholder(BuildContext context, String panelId) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('🚧', style: TextStyle(fontSize: 48)),
const SizedBox(height: 16),
Text(
'面板 "$panelId" 尚未注册',
style: const TextStyle(fontSize: 14),
),
],
),
);
}
}
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/right_panel_registry.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/layout/right_panel_registry.dart
git commit -m "feat: add RightPanelRegistry for panel management"
Task 5: AdaptiveNavBar 垂直/水平导航栏
Files:
-
Create:
lib/core/layout/adaptive_nav_bar.dart -
Step 1: 创建adaptive_nav_bar.dart
组件需要:
- 垂直模式(left/right):72px宽,3个Tab垂直排列,TabIconSprite(28x28)+标签(10px)
- 水平模式(top/bottom):与现有GlassBottomBar样式一致
- 毛玻璃背景
- 选中态:accent色+放大1.08x
- 未读红点支持
- 4种停靠位置
/// ============================================================
/// 闲言APP — 自适应导航栏
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 宽屏时垂直导航栏,窄屏时底部导航栏,支持4种停靠位置
/// 上次更新: 初始创建
/// ============================================================
import 'dart:ui';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/split_view_provider.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import '../theme/glass_tokens.dart';
import '../../features/discover/providers/chat_provider.dart';
import '../../features/mine/settings/providers/theme_settings_provider.dart';
import '../../shared/widgets/animation/tab_icon_sprite.dart';
class AdaptiveNavBar extends ConsumerWidget {
const AdaptiveNavBar({
required this.currentIndex,
required this.onTabSelected,
super.key,
});
final int currentIndex;
final ValueChanged<int> onTabSelected;
@override
Widget build(BuildContext context, WidgetRef ref) {
final position = ref.watch(navBarPositionProvider);
return switch (position) {
NavBarPosition.left || NavBarPosition.right => _buildVertical(context, ref),
NavBarPosition.top => _buildHorizontal(context, ref),
NavBarPosition.bottom => _buildHorizontal(context, ref),
};
}
Widget _buildVertical(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
final settings = ref.watch(themeSettingsProvider);
final unreadCount = ref.watch(chatProvider).unreadCount;
final animIntensity = settings.animationIntensity.durationMultiplier;
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
Widget buildTab(TabSpriteType type, int index, String label) {
final isSelected = index == currentIndex;
final showBadge = index == 1 && unreadCount > 0;
return GestureDetector(
onTap: () => onTabSelected(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedScale(
scale: isSelected ? 1.08 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: showBadge
? badges.Badge(
showBadge: true,
badgeContent: Text(
'',
style: TextStyle(
color: ext.textOnAccent,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
)
: TabIconSprite(
type: type,
label: '',
isSelected: isSelected,
adjacentDirection: 0,
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? (ext.isDark ? ext.textInverse : ext.accent)
: (ext.isDark
? ext.textInverse.withValues(alpha: 0.38)
: const Color(0xFFAEAEB2)),
),
),
],
),
),
);
}
return Container(
width: 72,
decoration: BoxDecoration(
color: ext.glassColor.withValues(
alpha: ext.isDark
? GlassTokens.elevatedOpacityDark
: GlassTokens.elevatedOpacityLight,
),
),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
),
child: SafeArea(
right: ref.watch(navBarPositionProvider) == NavBarPosition.right,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
buildTab(TabSpriteType.home, 0, '闲言'),
const SizedBox(height: AppSpacing.lg),
buildTab(TabSpriteType.discover, 1, '发现'),
const SizedBox(height: AppSpacing.lg),
buildTab(TabSpriteType.profile, 2, '我的'),
],
),
),
),
),
);
}
Widget _buildHorizontal(BuildContext context, WidgetRef ref) {
return const SizedBox.shrink();
}
}
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/adaptive_nav_bar.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/layout/adaptive_nav_bar.dart
git commit -m "feat: add AdaptiveNavBar with vertical/horizontal modes"
Task 6: OverviewDashboard 概览仪表盘
Files:
-
Create:
lib/core/layout/overview_dashboard.dart -
Step 1: 创建overview_dashboard.dart
概览仪表盘包含5个区块:问候区、今日推荐、快捷操作、最近浏览、数据统计。使用现有的设计令牌和GlassContainer。
/// ============================================================
/// 闲言APP — 概览仪表盘
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 作用: 宽屏分屏右侧面板的空状态页面,显示概览信息
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import '../theme/app_radius.dart';
import '../../shared/widgets/containers/glass_container.dart';
class OverviewDashboard extends ConsumerWidget {
const OverviewDashboard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGreeting(context),
const SizedBox(height: AppSpacing.lg),
_buildTodayRecommend(context),
const SizedBox(height: AppSpacing.lg),
_buildQuickActions(context),
const SizedBox(height: AppSpacing.lg),
_buildRecentHistory(context),
const SizedBox(height: AppSpacing.lg),
_buildStats(context),
const SizedBox(height: AppSpacing.xxl),
],
),
);
}
Widget _buildGreeting(BuildContext context) {
final ext = AppTheme.ext(context);
final hour = DateTime.now().hour;
final greeting = switch (hour) {
>= 6 && < 12 => '早上好 ☀️',
>= 12 && < 14 => '中午好 🌤️',
>= 14 && < 18 => '下午好 🌅',
>= 18 && < 22 => '晚上好 🌙',
_ => '夜深了 🌛',
};
return GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
greeting,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
'选择左侧内容查看详情',
style: TextStyle(
fontSize: 14,
color: ext.textSecondary,
),
),
],
),
),
],
),
);
}
Widget _buildTodayRecommend(BuildContext context) {
final ext = AppTheme.ext(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'✨ 今日推荐',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.sm),
SizedBox(
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: 3,
separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
itemBuilder: (context, index) {
return GlassContainer(
depth: GlassDepth.base,
width: 180,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📝 示例句子 ${index + 1}',
style: TextStyle(
fontSize: 13,
color: ext.textPrimary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
'—— 作者',
style: TextStyle(
fontSize: 11,
color: ext.textHint,
),
),
],
),
);
},
),
),
],
);
}
Widget _buildQuickActions(BuildContext context) {
final ext = AppTheme.ext(context);
final actions = [
('🔍', '搜索'),
('⭐', '收藏'),
('📖', '稍后读'),
('📝', '笔记'),
('📊', '统计'),
('⚙️', '设置'),
('🎨', '主题'),
('🔔', '提醒'),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'🚀 快捷操作',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: actions.map((action) {
return GestureDetector(
onTap: () {},
child: GlassContainer(
depth: GlassDepth.base,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(action.$1, style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.xs),
Text(
action.$2,
style: TextStyle(
fontSize: 13,
color: ext.textPrimary,
),
),
],
),
),
);
}).toList(),
),
],
);
}
Widget _buildRecentHistory(BuildContext context) {
final ext = AppTheme.ext(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'🕐 最近浏览',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
depth: GlassDepth.base,
padding: const EdgeInsets.all(AppSpacing.md),
child: Center(
child: Column(
children: [
Text('📭', style: const TextStyle(fontSize: 32)),
const SizedBox(height: AppSpacing.sm),
Text(
'暂无浏览记录',
style: TextStyle(fontSize: 13, color: ext.textHint),
),
],
),
),
),
],
);
}
Widget _buildStats(BuildContext context) {
final ext = AppTheme.ext(context);
final stats = [
('📖', '阅读', '0'),
('⭐', '收藏', '0'),
('👍', '点赞', '0'),
('🔥', '连续', '0天'),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📊 数据统计',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.sm),
Row(
children: stats.map((stat) {
return Expanded(
child: GlassContainer(
depth: GlassDepth.base,
padding: const EdgeInsets.all(AppSpacing.sm),
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.xs / 2),
child: Column(
children: [
Text(stat.$1, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 4),
Text(
stat.$3,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
Text(
stat.$2,
style: TextStyle(fontSize: 11, color: ext.textHint),
),
],
),
),
);
}).toList(),
),
],
);
}
}
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/overview_dashboard.dart
Expected: 无错误
- Step 3: Commit
git add lib/core/layout/overview_dashboard.dart
git commit -m "feat: add OverviewDashboard for split view empty state"
Task 7: 修改 AppShell 集成分屏布局
Files:
-
Modify:
lib/core/layout/app_shell.dart -
Step 1: 修改AppShell,添加宽屏分屏逻辑
在AppShell的build方法中,检测屏幕宽度:
- 宽屏(≥900px):使用Row布局 [AdaptiveNavBar + AdaptiveSplitView]
- 窄屏(<900px):保持现有Scaffold + GlassBottomBar
关键修改点:
- 导入新组件
- 在build方法开头检测屏幕宽度
- 宽屏时构建Row布局(NavBar + SplitView)
- 窄屏时保持现有逻辑
- 更新currentTabProvider
- Step 2: 验证编译
Run: flutter analyze lib/core/layout/app_shell.dart
Expected: 无错误
- Step 3: 运行应用验证窄屏行为不变
Run: flutter run
Expected: 窄屏设备上底部导航栏和页面行为与之前完全一致
- Step 4: Commit
git add lib/core/layout/app_shell.dart
git commit -m "feat: integrate AdaptiveSplitView into AppShell for widescreen"
Phase 2: 首页分屏适配
Task 8: SentenceDetailPanel 句子详情面板
Files:
-
Create:
lib/features/home/presentation/panels/sentence_detail_panel.dart -
Step 1: 从sentence_detail_sheet.dart提取内容为独立Widget
将_SentenceDetailContent提取为公共Widget SentenceDetailContent, SentenceDetailPanel包裹SentenceDetailContent并添加面板标题栏。 SentenceDetailSheet也改为使用SentenceDetailContent。
- Step 2: 注册面板
在RightPanelRegistry中注册 sentence_detail 和 home_dashboard。
- Step 3: 修改HomePage宽屏点击判断
点击句子卡片时:
-
宽屏:设置 homeRightPanelProvider 为 'sentence_detail',传递句子数据
-
窄屏:保持底部Sheet弹出
-
Step 4: 验证
Run: flutter run
Expected: 宽屏点击句子卡片→右侧显示详情面板;窄屏→底部Sheet
- Step 5: Commit
git add lib/features/home/presentation/panels/ lib/features/home/presentation/home_page.dart lib/features/home/presentation/providers/sentence_detail_sheet.dart
git commit -m "feat: add SentenceDetailPanel for home split view"
Phase 3: 发现页分屏适配
Task 9: ChatFlowPanel 会话流详情面板
Files:
-
Create:
lib/features/discover/presentation/panels/chat_flow_panel.dart -
Create:
lib/features/discover/presentation/panels/discover_dashboard.dart -
Step 1: 创建ChatFlowPanel
将ChatFlowPage的核心内容提取为可嵌入面板的Widget。 面板内包含:导航栏+搜索栏+分类栏+消息列表+输入栏。 需要处理键盘管理在面板内的适配。
- Step 2: 修改DiscoverPage宽屏点击判断
点击会话行时:
-
宽屏:设置 discoverRightPanelProvider 为 'chat_flow'
-
窄屏:保持全屏导航
-
Step 3: 验证
-
Step 4: Commit
git add lib/features/discover/presentation/panels/ lib/features/discover/presentation/pages/home/discover_page.dart
git commit -m "feat: add ChatFlowPanel for discover split view"
Phase 4: 我的页面分屏适配
Task 10: 设置项面板集合
Files:
-
Create:
lib/features/mine/settings/presentation/panels/settings_panels.dart -
Create:
lib/features/mine/profile/presentation/panels/profile_dashboard.dart -
Step 1: 创建各设置项面板
为每个设置项创建面板Widget,复用现有页面的内容区域。 面板包含:标题栏 + 设置内容。
-
Step 2: 修改ProfilePage宽屏点击判断
-
Step 3: 验证
-
Step 4: Commit
git add lib/features/mine/ lib/features/mine/profile/presentation/panels/
git commit -m "feat: add settings panels for profile split view"
Phase 5: 通用设置新增项
Task 11: 导航栏位置与分屏设置
Files:
-
Modify:
lib/features/mine/settings/presentation/general/general_settings_sections.dart -
Step 1: 在显示设置分组中新增3项
- 导航栏位置(selection):left/right/top/bottom
- 分屏默认比例(selection):30%-60%
- 分屏功能(toggle):on/off
-
Step 2: 验证
-
Step 3: Commit
git add lib/features/mine/settings/presentation/general/general_settings_sections.dart
git commit -m "feat: add nav bar position and split view settings"
Phase 6: 动画优化与最终验证
Task 12: 动画与过渡优化
- Step 1: 优化右侧面板滑入/滑出动画
确保AnimationController在宽屏↔窄屏切换时正确触发。
- Step 2: 添加导航栏位置切换动画
使用AnimatedSwitcher包裹NavBar。
- Step 3: 添加概览仪表盘入场动画
使用flutter_animate添加fadeIn + slideY + stagger。
- Step 4: 全量测试
验证所有断点、主题、手势隔离、面板切换。
- Step 5: Commit
git add -A
git commit -m "feat: polish animations and transitions for widescreen adaptation"
Phase 7: CHANGELOG与清理
Task 13: 更新CHANGELOG.md
- Step 1: 添加版本记录
在CHANGELOG.md中添加宽屏适配功能记录。
- Step 2: 删除spec文档(按用户要求)
删除 docs/specs/ 下的设计文档(开发完成后)。
- Step 3: 最终Commit
git add CHANGELOG.md
git commit -m "docs: update CHANGELOG for widescreen adaptation feature"