feat: 实现用户画像、占卜历史与后端用户管理模块

This commit is contained in:
ZL-Q
2026-04-06 01:28:10 +08:00
parent d87b2e1e3a
commit 8a18b3528b
77 changed files with 5850 additions and 2604 deletions
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import '../../../../core/auth/session_store.dart';
import '../../../divination/presentation/screens/divination_screen.dart';
import '../../../divination/presentation/screens/divination_result_screen.dart';
import '../../../divination/data/models/divination_params.dart';
import '../../../divination/data/models/divination_result.dart';
import '../../../settings/data/models/profile_settings.dart';
import '../../../settings/presentation/screens/settings_screen.dart';
import '../../../../l10n/app_localizations.dart';
@@ -18,8 +21,12 @@ class HomeScreen extends StatefulWidget {
required this.sessionStore,
required this.currentLocale,
required this.profileSettings,
required this.historyRecords,
required this.coinBalance,
required this.onLocaleChanged,
required this.onProfileSettingsChanged,
required this.onUploadAvatar,
required this.onDivinationCompleted,
required this.onLogout,
});
@@ -27,8 +34,14 @@ class HomeScreen extends StatefulWidget {
final SessionStore sessionStore;
final Locale currentLocale;
final ProfileSettingsV1 profileSettings;
final List<DivinationResultData> historyRecords;
final int coinBalance;
final Future<void> Function(String languageTag) onLocaleChanged;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
final Future<ProfileSettingsV1> Function(String filePath) onUploadAvatar;
final Future<void> Function(DivinationResultData result)
onDivinationCompleted;
final Future<void> Function() onLogout;
@override
@@ -69,26 +82,7 @@ class _HomeScreenState extends State<HomeScreen> {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
final historyItems = [
_HistoryItemData(
question: l10n.historyQuestion1,
category: _HistoryCategory.career,
guaName: l10n.guaName1,
sign: _HistorySign.good,
),
_HistoryItemData(
question: l10n.historyQuestion2,
category: _HistoryCategory.love,
guaName: l10n.guaName2,
sign: _HistorySign.normal,
),
_HistoryItemData(
question: l10n.historyQuestion3,
category: _HistoryCategory.money,
guaName: l10n.guaName3,
sign: _HistorySign.best,
),
];
final historyItems = widget.historyRecords;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
@@ -212,7 +206,23 @@ class _HomeScreenState extends State<HomeScreen> {
style: Theme.of(context).textTheme.titleMedium,
),
TextButton(
onPressed: () => _showSnack(context, l10n.featurePending),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => _HistoryRecordsScreen(
records: historyItems,
onOpenResult: (item) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) =>
DivinationResultScreen(data: item),
),
);
},
),
),
);
},
child: Text(l10n.more),
),
],
@@ -245,7 +255,17 @@ class _HomeScreenState extends State<HomeScreen> {
right: AppSpacing.md,
bottom: AppSpacing.md,
),
child: _HistoryCard(item: item),
child: _HistoryCard(
item: item,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) =>
DivinationResultScreen(data: item),
),
);
},
),
);
}).toList(),
),
@@ -270,6 +290,8 @@ class _HomeScreenState extends State<HomeScreen> {
settings: widget.profileSettings,
coinBalance: widget.coinBalance,
onInterfaceLanguageChanged: widget.onLocaleChanged,
onSettingsChanged: widget.onProfileSettingsChanged,
onUploadAvatar: widget.onUploadAvatar,
onLogout: widget.onLogout,
),
),
@@ -283,6 +305,7 @@ class _HomeScreenState extends State<HomeScreen> {
builder: (_) => DivinationScreen(
sessionStore: widget.sessionStore,
userId: widget.account,
onCompleted: widget.onDivinationCompleted,
),
),
);
@@ -294,9 +317,10 @@ class _HomeScreenState extends State<HomeScreen> {
}
class _HistoryCard extends StatelessWidget {
const _HistoryCard({required this.item});
const _HistoryCard({required this.item, required this.onTap});
final _HistoryItemData item;
final DivinationResultData item;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
@@ -304,80 +328,90 @@ class _HistoryCard extends StatelessWidget {
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
final categoryLabel = switch (item.category) {
_HistoryCategory.career => l10n.categoryCareer,
_HistoryCategory.love => l10n.categoryLove,
_HistoryCategory.money => l10n.categoryMoney,
final categoryLabel = switch (item.params.questionType) {
QuestionType.career || QuestionType.study => l10n.categoryCareer,
QuestionType.love => l10n.categoryLove,
_ => l10n.categoryMoney,
};
final categoryStyle = switch (item.category) {
_HistoryCategory.career => (
final categoryStyle = switch (item.params.questionType) {
QuestionType.career || QuestionType.study => (
palette.categoryCareerBg,
palette.categoryCareerText,
),
_HistoryCategory.love => (
palette.categoryLoveBg,
palette.categoryLoveText,
),
_HistoryCategory.money => (
palette.categoryMoneyBg,
palette.categoryMoneyText,
),
QuestionType.love => (palette.categoryLoveBg, palette.categoryLoveText),
_ => (palette.categoryMoneyBg, palette.categoryMoneyText),
};
final signLabel = switch (item.sign) {
_HistorySign.best => l10n.signBest,
_HistorySign.good => l10n.signGood,
_HistorySign.normal => l10n.signNormal,
};
final normalizedSignType = item.signType.trim();
final isBestSign = normalizedSignType.contains('上上');
final isGoodSign = !isBestSign && normalizedSignType.contains('中上');
final isWorstSign = normalizedSignType.contains('下下');
final signStyle = switch (item.sign) {
_HistorySign.best => (palette.historyGoldBg, palette.historyGoldText),
_HistorySign.good => (colors.surfaceContainerHighest, colors.primary),
_HistorySign.normal => (palette.historyGrayBg, palette.historyGrayText),
};
final signLabel = isBestSign
? l10n.signTypeShangShang
: isGoodSign
? l10n.signTypeZhongShang
: isWorstSign
? l10n.signTypeXiaXia
: l10n.signTypeZhongXia;
return Card(
margin: EdgeInsets.zero,
color: colors.surface,
elevation: 2,
shape: RoundedRectangleBorder(
final signStyle = isBestSign
? (palette.historyGoldBg, palette.historyGoldText)
: isGoodSign
? (colors.surfaceContainerHighest, colors.primary)
: isWorstSign
? (colors.errorContainer, colors.onErrorContainer)
: (palette.historyGrayBg, palette.historyGrayText);
return Material(
color: colors.surface.withValues(alpha: 0),
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.question,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
onTap: onTap,
child: Card(
margin: EdgeInsets.zero,
color: colors.surface,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Tag(
label: categoryLabel,
background: categoryStyle.$1,
foreground: categoryStyle.$2,
Text(
item.params.question,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
_Tag(
label: item.guaName,
background: palette.historyBlueBg,
foreground: palette.historyBlueText,
),
_Tag(
label: signLabel,
background: signStyle.$1,
foreground: signStyle.$2,
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: [
_Tag(
label: categoryLabel,
background: categoryStyle.$1,
foreground: categoryStyle.$2,
),
_Tag(
label: item.guaName,
background: palette.historyBlueBg,
foreground: palette.historyBlueText,
),
_Tag(
label: signLabel,
background: signStyle.$1,
foreground: signStyle.$2,
),
],
),
],
),
],
),
),
),
);
@@ -416,6 +450,57 @@ class _Tag extends StatelessWidget {
}
}
class _HistoryRecordsScreen extends StatelessWidget {
const _HistoryRecordsScreen({
required this.records,
required this.onOpenResult,
});
final List<DivinationResultData> records;
final ValueChanged<DivinationResultData> onOpenResult;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.historyTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: records.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.noRecords,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
Text(l10n.noRecordsSubtitle),
],
),
)
: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
itemBuilder: (context, index) {
final item = records[index];
return _HistoryCard(
item: item,
onTap: () => onOpenResult(item),
);
},
separatorBuilder: (_, _) => const SizedBox(height: AppSpacing.md),
itemCount: records.length,
),
);
}
}
class _WelcomeDialog extends StatefulWidget {
const _WelcomeDialog({required this.onDone});
@@ -576,21 +661,3 @@ class _WelcomeDialogState extends State<_WelcomeDialog> {
);
}
}
enum _HistoryCategory { career, love, money }
enum _HistorySign { best, good, normal }
class _HistoryItemData {
const _HistoryItemData({
required this.question,
required this.category,
required this.guaName,
required this.sign,
});
final String question;
final _HistoryCategory category;
final String guaName;
final _HistorySign sign;
}