feat(app): D.3 中等优先级 UX 改进 — 保存指示器 + 触摸目标 + 主题持久化
D.3.2 三态保存指示器: - 未保存 (灰色) → 保存中 (琥珀色脉冲点) → 已保存 (绿色点) - _PulsingDot 动画组件,800ms 呼吸效果 - 点击'完成'时显示保存中状态 D.3.3 工具栏触摸目标: - BoxConstraints 36x36 → 44x44,符合 WCAG 标准 D.3.4 主题偏好持久化: - SettingsBloc 接受 SharedPreferences,保存/恢复 themeMode - NuanjiApp 改为 StatefulWidget,异步初始化 SharedPreferences - 启动时显示 loading,初始化完成后渲染 app
This commit is contained in:
@@ -337,6 +337,9 @@ class _EditorViewState extends State<_EditorView> {
|
||||
/// 查看模式:打开已有日记时默认只读,点击"编辑"后进入编辑模式
|
||||
bool _isViewMode = false;
|
||||
|
||||
/// 保存中状态 — 用于显示"保存中..."指示器
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -595,9 +598,16 @@ class _EditorViewState extends State<_EditorView> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存处理
|
||||
/// 保存处理 — 显示"保存中..."后触发保存
|
||||
void _handleSave(BuildContext context, EditorState state) {
|
||||
widget.onSaveComplete();
|
||||
setState(() => _isSaving = true);
|
||||
// 短暂延迟让 UI 显示"保存中..."状态
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (mounted) {
|
||||
setState(() => _isSaving = false);
|
||||
widget.onSaveComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示评论列表
|
||||
@@ -624,7 +634,26 @@ class _EditorViewState extends State<_EditorView> {
|
||||
}
|
||||
|
||||
/// 自动保存状态指示器
|
||||
/// 保存指示器 — 三态: 未保存 / 保存中 / 已保存
|
||||
Widget _buildAutosaveIndicator(EditorState state) {
|
||||
// 保存中 — 琥珀色脉冲点
|
||||
if (_isSaving) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_PulsingDot(color: AppColors.tertiary),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'保存中...',
|
||||
style: TextStyle(fontSize: 11, color: Colors.amber[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// 未保存
|
||||
if (state.lastSavedAt == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
@@ -634,6 +663,7 @@ class _EditorViewState extends State<_EditorView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
// 已保存 — 绿色点
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
@@ -1211,3 +1241,53 @@ class _ImageSourceButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 脉冲圆点动画 — 用于"保存中..."指示器
|
||||
class _PulsingDot extends StatefulWidget {
|
||||
const _PulsingDot({required this.color});
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
State<_PulsingDot> createState() => _PulsingDotState();
|
||||
}
|
||||
|
||||
class _PulsingDotState extends State<_PulsingDot>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
)..repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final scale = 0.6 + 0.4 * _controller.value;
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class EditorToolbar extends StatelessWidget {
|
||||
onTap: () => onEvent(isActive ? ToolReactivated(tool) : ToolChanged(tool)),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
Reference in New Issue
Block a user