feat: 添加起卦教程首次访问追踪和Agent时间上下文

- 后端 ProfileSettingsV1 添加 DivinationTutorialSettings 字段
- 前端三个起卦页面添加首次访问检测,自动弹出教程
- 教程展示后更新 settings 标记,避免重复弹出
- 使用本地状态管理避免并发更新覆盖问题
- Agent 系统提示添加时间上下文信息
This commit is contained in:
qzl
2026-04-15 18:56:41 +08:00
parent 55eeab43df
commit 69b34bd723
13 changed files with 624 additions and 0 deletions
@@ -18,6 +18,7 @@ import '../../../../shared/widgets/divination/yao_line_row.dart';
import '../../../../shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../settings/data/models/profile_settings.dart';
import '../../data/models/divination_backend_models.dart';
import '../../data/apis/divination_api.dart';
import '../../data/models/divination_params.dart';
@@ -33,12 +34,17 @@ class AutoDivinationScreen extends StatefulWidget {
required this.runService,
this.divinationApi,
required this.onCompleted,
required this.profileSettings,
required this.onProfileSettingsChanged,
});
final DivinationParams params;
final DivinationRunService runService;
final DivinationApi? divinationApi;
final Future<void> Function(DivinationResultData result) onCompleted;
final ProfileSettingsV1 profileSettings;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
@override
State<AutoDivinationScreen> createState() => _AutoDivinationScreenState();
@@ -64,6 +70,8 @@ class _AutoDivinationScreenState extends State<AutoDivinationScreen>
DateTime _lastShake = DateTime.fromMillisecondsSinceEpoch(0);
bool _spinLocked = false;
bool _submitting = false;
bool _tutorialChecked = false;
late DivinationTutorialSettings _localTutorialSettings;
final GlobalKey<OnboardingState> _onboardingKey =
GlobalKey<OnboardingState>();
@@ -89,6 +97,37 @@ class _AutoDivinationScreenState extends State<AutoDivinationScreen>
duration: const Duration(milliseconds: 500),
)..repeat(reverse: true);
_listenShake();
_localTutorialSettings = widget.profileSettings.divinationTutorial;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_tutorialChecked) {
_tutorialChecked = true;
_checkFirstVisit();
}
}
void _checkFirstVisit() {
if (!_localTutorialSettings.autoDivinationShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_showGuide();
});
}
}
Future<void> _markTutorialShown() async {
setState(() {
_localTutorialSettings = _localTutorialSettings.copyWith(
autoDivinationShown: true,
);
});
final updated = widget.profileSettings.copyWith(
divinationTutorial: _localTutorialSettings,
);
await widget.onProfileSettingsChanged(updated);
}
@override
@@ -176,6 +215,7 @@ class _AutoDivinationScreenState extends State<AutoDivinationScreen>
key: _onboardingKey,
steps: steps,
onChanged: _onGuideStepChanged,
onEnd: (_) => _markTutorialShown(),
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(AppSpacing.xl),
@@ -10,6 +10,7 @@ import '../../../../shared/widgets/gua_icon.dart';
import '../../../../shared/widgets/divination/divination_shared_widgets.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../settings/data/models/profile_settings.dart';
import '../../data/apis/divination_api.dart';
import '../../data/models/divination_params.dart';
import '../../data/models/divination_result.dart';
@@ -26,6 +27,8 @@ class DivinationScreen extends StatefulWidget {
this.runServiceOverride,
this.divinationApiOverride,
this.allowVibration = true,
required this.profileSettings,
required this.onProfileSettingsChanged,
});
final SessionStore sessionStore;
@@ -34,6 +37,9 @@ class DivinationScreen extends StatefulWidget {
final DivinationRunService? runServiceOverride;
final DivinationApi? divinationApiOverride;
final bool allowVibration;
final ProfileSettingsV1 profileSettings;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
@override
State<DivinationScreen> createState() => _DivinationScreenState();
@@ -44,6 +50,8 @@ class _DivinationScreenState extends State<DivinationScreen> {
final TextEditingController _questionController = TextEditingController();
late final DivinationApi _divinationApi;
late final DivinationRunService _runService;
bool _tutorialChecked = false;
late DivinationTutorialSettings _localTutorialSettings;
@override
void initState() {
@@ -65,6 +73,47 @@ class _DivinationScreenState extends State<DivinationScreen> {
userId: widget.userId,
);
_questionController.addListener(_syncQuestion);
_localTutorialSettings = widget.profileSettings.divinationTutorial;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_tutorialChecked) {
_tutorialChecked = true;
_checkFirstVisit();
}
}
void _checkFirstVisit() {
if (!_localTutorialSettings.divinationEntryShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final l10n = AppLocalizations.of(context)!;
_showGuide(context, l10n);
_markTutorialShown('divination_entry');
});
}
}
Future<void> _markTutorialShown(String key) async {
setState(() {
_localTutorialSettings = _localTutorialSettings.copyWith(
divinationEntryShown: key == 'divination_entry'
? true
: _localTutorialSettings.divinationEntryShown,
autoDivinationShown: key == 'auto_divination'
? true
: _localTutorialSettings.autoDivinationShown,
manualDivinationShown: key == 'manual_divination'
? true
: _localTutorialSettings.manualDivinationShown,
);
});
final updated = widget.profileSettings.copyWith(
divinationTutorial: _localTutorialSettings,
);
await widget.onProfileSettingsChanged(updated);
}
@override
@@ -161,6 +210,10 @@ class _DivinationScreenState extends State<DivinationScreen> {
return;
}
final updatedSettings = widget.profileSettings.copyWith(
divinationTutorial: _localTutorialSettings,
);
if (_params.method == DivinationMethod.manual) {
final nextParams = _params.copyWith(divinationTime: DateTime.now());
Navigator.of(context).push(
@@ -170,6 +223,8 @@ class _DivinationScreenState extends State<DivinationScreen> {
runService: _runService,
divinationApi: _divinationApi,
onCompleted: widget.onCompleted,
profileSettings: updatedSettings,
onProfileSettingsChanged: widget.onProfileSettingsChanged,
),
),
);
@@ -187,6 +242,8 @@ class _DivinationScreenState extends State<DivinationScreen> {
runService: _runService,
divinationApi: _divinationApi,
onCompleted: widget.onCompleted,
profileSettings: updatedSettings,
onProfileSettingsChanged: widget.onProfileSettingsChanged,
),
),
);
@@ -15,6 +15,7 @@ import '../../../../shared/widgets/divination/yao_line_row.dart';
import '../../../../shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../settings/data/models/profile_settings.dart';
import '../../data/models/divination_backend_models.dart';
import '../../data/apis/divination_api.dart';
import '../../data/models/divination_params.dart';
@@ -30,12 +31,17 @@ class ManualDivinationScreen extends StatefulWidget {
required this.runService,
this.divinationApi,
required this.onCompleted,
required this.profileSettings,
required this.onProfileSettingsChanged,
});
final DivinationParams params;
final DivinationRunService runService;
final DivinationApi? divinationApi;
final Future<void> Function(DivinationResultData result) onCompleted;
final ProfileSettingsV1 profileSettings;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
@override
State<ManualDivinationScreen> createState() => _ManualDivinationScreenState();
@@ -47,6 +53,8 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
final List<YaoType?> _selectedYaos = List<YaoType?>.filled(6, null);
late final AnimationController _blinkController;
bool _submitting = false;
bool _tutorialChecked = false;
late DivinationTutorialSettings _localTutorialSettings;
final GlobalKey<OnboardingState> _onboardingKey =
GlobalKey<OnboardingState>();
final ScrollController _scrollController = ScrollController();
@@ -66,6 +74,37 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
vsync: this,
duration: const Duration(milliseconds: 500),
)..repeat(reverse: true);
_localTutorialSettings = widget.profileSettings.divinationTutorial;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_tutorialChecked) {
_tutorialChecked = true;
_checkFirstVisit();
}
}
void _checkFirstVisit() {
if (!_localTutorialSettings.manualDivinationShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_showGuide();
});
}
}
Future<void> _markTutorialShown() async {
setState(() {
_localTutorialSettings = _localTutorialSettings.copyWith(
manualDivinationShown: true,
);
});
final updated = widget.profileSettings.copyWith(
divinationTutorial: _localTutorialSettings,
);
await widget.onProfileSettingsChanged(updated);
}
@override
@@ -146,6 +185,7 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
key: _onboardingKey,
steps: guideSteps,
onChanged: _onGuideStepChanged,
onEnd: (_) => _markTutorialShown(),
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(AppSpacing.xl),
@@ -127,6 +127,8 @@ class _HomeScreenState extends State<HomeScreen> {
allowVibration: widget.profileSettings.notification.allowVibration,
notificationBloc: widget.notificationBloc,
notificationRepository: widget.notificationRepository,
profileSettings: widget.profileSettings,
onProfileSettingsChanged: widget.onProfileSettingsChanged,
),
_ProfileTab(
account: widget.account,
@@ -160,6 +162,8 @@ class _HomeScreenState extends State<HomeScreen> {
userId: widget.account,
onCompleted: widget.onDivinationCompleted,
allowVibration: widget.profileSettings.notification.allowVibration,
profileSettings: widget.profileSettings,
onProfileSettingsChanged: widget.onProfileSettingsChanged,
),
),
);
@@ -177,6 +181,8 @@ class _HomeTab extends StatelessWidget {
required this.allowVibration,
required this.notificationBloc,
required this.notificationRepository,
required this.profileSettings,
required this.onProfileSettingsChanged,
});
final List<DivinationResultData> historyItems;
@@ -189,6 +195,9 @@ class _HomeTab extends StatelessWidget {
final bool allowVibration;
final NotificationBloc notificationBloc;
final NotificationRepository notificationRepository;
final ProfileSettingsV1 profileSettings;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
@override
Widget build(BuildContext context) {
@@ -304,6 +313,9 @@ class _HomeTab extends StatelessWidget {
userId: userId,
onCompleted: onDivinationCompleted,
allowVibration: allowVibration,
profileSettings: profileSettings,
onProfileSettingsChanged:
onProfileSettingsChanged,
),
),
);
@@ -51,6 +51,14 @@ class ProfileApi {
'allow_notifications': settings.notification.allowNotifications,
'allow_vibration': settings.notification.allowVibration,
},
'divination_tutorial': {
'divination_entry_shown':
settings.divinationTutorial.divinationEntryShown,
'auto_divination_shown':
settings.divinationTutorial.autoDivinationShown,
'manual_divination_shown':
settings.divinationTutorial.manualDivinationShown,
},
},
};
final json = await _apiClient.rawDio.patch<Map<String, dynamic>>(
@@ -119,6 +127,23 @@ class ProfileApi {
)
: const NotificationSettings();
final divinationTutorialRaw = settingsRaw is Map<String, dynamic>
? settingsRaw['divination_tutorial']
: null;
final divinationTutorial = divinationTutorialRaw is Map<String, dynamic>
? DivinationTutorialSettings(
divinationEntryShown:
(divinationTutorialRaw['divination_entry_shown'] as bool?) ??
true,
autoDivinationShown:
(divinationTutorialRaw['auto_divination_shown'] as bool?) ??
true,
manualDivinationShown:
(divinationTutorialRaw['manual_divination_shown'] as bool?) ??
true,
)
: const DivinationTutorialSettings();
return ProfileSettingsV1(
displayName: (json['display_name'] as String?) ?? '',
bio: (json['bio'] as String?) ?? '',
@@ -130,6 +155,7 @@ class ProfileApi {
const <String, dynamic>{})
: const <String, dynamic>{},
notification: notification,
divinationTutorial: divinationTutorial,
);
}
}
@@ -59,6 +59,31 @@ class NotificationSettings {
}
}
class DivinationTutorialSettings {
const DivinationTutorialSettings({
this.divinationEntryShown = true,
this.autoDivinationShown = true,
this.manualDivinationShown = true,
});
final bool divinationEntryShown;
final bool autoDivinationShown;
final bool manualDivinationShown;
DivinationTutorialSettings copyWith({
bool? divinationEntryShown,
bool? autoDivinationShown,
bool? manualDivinationShown,
}) {
return DivinationTutorialSettings(
divinationEntryShown: divinationEntryShown ?? this.divinationEntryShown,
autoDivinationShown: autoDivinationShown ?? this.autoDivinationShown,
manualDivinationShown:
manualDivinationShown ?? this.manualDivinationShown,
);
}
}
class ProfileSettingsV1 {
const ProfileSettingsV1({
this.version = 1,
@@ -69,6 +94,7 @@ class ProfileSettingsV1 {
this.preferences = const PreferenceSettings(),
this.privacy = const <String, Object?>{},
this.notification = const NotificationSettings(),
this.divinationTutorial = const DivinationTutorialSettings(),
});
final int version;
@@ -79,6 +105,7 @@ class ProfileSettingsV1 {
final PreferenceSettings preferences;
final Map<String, Object?> privacy;
final NotificationSettings notification;
final DivinationTutorialSettings divinationTutorial;
ProfileSettingsV1 copyWith({
int? version,
@@ -89,6 +116,7 @@ class ProfileSettingsV1 {
PreferenceSettings? preferences,
Map<String, Object?>? privacy,
NotificationSettings? notification,
DivinationTutorialSettings? divinationTutorial,
}) {
return ProfileSettingsV1(
version: version ?? this.version,
@@ -99,6 +127,7 @@ class ProfileSettingsV1 {
preferences: preferences ?? this.preferences,
privacy: privacy ?? this.privacy,
notification: notification ?? this.notification,
divinationTutorial: divinationTutorial ?? this.divinationTutorial,
);
}