本次提交包含多项核心更新: 1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等 2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果 3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线 4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题 5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航 6. 新增收藏与时间线数据合并导入功能 7. 修复多处UI样式问题,统一主题颜色使用规范 8. 新增日历同步与跨平台触觉反馈依赖库 9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
1284 lines
38 KiB
Markdown
1284 lines
38 KiB
Markdown
# 宽屏适配 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**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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种停靠位置
|
||
|
||
```dart
|
||
/// ============================================================
|
||
/// 闲言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**
|
||
|
||
```bash
|
||
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。
|
||
|
||
```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 '../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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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. 导航栏位置(selection):left/right/top/bottom
|
||
2. 分屏默认比例(selection):30%-60%
|
||
3. 分屏功能(toggle):on/off
|
||
|
||
- [ ] **Step 2: 验证**
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
git add CHANGELOG.md
|
||
git commit -m "docs: update CHANGELOG for widescreen adaptation feature"
|
||
```
|