feat(home): 首页改为三列探索网格
This commit is contained in:
1 parent
6dea9d479e
commit
dd96792e33
29 files changed
+1912
-256
No files matched your search
@@ -0,0 +1,365 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
class HomeGridCategoryShortcut {
|
||||
const HomeGridCategoryShortcut({
|
||||
required this.label,
|
||||
required this.categoryName,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String categoryName;
|
||||
}
|
||||
|
||||
class HomeGridTagOption {
|
||||
const HomeGridTagOption({required this.id, required this.label});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
}
|
||||
|
||||
class HomeGridCallbacks {
|
||||
const HomeGridCallbacks({
|
||||
required this.onSearch,
|
||||
required this.onOpenVip,
|
||||
required this.onOpenWallet,
|
||||
required this.onOpenNewReleases,
|
||||
required this.onOpenCategories,
|
||||
required this.onOpenRanking,
|
||||
required this.onOpenFilter,
|
||||
});
|
||||
|
||||
final VoidCallback onSearch;
|
||||
final VoidCallback onOpenVip;
|
||||
final VoidCallback onOpenWallet;
|
||||
final VoidCallback onOpenNewReleases;
|
||||
final VoidCallback onOpenCategories;
|
||||
final VoidCallback onOpenRanking;
|
||||
final VoidCallback onOpenFilter;
|
||||
}
|
||||
|
||||
class HomeGridHeader extends StatelessWidget {
|
||||
const HomeGridHeader({
|
||||
super.key,
|
||||
required this.callbacks,
|
||||
required this.categories,
|
||||
required this.tags,
|
||||
required this.selectedCategory,
|
||||
required this.selectedTag,
|
||||
required this.onCategorySelected,
|
||||
required this.onTagSelected,
|
||||
this.walletBadge,
|
||||
});
|
||||
|
||||
final HomeGridCallbacks callbacks;
|
||||
final List<HomeGridCategoryShortcut> categories;
|
||||
final List<HomeGridTagOption> tags;
|
||||
final String? selectedCategory;
|
||||
final String? selectedTag;
|
||||
final ValueChanged<String?> onCategorySelected;
|
||||
final ValueChanged<String?> onTagSelected;
|
||||
final int? walletBadge;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
key: const Key('home-grid-header'),
|
||||
children: [_searchRow(l10n), _primaryRow(l10n), _tagRow(l10n)],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _searchRow(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
key: const Key('home-search-entry'),
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
onTap: callbacks.onSearch,
|
||||
child: Container(
|
||||
height: AppDimens.searchHeight,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.spaceMd,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.search_rounded,
|
||||
size: AppDimens.homeHeaderIcon,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.homeSearchHint,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.body,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
_headerAction(
|
||||
key: const Key('home-vip-entry'),
|
||||
icon: Icons.workspace_premium_outlined,
|
||||
tooltip: l10n.vipMembership,
|
||||
onTap: callbacks.onOpenVip,
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceXs),
|
||||
_headerAction(
|
||||
key: const Key('home-wallet-entry'),
|
||||
icon: Icons.card_giftcard_rounded,
|
||||
tooltip: l10n.wallet,
|
||||
onTap: callbacks.onOpenWallet,
|
||||
badge: walletBadge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _headerAction({
|
||||
required Key key,
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
required VoidCallback onTap,
|
||||
int? badge,
|
||||
}) {
|
||||
return SizedBox.square(
|
||||
dimension: AppDimens.homeIconSlot,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: Material(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
child: InkWell(
|
||||
key: key,
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
onTap: onTap,
|
||||
child: Icon(
|
||||
icon,
|
||||
size: AppDimens.homeHeaderIcon,
|
||||
color: AppColors.navSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (badge != null && badge > 0)
|
||||
Positioned(
|
||||
key: const Key('home-wallet-badge'),
|
||||
top: -5,
|
||||
right: -6,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 20, minHeight: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.badgeHot,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
child: Text(
|
||||
formatInteractionCount(badge),
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.homeCardBadge,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _primaryRow(AppLocalizations l10n) {
|
||||
return SingleChildScrollView(
|
||||
key: const Key('home-primary-sections'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal),
|
||||
child: Row(
|
||||
children: [
|
||||
_primaryItem(
|
||||
key: const Key('home-section-featured'),
|
||||
label: l10n.homeSectionFeatured,
|
||||
selected: selectedCategory == null,
|
||||
onTap: () => onCategorySelected(null),
|
||||
),
|
||||
_primaryItem(
|
||||
key: const Key('home-section-new'),
|
||||
label: l10n.homeSectionNew,
|
||||
selected: false,
|
||||
onTap: callbacks.onOpenNewReleases,
|
||||
),
|
||||
_primaryItem(
|
||||
key: const Key('home-section-categories'),
|
||||
label: l10n.homeSectionCategories,
|
||||
selected: false,
|
||||
onTap: callbacks.onOpenCategories,
|
||||
),
|
||||
_primaryItem(
|
||||
key: const Key('home-section-ranking'),
|
||||
label: l10n.homeSectionRanking,
|
||||
selected: false,
|
||||
onTap: callbacks.onOpenRanking,
|
||||
),
|
||||
for (final category in categories)
|
||||
_primaryItem(
|
||||
key: Key('home-section-category-${category.categoryName}'),
|
||||
label: category.label,
|
||||
selected: selectedCategory == category.categoryName,
|
||||
onTap: () => onCategorySelected(category.categoryName),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _primaryItem({
|
||||
required Key key,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
key: key,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: AppDimens.spaceXl),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? AppColors.navSelected
|
||||
: AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeSectionFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Container(
|
||||
height: AppDimens.homeSectionIndicator,
|
||||
width: selected ? 20 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.navSelected,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tagRow(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
key: const Key('home-secondary-tags'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final tag in tags) ...[
|
||||
_tagChip(tag),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceXs),
|
||||
Tooltip(
|
||||
message: l10n.filter,
|
||||
child: SizedBox.square(
|
||||
dimension: AppDimens.tapTarget,
|
||||
child: Material(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
child: InkWell(
|
||||
key: const Key('home-grid-filter'),
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
onTap: callbacks.onOpenFilter,
|
||||
child: const Icon(
|
||||
Icons.filter_list_rounded,
|
||||
color: AppColors.navSelected,
|
||||
size: AppDimens.homeHeaderIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tagChip(HomeGridTagOption tag) {
|
||||
final selected = selectedTag == tag.id;
|
||||
return InkWell(
|
||||
key: Key('home-tag-${tag.id}'),
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
onTap: () => onTagSelected(selected ? null : tag.id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.spaceMd,
|
||||
vertical: AppDimens.spaceSm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.contentChip : AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
child: Text(
|
||||
tag.label,
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.navSelected : AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeSectionFont - 2,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user