Skip to content

Commit 89fb145

Browse files
authored
refactor(settings): 重构设置页面UI组件使用统一Material Design 3设计风格 (#87)
* refactor(settings): 重构设置页面布局和组件结构 将设置页面的布局重构为更模块化的组件结构,包括: - 添加分割线辅助方法 - 创建可复用的设置分组组件 - 实现主题模式选择组件 - 优化导航项和功能项的构建方式 - 改进UI一致性和Material 3设计规范 * refactor(settings): 抽取设置页面基础组件并更新文档 将 SettingsSection、SettingsNavigationTile 和 FilterChipSelector 抽取为独立组件 更新 Material Design 3 设置页面实现规范文档 重构 settings_screen.dart 使用新组件 * refactor(settings): 重构设置页面UI组件使用统一设计风格 移除页面描述文本,使用新的SettingsSection和SettingsNavigationTile组件 统一语言选择对话框的样式和交互 调整间距和布局以符合设计规范 * refactor(settings): 将语言选择弹窗改为底部抽屉并优化样式 优化语言选择交互,将原来的对话框改为可拖拽的底部抽屉,提升移动端操作体验 移除测试中不再需要的 chevron_right 图标检查 为 AI 标签设置页面添加滚动支持 * refactor(translation_settings): 简化翻译目标语种选择界面样式 移除关闭按钮和分隔线,优化列表项背景色实现方式 * feat(ui): 将语言选择对话框改为底部弹窗以提升用户体验 * test: 更新测试用例以使用BottomSheet替代AlertDialog refactor: 添加Mockito测试桩并重构AppUpdateUseCase测试 * refactor(ui): 优化 FilterChipSelector 布局并使用主题文本样式 将 Row 布局改为 Wrap 以支持自动换行 使用主题的 labelSmall 文本样式并调整字重 移除未使用的 ChooseThemeDialog 组件 * style(settings): 使用主题文本样式替代硬编码样式 * fix(settings): 添加导出日志按钮执行状态检查 防止在导出日志操作执行中重复点击
1 parent 1013ece commit 89fb145

12 files changed

+1187
-625
lines changed

.trae/rules/project_rules.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,3 +619,81 @@ This project follows a Test-Driven Development (TDD) approach. All new features
619619
### 注意事项
620620

621621
- 遇到 Mockito null safety 错误时,首先运行 flutter pub run build_runner build
622+
623+
## Material Design 3 设置页面实现规范
624+
625+
基于 `lib/ui/settings/widgets/settings_screen.dart` 的标准实现以及抽取的基础组件
626+
627+
### 核心架构模式
628+
629+
**分组设置架构**:使用分组标题 + 卡片容器 + 列表项的三层结构
630+
- 标题:`titleSmall` + `primary` 颜色 + `fontWeight.w600` + `letterSpacing: 0.1`
631+
- 容器:`surfaceContainerLow` 背景 + `elevation: 0` 扁平化设计
632+
- 列表项:标准 `ListTile` + 自定义内容边距
633+
634+
### 样式实现要点
635+
636+
1. **主题一致性**:所有样式基于 `Theme.of(context)` 获取,禁止硬编码颜色、字号
637+
2. **边距规范**
638+
- 分组标题:左侧 16px,底部 8px
639+
- 列表项内容:水平 16px,垂直 4px
640+
- 分组间距:24px
641+
3. **图标规范**:24px 大小,使用 `onSurfaceVariant` 颜色
642+
4. **分割线系统**:1px 厚度,`surface` 颜色,动态插入列表项间
643+
644+
### Command 集成模式
645+
646+
使用 `CommandBuilder` 处理异步操作的三种状态:
647+
- **执行中**:显示 `CircularProgressIndicator`
648+
- **成功**:显示 `check_circle` 图标
649+
- **错误**:显示 `error` 图标 + `error` 颜色
650+
651+
### 特殊组件实现
652+
653+
1. **主题选择器**`FilterChip` 实现多选一
654+
- 选中状态:`secondaryContainer` + `onSecondaryContainer` 配色
655+
- 文本样式:基于 `Theme.of(context).textTheme.labelSmall` 并通过 `copyWith(fontWeight: FontWeight.w500)` 调整字重
656+
657+
2. **导航设置项**:图标 + 标题 + 副标题的标准布局
658+
- 支持可选的 `trailing` 组件(如更新提示标签)
659+
660+
### 基础组件库
661+
662+
项目中已抽取以下可复用组件,位于 `lib/ui/core/ui/`
663+
664+
1. **SettingsSection**:标准化设置分组组件
665+
```dart
666+
SettingsSection(
667+
title: '连接设置',
668+
children: [/* 子组件列表 */],
669+
)
670+
```
671+
672+
2. **SettingsNavigationTile**:导航设置项组件
673+
```dart
674+
SettingsNavigationTile(
675+
icon: Icons.api,
676+
title: 'API 配置',
677+
subtitle: '配置 Readeck 服务器连接',
678+
onTap: () => context.push(Routes.apiConfigSetting),
679+
)
680+
```
681+
682+
3. **FilterChipSelector**:多选一选择器组件
683+
```dart
684+
FilterChipSelector<ThemeMode>(
685+
options: ThemeMode.values,
686+
selectedValue: viewModel.themeMode,
687+
onSelectionChanged: (mode) => viewModel.setThemeMode.execute(mode),
688+
labelBuilder: _getThemeModeText,
689+
)
690+
```
691+
692+
> **注意**:分割线逻辑已内置在 `SettingsSection` 组件中,无需额外处理。
693+
694+
### 使用规范
695+
696+
1. **优先使用基础组件**:新的设置页面应优先使用已抽取的基础组件
697+
2. **保持一致性**:所有设置页面都应遵循相同的视觉规范和交互模式
698+
3. **主题兼容**:确保所有组件在不同主题模式下正常显示
699+
4. **可访问性**:保持足够的触摸目标大小和对比度
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:flutter/material.dart';
2+
3+
class FilterChipSelector<T> extends StatelessWidget {
4+
const FilterChipSelector({
5+
super.key,
6+
required this.options,
7+
required this.selectedValue,
8+
required this.onSelectionChanged,
9+
required this.labelBuilder,
10+
});
11+
12+
final List<T> options;
13+
final T selectedValue;
14+
final ValueChanged<T> onSelectionChanged;
15+
final String Function(T) labelBuilder;
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
return Wrap(
20+
spacing: 8.0,
21+
children: options.map((option) {
22+
final isSelected = selectedValue == option;
23+
return FilterChip(
24+
selected: isSelected,
25+
label: Text(labelBuilder(option)),
26+
onSelected: (selected) {
27+
if (selected) {
28+
onSelectionChanged(option);
29+
}
30+
},
31+
selectedColor: Theme.of(context).colorScheme.secondaryContainer,
32+
checkmarkColor: Theme.of(context).colorScheme.onSecondaryContainer,
33+
labelStyle: Theme.of(context).textTheme.labelSmall?.copyWith(
34+
color: isSelected
35+
? Theme.of(context).colorScheme.onSecondaryContainer
36+
: Theme.of(context).colorScheme.onSurface,
37+
fontWeight: FontWeight.w500,
38+
),
39+
);
40+
}).toList(),
41+
);
42+
}
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:flutter/material.dart';
2+
3+
class SettingsNavigationTile extends StatelessWidget {
4+
const SettingsNavigationTile({
5+
super.key,
6+
required this.icon,
7+
required this.title,
8+
required this.subtitle,
9+
required this.onTap,
10+
this.trailing,
11+
});
12+
13+
final IconData icon;
14+
final String title;
15+
final String subtitle;
16+
final VoidCallback onTap;
17+
final Widget? trailing;
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return ListTile(
22+
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
23+
leading: Icon(
24+
icon,
25+
color: Theme.of(context).colorScheme.onSurfaceVariant,
26+
size: 24,
27+
),
28+
title: Text(
29+
title,
30+
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
31+
fontWeight: FontWeight.w500,
32+
),
33+
),
34+
subtitle: Text(
35+
subtitle,
36+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
37+
color: Theme.of(context).colorScheme.onSurfaceVariant,
38+
),
39+
),
40+
trailing: trailing,
41+
onTap: onTap,
42+
);
43+
}
44+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter/material.dart';
2+
3+
class SettingsSection extends StatelessWidget {
4+
const SettingsSection({
5+
super.key,
6+
required this.title,
7+
required this.children,
8+
});
9+
10+
final String title;
11+
final List<Widget> children;
12+
13+
List<Widget> _addDividersBetweenChildren(
14+
List<Widget> children, BuildContext context) {
15+
if (children.length <= 1) return children;
16+
17+
final List<Widget> result = [];
18+
for (int i = 0; i < children.length; i++) {
19+
result.add(children[i]);
20+
if (i < children.length - 1) {
21+
result.add(Divider(
22+
height: 1,
23+
thickness: 1,
24+
color: Theme.of(context).colorScheme.surface,
25+
));
26+
}
27+
}
28+
return result;
29+
}
30+
31+
@override
32+
Widget build(BuildContext context) {
33+
return Column(
34+
crossAxisAlignment: CrossAxisAlignment.start,
35+
children: [
36+
Padding(
37+
padding: const EdgeInsets.only(left: 16, bottom: 8),
38+
child: Text(
39+
title,
40+
style: Theme.of(context).textTheme.titleSmall?.copyWith(
41+
color: Theme.of(context).colorScheme.primary,
42+
fontWeight: FontWeight.w600,
43+
letterSpacing: 0.1,
44+
),
45+
),
46+
),
47+
Card(
48+
margin: EdgeInsets.zero,
49+
elevation: 0,
50+
color: Theme.of(context).colorScheme.surfaceContainerLow,
51+
clipBehavior: Clip.antiAlias,
52+
child: Column(
53+
children: _addDividersBetweenChildren(children, context),
54+
),
55+
),
56+
],
57+
);
58+
}
59+
}

0 commit comments

Comments
 (0)