fix:导航栏交互和UI
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
||||||
|
|
||||||
import '../../data/api/report_data_source.dart';
|
import '../../data/api/report_data_source.dart';
|
||||||
import '../../data/content_providers.dart';
|
import '../../data/content_providers.dart';
|
||||||
@@ -90,9 +89,7 @@ class FeedPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
const SizedBox(height: YantingSpacing.cardGap),
|
||||||
const ShadSeparator.horizontal(),
|
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
|
||||||
if (visible.isEmpty)
|
if (visible.isEmpty)
|
||||||
const EmptyState(
|
const EmptyState(
|
||||||
title: '暂无可推荐的研报解读',
|
title: '暂无可推荐的研报解读',
|
||||||
@@ -115,7 +112,7 @@ class FeedPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPlayTap: () => _playFromReport(onPlay, visible.first),
|
onPlayTap: () => _playFromReport(onPlay, visible.first),
|
||||||
),
|
),
|
||||||
const SizedBox(height: YantingSpacing.x6),
|
const SizedBox(height: YantingSpacing.sectionGap),
|
||||||
const SectionTitle(title: '最新解读', icon: Icons.chevron_right),
|
const SectionTitle(title: '最新解读', icon: Icons.chevron_right),
|
||||||
for (final report in visible.skip(1)) ...[
|
for (final report in visible.skip(1)) ...[
|
||||||
ReportCardWidget(
|
ReportCardWidget(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
||||||
|
|
||||||
import '../../data/api/report_data_source.dart';
|
import '../../data/api/report_data_source.dart';
|
||||||
import '../../data/content_providers.dart';
|
import '../../data/content_providers.dart';
|
||||||
@@ -43,9 +42,7 @@ class InstitutionsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
const PageHeader(title: '机构', subtitle: '可获取研报的机构'),
|
const PageHeader(title: '机构', subtitle: '可获取研报的机构'),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
const SizedBox(height: YantingSpacing.cardGap),
|
||||||
const ShadSeparator.horizontal(),
|
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
|
||||||
for (final item in sorted) ...[
|
for (final item in sorted) ...[
|
||||||
InstitutionCard(
|
InstitutionCard(
|
||||||
institution: item,
|
institution: item,
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ class ListenPage extends HookConsumerWidget {
|
|||||||
onPlay: () => onPlay(current),
|
onPlay: () => onPlay(current),
|
||||||
),
|
),
|
||||||
const SectionTitle(title: '全部音频', icon: AppIcons.arrowRight),
|
const SectionTitle(title: '全部音频', icon: AppIcons.arrowRight),
|
||||||
const ShadSeparator.horizontal(),
|
const SizedBox(height: YantingSpacing.cardGap),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
|
||||||
for (final item in items.skip(1)) ...[
|
for (final item in items.skip(1)) ...[
|
||||||
_AudioListCard(item: item, onPlay: () => onPlay(item)),
|
_AudioListCard(item: item, onPlay: () => onPlay(item)),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
const SizedBox(height: YantingSpacing.x3),
|
||||||
@@ -115,12 +114,7 @@ class _ContinueListeningCard extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ShadButton(
|
_PlayControlButton(onPressed: onPlay, size: 54, iconSize: 22),
|
||||||
onPressed: onPlay,
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
child: const Icon(AppIcons.play, size: 18),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 13),
|
const SizedBox(width: 13),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -197,14 +191,41 @@ class _AudioListCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
ShadButton(
|
_PlayControlButton(onPressed: onPlay, size: 44, iconSize: 18),
|
||||||
onPressed: onPlay,
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
child: const Icon(AppIcons.play, size: 16),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PlayControlButton extends StatelessWidget {
|
||||||
|
const _PlayControlButton({
|
||||||
|
required this.onPressed,
|
||||||
|
required this.size,
|
||||||
|
required this.iconSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final double size;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: YantingColors.primary,
|
||||||
|
borderRadius: BorderRadius.circular(YantingRadius.pill),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(YantingRadius.pill),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: size,
|
||||||
|
child: Icon(
|
||||||
|
AppIcons.play,
|
||||||
|
color: YantingColors.primaryForeground,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,19 @@ class _MenuGroup extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppCard(
|
return AppCard(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
child: Column(children: children),
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (var index = 0; index < children.length; index++) ...[
|
||||||
|
children[index],
|
||||||
|
if (index != children.length - 1)
|
||||||
|
const Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: YantingColors.border,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,8 +99,12 @@ class ReportsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onChanged: (value) => query.value = value.trim(),
|
onChanged: (value) => query.value = value.trim(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
const SizedBox(height: YantingSpacing.cardGap),
|
||||||
Wrap(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Wrap(
|
||||||
spacing: YantingSpacing.x2,
|
spacing: YantingSpacing.x2,
|
||||||
runSpacing: YantingSpacing.x2,
|
runSpacing: YantingSpacing.x2,
|
||||||
children: [
|
children: [
|
||||||
@@ -120,7 +124,10 @@ class ReportsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
ShadButton.outline(
|
ShadButton.outline(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
leading: const Icon(LucideIcons.arrowUpDown, size: 16),
|
leading: const Icon(
|
||||||
|
LucideIcons.arrowUpDown,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
child: const Text('最新'),
|
child: const Text('最新'),
|
||||||
),
|
),
|
||||||
ShadBadge.secondary(
|
ShadBadge.secondary(
|
||||||
@@ -132,20 +139,26 @@ class ReportsPage extends HookConsumerWidget {
|
|||||||
? theme.colorScheme.background
|
? theme.colorScheme.background
|
||||||
: theme.colorScheme.secondaryForeground,
|
: theme.colorScheme.secondaryForeground,
|
||||||
hoverBackgroundColor: currentHasAudio
|
hoverBackgroundColor: currentHasAudio
|
||||||
? theme.colorScheme.foreground.withValues(alpha: 0.9)
|
? theme.colorScheme.foreground.withValues(
|
||||||
|
alpha: 0.9,
|
||||||
|
)
|
||||||
: theme.colorScheme.border,
|
: theme.colorScheme.border,
|
||||||
child: const Text('音频'),
|
child: const Text('音频'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text('共 ${filtered.length} 篇', style: YantingText.meta),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
const SizedBox(width: YantingSpacing.x2),
|
||||||
const ShadSeparator.horizontal(),
|
Padding(
|
||||||
const SizedBox(height: YantingSpacing.x3),
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: Text(
|
||||||
|
'共 ${filtered.length} 篇',
|
||||||
|
style: YantingText.meta,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: YantingSpacing.cardGap),
|
||||||
if (filtered.isEmpty)
|
if (filtered.isEmpty)
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报',
|
title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报',
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ class ReportCardWidget extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: WiseSpacing.x2,
|
spacing: hero ? WiseSpacing.x2 : 7,
|
||||||
runSpacing: WiseSpacing.x2,
|
runSpacing: hero ? WiseSpacing.x2 : 7,
|
||||||
children: [
|
children: [
|
||||||
AppBadge(text: report.interpretationLabel, kind: BadgeKind.brand),
|
AppBadge(text: report.interpretationLabel, kind: BadgeKind.brand),
|
||||||
if (report.hasAudio)
|
if (report.hasAudio)
|
||||||
@@ -46,27 +46,32 @@ class ReportCardWidget extends StatelessWidget {
|
|||||||
for (final topic in report.topics.take(3)) AppBadge(text: topic),
|
for (final topic in report.topics.take(3)) AppBadge(text: topic),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: WiseSpacing.x3),
|
SizedBox(height: hero ? WiseSpacing.x3 : 10),
|
||||||
Text(
|
Text(
|
||||||
report.titleCn,
|
report.titleCn,
|
||||||
maxLines: hero ? 3 : 2,
|
maxLines: hero ? 3 : 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: hero
|
style: hero
|
||||||
? YantingText.sectionTitle.copyWith(fontSize: 21, height: 1.4)
|
? YantingText.sectionTitle.copyWith(fontSize: 21, height: 1.4)
|
||||||
: YantingText.cardTitle,
|
: YantingText.listTitle.copyWith(
|
||||||
|
fontSize: 17.5,
|
||||||
|
height: 1.38,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (report.oneLiner.isNotEmpty) ...[
|
if (report.oneLiner.isNotEmpty) ...[
|
||||||
const SizedBox(height: WiseSpacing.x2),
|
SizedBox(height: hero ? WiseSpacing.x2 : 7),
|
||||||
Text(
|
Text(
|
||||||
report.oneLiner,
|
report.oneLiner,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: YantingText.body.copyWith(
|
style: YantingText.body.copyWith(
|
||||||
color: YantingColors.mutedForeground,
|
color: YantingColors.mutedForeground,
|
||||||
|
fontSize: hero ? null : 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: WiseSpacing.x3),
|
SizedBox(height: hero ? WiseSpacing.x3 : 10),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -96,6 +101,7 @@ class ReportCardWidget extends StatelessWidget {
|
|||||||
label: '听研报',
|
label: '听研报',
|
||||||
icon: AppIcons.play,
|
icon: AppIcons.play,
|
||||||
kind: hero ? AppButtonKind.primary : AppButtonKind.accent,
|
kind: hero ? AppButtonKind.primary : AppButtonKind.accent,
|
||||||
|
compact: !hero,
|
||||||
onPressed: onPlayTap,
|
onPressed: onPlayTap,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -103,7 +109,11 @@ class ReportCardWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
return hero
|
return hero
|
||||||
? HeroReportCard(onTap: onTap, child: child)
|
? HeroReportCard(onTap: onTap, child: child)
|
||||||
: AppCard(onTap: onTap, child: child);
|
: AppCard(
|
||||||
|
onTap: onTap,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,40 @@ import '../theme/yanting_text.dart';
|
|||||||
import '../widgets/bottom_tab_bar.dart';
|
import '../widgets/bottom_tab_bar.dart';
|
||||||
import '../widgets/mini_player.dart';
|
import '../widgets/mini_player.dart';
|
||||||
|
|
||||||
class ShellPage extends ConsumerWidget {
|
class ShellPage extends ConsumerStatefulWidget {
|
||||||
const ShellPage({required this.child, required this.currentPath, super.key});
|
const ShellPage({required this.child, required this.currentPath, super.key});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final String currentPath;
|
final String currentPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<ShellPage> createState() => _ShellPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShellPageState extends ConsumerState<ShellPage> {
|
||||||
|
static const double _compactHeaderThreshold = 34;
|
||||||
|
|
||||||
|
bool _showCompactHeader = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ShellPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.currentPath != widget.currentPath && _showCompactHeader) {
|
||||||
|
_showCompactHeader = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final theme = ShadTheme.of(context);
|
final theme = ShadTheme.of(context);
|
||||||
final player = ref.watch(audioPlayerControllerProvider);
|
final player = ref.watch(audioPlayerControllerProvider);
|
||||||
final controller = ref.read(audioPlayerControllerProvider.notifier);
|
final controller = ref.read(audioPlayerControllerProvider.notifier);
|
||||||
final canPop = GoRouter.of(context).canPop();
|
final canPop = GoRouter.of(context).canPop();
|
||||||
final selectedIndex = _tabs.indexWhere((tab) => tab.path == currentPath);
|
final selectedIndex = _tabs.indexWhere(
|
||||||
|
(tab) => tab.path == widget.currentPath,
|
||||||
|
);
|
||||||
final safeIndex = selectedIndex < 0 ? 0 : selectedIndex;
|
final safeIndex = selectedIndex < 0 ? 0 : selectedIndex;
|
||||||
|
final header = _headerForPath(widget.currentPath);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.colorScheme.background,
|
backgroundColor: theme.colorScheme.background,
|
||||||
@@ -30,30 +50,45 @@ class ShellPage extends ConsumerWidget {
|
|||||||
backgroundColor: theme.colorScheme.background,
|
backgroundColor: theme.colorScheme.background,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
leading: canPop
|
leading: canPop
|
||||||
? ShadIconButton.ghost(
|
? ShadIconButton.ghost(
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(LucideIcons.chevronLeft, size: 18),
|
icon: const Icon(LucideIcons.chevronLeft, size: 18),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
title: Column(
|
title: AnimatedOpacity(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
opacity: _showCompactHeader ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 160),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('研听', style: YantingText.listTitle),
|
Text(
|
||||||
Text('全球机构研报中文解读', style: YantingText.meta.copyWith(fontSize: 12)),
|
header.title,
|
||||||
],
|
textAlign: TextAlign.center,
|
||||||
|
style: YantingText.listTitle,
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
if (header.subtitle.isNotEmpty)
|
||||||
preferredSize: const Size.fromHeight(1),
|
Text(
|
||||||
child: ColoredBox(
|
header.subtitle,
|
||||||
color: theme.colorScheme.border,
|
maxLines: 1,
|
||||||
child: const SizedBox(height: 1, width: double.infinity),
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: YantingText.meta.copyWith(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ColoredBox(
|
body: ColoredBox(
|
||||||
color: theme.colorScheme.background,
|
color: theme.colorScheme.background,
|
||||||
child: Stack(children: [Positioned.fill(child: child)]),
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: _handleScrollNotification,
|
||||||
|
child: Stack(children: [Positioned.fill(child: widget.child)]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
@@ -71,6 +106,34 @@ class ShellPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _handleScrollNotification(ScrollNotification notification) {
|
||||||
|
if (notification.metrics.axis != Axis.vertical) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final next = notification.metrics.pixels > _compactHeaderThreshold;
|
||||||
|
if (next != _showCompactHeader && mounted) {
|
||||||
|
setState(() => _showCompactHeader = next);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ShellHeader _headerForPath(String path) {
|
||||||
|
return switch (path) {
|
||||||
|
AppRoutes.reports => const _ShellHeader('研报', '全部已发布研报解读'),
|
||||||
|
AppRoutes.institutions => const _ShellHeader('机构', '可获取研报的机构'),
|
||||||
|
AppRoutes.listen => const _ShellHeader('听单', '已转音频的研报解读'),
|
||||||
|
AppRoutes.profile => const _ShellHeader('我的', ''),
|
||||||
|
_ => const _ShellHeader('研听', '全球机构研报中文解读'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShellHeader {
|
||||||
|
const _ShellHeader(this.title, this.subtitle);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TabItem {
|
class _TabItem {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import '../theme/yanting_text.dart';
|
||||||
import '../theme/yanting_tokens.dart';
|
import '../theme/yanting_tokens.dart';
|
||||||
|
|
||||||
class AppButton extends StatelessWidget {
|
class AppButton extends StatelessWidget {
|
||||||
@@ -10,6 +11,7 @@ class AppButton extends StatelessWidget {
|
|||||||
this.icon,
|
this.icon,
|
||||||
this.kind = AppButtonKind.primary,
|
this.kind = AppButtonKind.primary,
|
||||||
this.expand = false,
|
this.expand = false,
|
||||||
|
this.compact = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,45 +20,49 @@ class AppButton extends StatelessWidget {
|
|||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
final AppButtonKind kind;
|
final AppButtonKind kind;
|
||||||
final bool expand;
|
final bool expand;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final leading = icon == null ? null : Icon(icon, size: 16);
|
final variant = switch (kind) {
|
||||||
final width = expand ? double.infinity : null;
|
AppButtonKind.primary => ShadButtonVariant.primary,
|
||||||
final child = Text(label);
|
AppButtonKind.dark => ShadButtonVariant.primary,
|
||||||
|
AppButtonKind.accent => ShadButtonVariant.secondary,
|
||||||
return switch (kind) {
|
AppButtonKind.ghost => ShadButtonVariant.outline,
|
||||||
AppButtonKind.primary => ShadButton(
|
|
||||||
width: width,
|
|
||||||
onPressed: onPressed,
|
|
||||||
leading: leading,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
AppButtonKind.dark => ShadButton(
|
|
||||||
width: width,
|
|
||||||
onPressed: onPressed,
|
|
||||||
leading: leading,
|
|
||||||
backgroundColor: YantingColors.foreground,
|
|
||||||
foregroundColor: YantingColors.background,
|
|
||||||
hoverBackgroundColor: YantingColors.foreground.withValues(alpha: 0.9),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
AppButtonKind.accent => ShadButton.secondary(
|
|
||||||
width: width,
|
|
||||||
onPressed: onPressed,
|
|
||||||
leading: leading,
|
|
||||||
backgroundColor: YantingColors.brandSoft,
|
|
||||||
foregroundColor: YantingColors.primaryForeground,
|
|
||||||
hoverBackgroundColor: YantingColors.brandSoftBorder,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
AppButtonKind.ghost => ShadButton.outline(
|
|
||||||
width: width,
|
|
||||||
onPressed: onPressed,
|
|
||||||
leading: leading,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
final colors = switch (kind) {
|
||||||
|
AppButtonKind.primary => (null, null, null),
|
||||||
|
AppButtonKind.dark => (
|
||||||
|
YantingColors.foreground,
|
||||||
|
YantingColors.background,
|
||||||
|
YantingColors.foreground.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
AppButtonKind.accent => (
|
||||||
|
YantingColors.brandSoft,
|
||||||
|
YantingColors.primaryForeground,
|
||||||
|
YantingColors.brandSoftBorder,
|
||||||
|
),
|
||||||
|
AppButtonKind.ghost => (null, null, null),
|
||||||
|
};
|
||||||
|
final button = ShadButton.raw(
|
||||||
|
variant: variant,
|
||||||
|
enabled: onPressed != null,
|
||||||
|
onPressed: onPressed,
|
||||||
|
width: expand ? double.infinity : null,
|
||||||
|
height: compact ? 36 : 44,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: compact ? 16 : 20),
|
||||||
|
backgroundColor: colors.$1,
|
||||||
|
foregroundColor: colors.$2,
|
||||||
|
hoverBackgroundColor: colors.$3,
|
||||||
|
leading: icon == null ? null : Icon(icon, size: compact ? 15 : 16),
|
||||||
|
gap: compact ? 5 : 7,
|
||||||
|
textStyle: (compact ? YantingText.badge : YantingText.body).copyWith(
|
||||||
|
color: colors.$2,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
child: Text(label),
|
||||||
|
);
|
||||||
|
return expand ? SizedBox(width: double.infinity, child: button) : button;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-15
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
||||||
|
|
||||||
import '../theme/yanting_tokens.dart';
|
import '../theme/yanting_tokens.dart';
|
||||||
|
|
||||||
@@ -21,29 +20,31 @@ class AppCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = ShadTheme.of(context);
|
|
||||||
final radius = BorderRadius.circular(YantingRadius.xl);
|
final radius = BorderRadius.circular(YantingRadius.xl);
|
||||||
final content = ShadCard(
|
final decoration = BoxDecoration(
|
||||||
padding: padding,
|
color: color,
|
||||||
backgroundColor: color,
|
borderRadius: radius,
|
||||||
radius: radius,
|
border: Border.all(color: borderColor),
|
||||||
border: ShadBorder.all(color: borderColor),
|
|
||||||
shadows: const [],
|
|
||||||
child: child,
|
|
||||||
);
|
);
|
||||||
if (onTap == null) return content;
|
if (onTap == null) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: decoration,
|
||||||
|
child: Padding(padding: padding, child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
|
child: Ink(
|
||||||
|
decoration: decoration,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
splashColor: theme.colorScheme.mutedForeground.withValues(alpha: 0.08),
|
splashColor: YantingColors.mutedForeground.withValues(alpha: 0.08),
|
||||||
highlightColor: theme.colorScheme.mutedForeground.withValues(
|
highlightColor: YantingColors.mutedForeground.withValues(alpha: 0.04),
|
||||||
alpha: 0.04,
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: content,
|
child: Padding(padding: padding, child: child),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ class InstitutionCard extends StatelessWidget {
|
|||||||
: institution.nameCn.characters.take(2).toString();
|
: institution.nameCn.characters.take(2).toString();
|
||||||
return AppCard(
|
return AppCard(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
InstitutionLogo(
|
InstitutionLogo(
|
||||||
logoUrl: institution.logoUrl,
|
logoUrl: institution.logoUrl,
|
||||||
initials: initials,
|
initials: initials,
|
||||||
size: 52,
|
size: 48,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -54,10 +54,10 @@ class InstitutionCard extends StatelessWidget {
|
|||||||
style: YantingText.meta,
|
style: YantingText.meta,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 11),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 7,
|
||||||
runSpacing: 8,
|
runSpacing: 7,
|
||||||
children: [
|
children: [
|
||||||
if (institution.institutionType.isNotEmpty)
|
if (institution.institutionType.isNotEmpty)
|
||||||
AppBadge(
|
AppBadge(
|
||||||
@@ -71,14 +71,14 @@ class InstitutionCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${institution.reportCount}',
|
'${institution.reportCount}',
|
||||||
style: YantingText.sectionTitle.copyWith(
|
style: YantingText.sectionTitle.copyWith(
|
||||||
fontSize: 21,
|
fontSize: 20,
|
||||||
fontFeatures: YantingTypographyFeatures.tabularNums,
|
fontFeatures: YantingTypographyFeatures.tabularNums,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user