feat: 实现用户画像、占卜历史与后端用户管理模块
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user