feat: 添加起卦教程首次访问追踪和Agent时间上下文
- 后端 ProfileSettingsV1 添加 DivinationTutorialSettings 字段 - 前端三个起卦页面添加首次访问检测,自动弹出教程 - 教程展示后更新 settings 标记,避免重复弹出 - 使用本地状态管理避免并发更新覆盖问题 - Agent 系统提示添加时间上下文信息
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user