fix:按照shadcn_ui对着demo_shadcn对齐

This commit is contained in:
jingyun
2026-06-05 15:04:39 +08:00
parent 9727b906c6
commit c5288f397d
29 changed files with 1425 additions and 642 deletions
+73 -37
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class AppButton extends StatelessWidget {
@@ -21,48 +21,84 @@ class AppButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = switch (kind) {
AppButtonKind.primary => (
YantingColors.primary,
YantingColors.primaryForeground,
Colors.transparent,
final leading = icon == null ? null : Icon(icon, size: 16);
final width = expand ? double.infinity : null;
final child = Text(label);
return switch (kind) {
AppButtonKind.primary => ShadButton(
width: width,
onPressed: onPressed,
leading: leading,
child: child,
),
AppButtonKind.dark => (
YantingColors.foreground,
YantingColors.background,
Colors.transparent,
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 => (
YantingColors.brandSoft,
YantingColors.primaryForeground,
Colors.transparent,
AppButtonKind.accent => ShadButton.secondary(
width: width,
onPressed: onPressed,
leading: leading,
backgroundColor: YantingColors.brandSoft,
foregroundColor: YantingColors.primaryForeground,
hoverBackgroundColor: YantingColors.brandSoftBorder,
child: child,
),
AppButtonKind.ghost => (
YantingColors.background,
YantingColors.foreground,
YantingColors.border,
AppButtonKind.ghost => ShadButton.outline(
width: width,
onPressed: onPressed,
leading: leading,
child: child,
),
};
final child = FilledButton.icon(
onPressed: onPressed,
icon: icon == null ? const SizedBox.shrink() : Icon(icon, size: 18),
label: Text(label),
style: FilledButton.styleFrom(
backgroundColor: colors.$1,
foregroundColor: colors.$2,
disabledBackgroundColor: YantingColors.border,
disabledForegroundColor: YantingColors.mutedForeground,
minimumSize: Size(expand ? double.infinity : 0, 44),
textStyle: YantingText.body.copyWith(fontWeight: FontWeight.w600),
side: colors.$3 == Colors.transparent
? BorderSide.none
: BorderSide(color: colors.$3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(YantingRadius.md),
),
}
}
class AppIconButton extends StatelessWidget {
const AppIconButton({
required this.icon,
required this.onPressed,
this.kind = AppButtonKind.ghost,
super.key,
});
final IconData icon;
final VoidCallback? onPressed;
final AppButtonKind kind;
@override
Widget build(BuildContext context) {
final iconWidget = Icon(icon, size: 16);
return switch (kind) {
AppButtonKind.primary => ShadIconButton(
onPressed: onPressed,
icon: iconWidget,
),
);
return expand ? SizedBox(width: double.infinity, child: child) : child;
AppButtonKind.dark => ShadIconButton(
onPressed: onPressed,
backgroundColor: YantingColors.foreground,
foregroundColor: YantingColors.background,
hoverBackgroundColor: YantingColors.foreground.withValues(alpha: 0.9),
icon: iconWidget,
),
AppButtonKind.accent => ShadIconButton.secondary(
onPressed: onPressed,
backgroundColor: YantingColors.brandSoft,
foregroundColor: YantingColors.primaryForeground,
hoverBackgroundColor: YantingColors.brandSoftBorder,
icon: iconWidget,
),
AppButtonKind.ghost => ShadIconButton.outline(
onPressed: onPressed,
icon: iconWidget,
),
};
}
}
+19 -18
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/yanting_tokens.dart';
@@ -20,29 +21,29 @@ class AppCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final decoration = BoxDecoration(
color: color,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(YantingRadius.xl),
);
final content = DecoratedBox(
decoration: BoxDecoration(
color: color,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(YantingRadius.xl),
),
child: Padding(padding: padding, child: child),
final theme = ShadTheme.of(context);
final radius = BorderRadius.circular(YantingRadius.xl);
final content = ShadCard(
padding: padding,
backgroundColor: color,
radius: radius,
border: ShadBorder.all(color: borderColor),
shadows: const [],
child: child,
);
if (onTap == null) return content;
return Material(
color: Colors.transparent,
child: Ink(
decoration: decoration,
child: InkWell(
borderRadius: BorderRadius.circular(YantingRadius.xl),
onTap: onTap,
child: Padding(padding: padding, child: child),
borderRadius: radius,
child: InkWell(
borderRadius: radius,
splashColor: theme.colorScheme.mutedForeground.withValues(alpha: 0.08),
highlightColor: theme.colorScheme.mutedForeground.withValues(
alpha: 0.04,
),
onTap: onTap,
child: content,
),
);
}
+49 -77
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class AppBadge extends StatelessWidget {
@@ -17,60 +17,45 @@ class AppBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = switch (kind) {
BadgeKind.brand => (
YantingColors.primary,
YantingColors.primaryForeground,
Colors.transparent,
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[Icon(icon, size: 12), const SizedBox(width: 4)],
Text(text),
],
);
final shape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(YantingRadius.sm),
side: kind == BadgeKind.tier || kind == BadgeKind.warning
? const BorderSide(color: YantingColors.border)
: BorderSide.none,
);
return switch (kind) {
BadgeKind.brand => ShadBadge(
shape: shape,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
child: child,
),
BadgeKind.audio => (
YantingColors.secondary,
YantingColors.secondaryForeground,
Colors.transparent,
BadgeKind.audio || BadgeKind.neutral => ShadBadge.secondary(
shape: shape,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
child: child,
),
BadgeKind.tier => (
YantingColors.background,
YantingColors.mutedForeground,
YantingColors.border,
BadgeKind.tier => ShadBadge.outline(
shape: shape,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
foregroundColor: YantingColors.mutedForeground,
child: child,
),
BadgeKind.warning => (
YantingColors.background,
YantingColors.destructive,
YantingColors.border,
),
BadgeKind.neutral => (
YantingColors.secondary,
YantingColors.secondaryForeground,
Colors.transparent,
BadgeKind.warning => ShadBadge.destructive(
shape: shape,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
backgroundColor: YantingColors.background,
foregroundColor: YantingColors.destructive,
child: child,
),
};
return DecoratedBox(
decoration: BoxDecoration(
color: colors.$1,
border: colors.$3 == Colors.transparent
? null
: Border.all(color: colors.$3),
borderRadius: BorderRadius.circular(YantingRadius.sm),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 14, color: colors.$2),
const SizedBox(width: 4),
],
Text(
text,
style:
(Theme.of(context).textTheme.labelSmall ?? YantingText.badge)
.copyWith(color: colors.$2, letterSpacing: 0),
),
],
),
),
);
}
}
@@ -90,33 +75,20 @@ class AppChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final background = selected
? YantingColors.foreground
: YantingColors.secondary;
final foreground = selected
? YantingColors.background
: YantingColors.secondaryForeground;
return Material(
color: Colors.transparent,
child: Ink(
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(YantingRadius.pill),
),
child: InkWell(
borderRadius: BorderRadius.circular(YantingRadius.pill),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 9),
child: Text(
label,
style:
(Theme.of(context).textTheme.labelLarge ?? YantingText.chip)
.copyWith(color: foreground, letterSpacing: 0),
),
),
),
),
return ShadBadge.secondary(
onPressed: onTap,
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 9),
backgroundColor: selected
? YantingColors.foreground
: YantingColors.secondary,
hoverBackgroundColor: selected
? YantingColors.foreground.withValues(alpha: 0.9)
: YantingColors.border,
foregroundColor: selected
? YantingColors.background
: YantingColors.secondaryForeground,
child: Text(label),
);
}
}
+14 -35
View File
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../data/models/models.dart';
import '../theme/app_icons.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
import '../theme/wise_tokens.dart';
import 'app_buttons.dart';
import 'app_card.dart';
class PlayerStateModel {
@@ -126,14 +128,12 @@ class MiniPlayer extends StatelessWidget {
],
),
),
IconButton(
ShadIconButton.ghost(
onPressed: onToggle,
icon: Icon(
player.playing ? AppIcons.pause : AppIcons.playCircle,
size: player.playing ? 24 : 28,
),
color: YantingColors.foreground,
visualDensity: VisualDensity.compact,
),
],
),
@@ -184,13 +184,7 @@ class PlayerCard extends StatelessWidget {
style: YantingText.meta.copyWith(fontSize: 12.5),
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: ratio.clamp(0, 1),
minHeight: 4,
borderRadius: BorderRadius.circular(YantingRadius.pill),
backgroundColor: YantingColors.border,
color: YantingColors.primary,
),
ShadProgress(value: ratio.clamp(0, 1)),
const SizedBox(height: WiseSpacing.x2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -216,18 +210,15 @@ class PlayerCard extends StatelessWidget {
children: [
_SkipButton(label: '-15', onPressed: () => onSeek(-15)),
const SizedBox(width: 26),
IconButton.filled(
onPressed: active ? onToggle : onStart,
icon: Icon(
active && player.playing
SizedBox(
width: 56,
height: 56,
child: AppIconButton(
kind: AppButtonKind.primary,
onPressed: active ? onToggle : onStart,
icon: active && player.playing
? AppIcons.pause
: AppIcons.play,
size: 28,
),
style: IconButton.styleFrom(
backgroundColor: YantingColors.primary,
foregroundColor: YantingColors.primaryForeground,
fixedSize: const Size(56, 56),
),
),
const SizedBox(width: 26),
@@ -236,22 +227,10 @@ class PlayerCard extends StatelessWidget {
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
child: ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: onSpeed,
style: TextButton.styleFrom(
backgroundColor: YantingColors.background,
foregroundColor: YantingColors.foreground,
side: const BorderSide(color: YantingColors.border),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: Text(
'${player.speed.toStringAsFixed(1)}x',
style: YantingText.meta.copyWith(
color: YantingColors.foreground,
fontWeight: FontWeight.w600,
),
),
child: Text('${player.speed.toStringAsFixed(1)}x'),
),
),
],
+25 -46
View File
@@ -1,27 +1,22 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/wise_tokens.dart';
import 'app_buttons.dart';
import 'states.dart';
Future<void> showLoginSheet(BuildContext context, {String reason = '登录后保存当前动作'}) {
return showModalBottomSheet<void>(
Future<void> showLoginSheet(
BuildContext context, {
String reason = '登录后保存当前动作',
}) {
return showShadSheet<void>(
context: context,
showDragHandle: true,
backgroundColor: WiseColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)),
),
builder: (context) => Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 28),
side: ShadSheetSide.bottom,
builder: (context) => ShadSheet(
title: const Text('登录研听'),
description: Text(reason),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('登录研听', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: WiseSpacing.x2),
Text(reason, style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: WiseSpacing.x4),
AppButton(
label: '使用手机号继续',
icon: Icons.phone_iphone,
@@ -31,7 +26,7 @@ Future<void> showLoginSheet(BuildContext context, {String reason = '登录后保
showAppToast(context, '登录接口待接入,已保留当前页面');
},
),
const SizedBox(height: WiseSpacing.x2),
const SizedBox(height: 8),
AppButton(
label: '微信 / Apple 登录占位',
icon: Icons.account_circle_outlined,
@@ -49,37 +44,21 @@ Future<void> showLoginSheet(BuildContext context, {String reason = '登录后保
}
Future<void> showOutboundSheet(BuildContext context, {required String title}) {
return showModalBottomSheet<void>(
return showShadSheet<void>(
context: context,
showDragHandle: true,
backgroundColor: WiseColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)),
),
builder: (context) => Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 28),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('即将打开外部服务', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: WiseSpacing.x2),
Text(
'$title\n外跳仅用于了解原文或相关服务,本内容不构成投资建议。',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: WiseSpacing.x4),
AppButton(
label: '确认并记录占位事件',
icon: Icons.open_in_new,
kind: AppButtonKind.accent,
expand: true,
onPressed: () {
Navigator.pop(context);
showAppToast(context, '外跳事件接口待接入');
},
),
],
side: ShadSheetSide.bottom,
builder: (context) => ShadSheet(
title: const Text('即将打开外部服务'),
description: Text('$title\n外跳仅用于了解原文或相关服务,本内容不构成投资建议。'),
child: AppButton(
label: '确认并记录占位事件',
icon: Icons.open_in_new,
kind: AppButtonKind.accent,
expand: true,
onPressed: () {
Navigator.pop(context);
showAppToast(context, '外跳事件接口待接入');
},
),
),
);
+70 -24
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../theme/wise_tokens.dart';
import '../theme/yanting_tokens.dart';
import 'app_buttons.dart';
import 'app_card.dart';
@@ -12,9 +13,10 @@ class LoadingState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(WiseSpacing.x4),
padding: const EdgeInsets.all(YantingSpacing.screenX),
itemCount: 4,
separatorBuilder: (_, _) => const SizedBox(height: WiseSpacing.x3),
separatorBuilder: (_, _) =>
const SizedBox(height: YantingSpacing.cardGap),
itemBuilder: (context, index) => const SkeletonCard(),
);
}
@@ -30,11 +32,11 @@ class SkeletonCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SkeletonLine(width: 96),
SizedBox(height: WiseSpacing.x3),
SizedBox(height: YantingSpacing.cardGap),
SkeletonLine(width: double.infinity, height: 18),
SizedBox(height: WiseSpacing.x2),
SizedBox(height: YantingSpacing.x2),
SkeletonLine(width: 240),
SizedBox(height: WiseSpacing.x3),
SizedBox(height: YantingSpacing.cardGap),
SkeletonLine(width: 160),
],
),
@@ -50,12 +52,58 @@ class SkeletonLine extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
final theme = ShadTheme.of(context);
return _PulsingSkeleton(
width: width,
height: height,
decoration: BoxDecoration(
color: WiseColors.border,
borderRadius: BorderRadius.circular(WiseRadius.pill),
color: theme.colorScheme.muted,
);
}
}
class _PulsingSkeleton extends StatefulWidget {
const _PulsingSkeleton({
required this.color,
required this.width,
required this.height,
});
final Color color;
final double width;
final double height;
@override
State<_PulsingSkeleton> createState() => _PulsingSkeletonState();
}
class _PulsingSkeletonState extends State<_PulsingSkeleton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return FadeTransition(
opacity: Tween<double>(
begin: 0.4,
end: 1,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)),
child: Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: widget.color,
borderRadius: theme.radius,
),
),
);
}
@@ -79,24 +127,29 @@ class EmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(WiseSpacing.x6),
padding: const EdgeInsets.all(YantingSpacing.x6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 42, color: WiseColors.primary),
const SizedBox(height: WiseSpacing.x3),
Icon(icon, size: 42, color: theme.colorScheme.foreground),
const SizedBox(height: YantingSpacing.cardGap),
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: WiseSpacing.x2),
const SizedBox(height: YantingSpacing.x2),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
if (actionLabel != null) ...[
const SizedBox(height: WiseSpacing.x4),
AppButton(label: actionLabel!, onPressed: onAction, kind: AppButtonKind.ghost),
const SizedBox(height: YantingSpacing.x4),
AppButton(
label: actionLabel!,
onPressed: onAction,
kind: AppButtonKind.ghost,
),
],
],
),
@@ -124,12 +177,5 @@ class ErrorState extends StatelessWidget {
}
void showAppToast(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
backgroundColor: WiseColors.primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(WiseRadius.md)),
),
);
ShadToaster.of(context).show(ShadToast(title: Text(message)));
}