Files
xianyan/docs/superpowers/plans/2026-05-29-widescreen-adaptation.md
Developer 5a49d20c8a chore: 完成项目品牌域名批量替换与功能迭代
本次提交包含多项核心更新:
1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等
2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果
3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线
4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题
5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航
6. 新增收藏与时间线数据合并导入功能
7. 修复多处UI样式问题,统一主题颜色使用规范
8. 新增日历同步与跨平台触觉反馈依赖库
9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
2026-05-29 10:06:55 +08:00

38 KiB
Raw Blame History

宽屏适配 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/right72px宽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

关键修改点:

  1. 导入新组件
  2. 在build方法开头检测屏幕宽度
  3. 宽屏时构建Row布局NavBar + SplitView
  4. 窄屏时保持现有逻辑
  5. 更新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_detailhome_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项

  1. 导航栏位置selectionleft/right/top/bottom
  2. 分屏默认比例selection30%-60%
  3. 分屏功能toggleon/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"