fix:按html的假数据demo

This commit is contained in:
jingyun
2026-06-05 11:12:55 +08:00
parent b4272b5ec9
commit 9727b906c6
28 changed files with 2159 additions and 711 deletions
+31 -8
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../theme/wise_tokens.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class AppButton extends StatelessWidget {
const AppButton({
@@ -21,10 +22,26 @@ class AppButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = switch (kind) {
AppButtonKind.primary => (WiseColors.secondary, WiseColors.primary),
AppButtonKind.dark => (WiseColors.primary, Colors.white),
AppButtonKind.accent => (WiseColors.accent, Colors.white),
AppButtonKind.ghost => (WiseColors.surface, WiseColors.primary),
AppButtonKind.primary => (
YantingColors.primary,
YantingColors.primaryForeground,
Colors.transparent,
),
AppButtonKind.dark => (
YantingColors.foreground,
YantingColors.background,
Colors.transparent,
),
AppButtonKind.accent => (
YantingColors.brandSoft,
YantingColors.primaryForeground,
Colors.transparent,
),
AppButtonKind.ghost => (
YantingColors.background,
YantingColors.foreground,
YantingColors.border,
),
};
final child = FilledButton.icon(
onPressed: onPressed,
@@ -33,10 +50,16 @@ class AppButton extends StatelessWidget {
style: FilledButton.styleFrom(
backgroundColor: colors.$1,
foregroundColor: colors.$2,
disabledBackgroundColor: WiseColors.border,
disabledForegroundColor: WiseColors.textTertiary,
disabledBackgroundColor: YantingColors.border,
disabledForegroundColor: YantingColors.mutedForeground,
minimumSize: Size(expand ? double.infinity : 0, 44),
shape: const StadiumBorder(),
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),
),
),
);
return expand ? SizedBox(width: double.infinity, child: child) : child;
+25 -11
View File
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import '../theme/wise_tokens.dart';
import '../theme/yanting_tokens.dart';
class AppCard extends StatelessWidget {
const AppCard({
required this.child,
this.onTap,
this.padding = const EdgeInsets.all(WiseSpacing.x4),
this.color = WiseColors.surface,
this.padding = const EdgeInsets.all(YantingSpacing.cardPadding),
this.color = YantingColors.card,
this.borderColor = YantingColors.border,
super.key,
});
@@ -15,22 +16,34 @@ class AppCard extends StatelessWidget {
final VoidCallback? onTap;
final EdgeInsetsGeometry padding;
final Color color;
final Color borderColor;
@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,
borderRadius: BorderRadius.circular(WiseRadius.md),
boxShadow: WiseShadows.card,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(YantingRadius.xl),
),
child: Padding(padding: padding, child: child),
);
if (onTap == null) return content;
return InkWell(
borderRadius: BorderRadius.circular(WiseRadius.md),
onTap: onTap,
child: 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),
),
),
);
}
}
@@ -45,8 +58,9 @@ class HeroReportCard extends StatelessWidget {
Widget build(BuildContext context) {
return AppCard(
onTap: onTap,
color: WiseColors.secondary200,
padding: const EdgeInsets.all(WiseSpacing.x5),
color: YantingColors.brandSoft,
borderColor: YantingColors.brandSoftBorder,
padding: const EdgeInsets.all(YantingSpacing.cardPadding),
child: child,
);
}
+61 -18
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../theme/wise_tokens.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class AppBadge extends StatelessWidget {
const AppBadge({
@@ -17,19 +18,42 @@ class AppBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = switch (kind) {
BadgeKind.brand => (WiseColors.secondary200, WiseColors.primarySoft),
BadgeKind.audio => (const Color(0x1F00A2DD), WiseColors.accent),
BadgeKind.tier => (const Color(0x1A008026), WiseColors.positive),
BadgeKind.warning => (const Color(0x209A6500), WiseColors.warning),
BadgeKind.neutral => (const Color(0x1286A7BD), WiseColors.textSecondary),
BadgeKind.brand => (
YantingColors.primary,
YantingColors.primaryForeground,
Colors.transparent,
),
BadgeKind.audio => (
YantingColors.secondary,
YantingColors.secondaryForeground,
Colors.transparent,
),
BadgeKind.tier => (
YantingColors.background,
YantingColors.mutedForeground,
YantingColors.border,
),
BadgeKind.warning => (
YantingColors.background,
YantingColors.destructive,
YantingColors.border,
),
BadgeKind.neutral => (
YantingColors.secondary,
YantingColors.secondaryForeground,
Colors.transparent,
),
};
return DecoratedBox(
decoration: BoxDecoration(
color: colors.$1,
borderRadius: BorderRadius.circular(WiseRadius.pill),
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: 4),
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -39,7 +63,9 @@ class AppBadge extends StatelessWidget {
],
Text(
text,
style: Theme.of(context).textTheme.labelSmall?.copyWith(color: colors.$2),
style:
(Theme.of(context).textTheme.labelSmall ?? YantingText.badge)
.copyWith(color: colors.$2, letterSpacing: 0),
),
],
),
@@ -64,16 +90,33 @@ class AppChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ActionChip(
onPressed: onTap,
label: Text(label),
labelStyle: TextStyle(
color: selected ? Colors.white : WiseColors.textSecondary,
fontWeight: FontWeight.w700,
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),
),
),
),
),
backgroundColor: selected ? WiseColors.primary : WiseColors.surface,
side: const BorderSide(color: WiseColors.border),
shape: const StadiumBorder(),
);
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import '../theme/app_icons.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class BottomTabBarItem {
const BottomTabBarItem({
required this.label,
required this.icon,
required this.selectedIcon,
});
final String label;
final IconData icon;
final IconData selectedIcon;
}
class BottomTabBar extends StatelessWidget {
const BottomTabBar({
required this.items,
required this.selectedIndex,
required this.onSelected,
super.key,
});
final List<BottomTabBarItem> items;
final int selectedIndex;
final ValueChanged<int> onSelected;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
color: YantingColors.background,
border: Border(top: BorderSide(color: YantingColors.border)),
),
child: SizedBox(
height: YantingSpacing.tabBarHeight,
child: Row(
children: [
for (var index = 0; index < items.length; index++)
Expanded(
child: _BottomTabButton(
item: items[index],
selected: index == selectedIndex,
onTap: () => onSelected(index),
),
),
],
),
),
);
}
}
class _BottomTabButton extends StatelessWidget {
const _BottomTabButton({
required this.item,
required this.selected,
required this.onTap,
});
final BottomTabBarItem item;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final color = selected
? YantingColors.foreground
: YantingColors.mutedForeground;
return InkWell(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
size: 22,
color: color,
),
const SizedBox(height: 4),
Text(
item.label,
style: YantingText.meta.copyWith(
color: color,
fontSize: 11,
letterSpacing: 0,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
),
),
],
),
);
}
}
const yantingBottomTabItems = [
BottomTabBarItem(
label: '推荐',
icon: AppIcons.sparkle,
selectedIcon: AppIcons.sparkleFill,
),
BottomTabBarItem(
label: '研报',
icon: AppIcons.article,
selectedIcon: AppIcons.articleFill,
),
BottomTabBarItem(
label: '机构',
icon: AppIcons.bank,
selectedIcon: AppIcons.bankFill,
),
BottomTabBarItem(
label: '听单',
icon: AppIcons.headphones,
selectedIcon: AppIcons.headphonesFill,
),
BottomTabBarItem(
label: '我的',
icon: AppIcons.user,
selectedIcon: AppIcons.userFill,
),
];
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import '../data/models/models.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
import 'app_card.dart';
import 'badges.dart';
class InstitutionCard extends StatelessWidget {
const InstitutionCard({
required this.institution,
required this.onTap,
super.key,
});
final Institution institution;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final initials = institution.nameCn.isEmpty
? ''
: institution.nameCn.characters.take(2).toString();
return AppCard(
onTap: onTap,
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InstitutionLogo(
logoUrl: institution.logoUrl,
initials: initials,
size: 52,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
institution.nameCn,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: YantingText.listTitle.copyWith(
fontWeight: FontWeight.w700,
),
),
if (institution.nameEn.isNotEmpty) ...[
const SizedBox(height: 3),
Text(
institution.nameEn,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: YantingText.meta,
),
],
const SizedBox(height: 11),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (institution.institutionType.isNotEmpty)
AppBadge(
text: institution.institutionType,
kind: BadgeKind.tier,
),
for (final topic in institution.coveredTopics.take(3))
AppBadge(text: topic),
],
),
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${institution.reportCount}',
style: YantingText.sectionTitle.copyWith(
fontSize: 21,
fontFeatures: YantingTypographyFeatures.tabularNums,
),
),
Text('份研报', style: YantingText.meta.copyWith(fontSize: 11)),
],
),
],
),
);
}
}
class InstitutionLogo extends StatelessWidget {
const InstitutionLogo({
required this.logoUrl,
required this.initials,
required this.size,
super.key,
});
final String logoUrl;
final String initials;
final double size;
@override
Widget build(BuildContext context) {
final fallback = DecoratedBox(
decoration: BoxDecoration(
color: YantingColors.secondary,
border: Border.all(color: YantingColors.border),
borderRadius: BorderRadius.circular(size * 0.25),
),
child: Center(
child: Text(
initials,
style: YantingText.meta.copyWith(
color: YantingColors.secondaryForeground,
fontSize: 14,
fontWeight: FontWeight.w700,
fontFeatures: null,
),
),
),
);
if (logoUrl.isEmpty) {
return SizedBox(width: size, height: size, child: fallback);
}
return ClipRRect(
borderRadius: BorderRadius.circular(size * 0.25),
child: Image.network(
logoUrl,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => fallback,
),
);
}
}
+167 -60
View File
@@ -1,6 +1,9 @@
import 'package:flutter/material.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_card.dart';
@@ -47,11 +50,7 @@ class PlayerStateModel {
}
class MiniPlayer extends StatelessWidget {
const MiniPlayer({
required this.player,
required this.onToggle,
super.key,
});
const MiniPlayer({required this.player, required this.onToggle, super.key});
final PlayerStateModel player;
final VoidCallback onToggle;
@@ -59,54 +58,87 @@ class MiniPlayer extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!player.hasAudio) return const SizedBox.shrink();
final ratio = player.durationSec == 0 ? 0.0 : player.positionSec / player.durationSec;
return Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: AppCard(
padding: const EdgeInsets.all(12),
color: WiseColors.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
final ratio = player.durationSec == 0
? 0.0
: player.positionSec / player.durationSec;
return DecoratedBox(
decoration: const BoxDecoration(
color: YantingColors.secondary,
border: Border(top: BorderSide(color: YantingColors.border)),
),
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: ratio.clamp(0, 1),
child: const SizedBox(
height: 2,
child: ColoredBox(color: YantingColors.primary),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 10),
child: Row(
children: [
IconButton.filled(
onPressed: onToggle,
icon: Icon(player.playing ? Icons.pause : Icons.play_arrow),
style: IconButton.styleFrom(
backgroundColor: WiseColors.secondary,
foregroundColor: WiseColors.primary,
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: YantingColors.primary,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
AppIcons.disc,
color: YantingColors.primaryForeground,
size: 20,
),
),
const SizedBox(width: WiseSpacing.x2),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
player.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800),
style: YantingText.meta.copyWith(
color: YantingColors.foreground,
fontWeight: FontWeight.w600,
fontFeatures: null,
),
),
const SizedBox(height: 2),
Text(
'${formatDuration(player.positionSec)} / ${formatDuration(player.durationSec)} · ${player.speed.toStringAsFixed(1)}x',
style: const TextStyle(color: Color(0xCCFFFFFF), fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: YantingText.meta.copyWith(fontSize: 11),
),
],
),
),
IconButton(
onPressed: onToggle,
icon: Icon(
player.playing ? AppIcons.pause : AppIcons.playCircle,
size: player.playing ? 24 : 28,
),
color: YantingColors.foreground,
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: WiseSpacing.x2),
LinearProgressIndicator(
value: ratio.clamp(0, 1),
minHeight: 4,
backgroundColor: const Color(0x33FFFFFF),
color: WiseColors.secondary,
),
],
),
),
],
),
);
}
@@ -138,51 +170,126 @@ class PlayerCard extends StatelessWidget {
final position = active ? player.positionSec : 0;
final ratio = durationSec == 0 ? 0.0 : position / durationSec;
return AppCard(
color: WiseColors.secondary200,
color: YantingColors.secondary,
borderColor: YantingColors.border,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('音频解读', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: WiseSpacing.x2),
Text(title, style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: WiseSpacing.x3),
Text('音频解读', style: YantingText.listTitle),
const SizedBox(height: 6),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: YantingText.meta.copyWith(fontSize: 12.5),
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: ratio.clamp(0, 1),
minHeight: 6,
backgroundColor: Colors.white,
color: WiseColors.accent,
minHeight: 4,
borderRadius: BorderRadius.circular(YantingRadius.pill),
backgroundColor: YantingColors.border,
color: YantingColors.primary,
),
const SizedBox(height: WiseSpacing.x2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(formatDuration(position), style: Theme.of(context).textTheme.bodySmall),
Text(formatDuration(durationSec), style: Theme.of(context).textTheme.bodySmall),
],
),
const SizedBox(height: WiseSpacing.x3),
Row(
children: [
IconButton.outlined(onPressed: () => onSeek(-15), icon: const Icon(Icons.replay_10)),
IconButton.filled(
onPressed: active ? onToggle : onStart,
icon: Icon(active && player.playing ? Icons.pause : Icons.play_arrow),
style: IconButton.styleFrom(
backgroundColor: WiseColors.primary,
foregroundColor: Colors.white,
),
Text(
formatDuration(position),
style: YantingText.meta.copyWith(fontSize: 11),
),
Text(
formatDuration(durationSec),
style: YantingText.meta.copyWith(fontSize: 11),
),
IconButton.outlined(onPressed: () => onSeek(15), icon: const Icon(Icons.forward_10)),
const Spacer(),
TextButton(onPressed: onSpeed, child: Text('${player.speed.toStringAsFixed(1)}x')),
],
),
const SizedBox(height: 18),
SizedBox(
height: 56,
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_SkipButton(label: '-15', onPressed: () => onSeek(-15)),
const SizedBox(width: 26),
IconButton.filled(
onPressed: active ? onToggle : onStart,
icon: 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),
_SkipButton(label: '+15', onPressed: () => onSeek(15)),
],
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
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,
),
),
),
),
],
),
),
const SizedBox(height: 16),
Text(
'真实音频流待 /audio/{id}/stream 接入,当前为本地进度占位。',
style: Theme.of(context).textTheme.bodySmall,
style: YantingText.meta.copyWith(fontSize: 11.5, height: 1.6),
),
],
),
);
}
}
class _SkipButton extends StatelessWidget {
const _SkipButton({required this.label, required this.onPressed});
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
foregroundColor: YantingColors.foreground,
minimumSize: const Size(40, 40),
padding: EdgeInsets.zero,
),
child: Text(
label,
style: YantingText.meta.copyWith(
color: YantingColors.foreground,
fontWeight: FontWeight.w600,
),
),
);
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../theme/yanting_text.dart';
import '../theme/yanting_tokens.dart';
class PageHeader extends StatelessWidget {
const PageHeader({required this.title, this.subtitle, super.key});
final String title;
final String? subtitle;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4, bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: YantingText.appTitle),
if (subtitle != null && subtitle!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
subtitle!,
style: YantingText.sub.copyWith(
color: YantingColors.mutedForeground,
),
),
],
],
),
);
}
}
class SectionTitle extends StatelessWidget {
const SectionTitle({required this.title, this.icon, super.key});
final String title;
final IconData? icon;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: YantingSpacing.sectionGap,
bottom: 16,
),
child: Row(
children: [
Text(title, style: YantingText.sectionTitle),
if (icon != null) ...[
const SizedBox(width: 6),
Icon(icon, size: 18, color: YantingColors.mutedForeground),
],
],
),
);
}
}
+3
View File
@@ -1,6 +1,9 @@
export 'app_buttons.dart';
export 'app_card.dart';
export 'badges.dart';
export 'bottom_tab_bar.dart';
export 'institution_card.dart';
export 'mini_player.dart';
export 'page_header.dart';
export 'sheets.dart';
export 'states.dart';