Files
xianyan/lib/features/widget/presentation/widget_management_page.dart
2026-06-11 18:51:13 +08:00

1043 lines
33 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 桌面小部件管理页面
/// 创建时间: 2026-05-19
/// 更新时间: 2026-06-04
/// 作用: 管理桌面小部件的安装、数据推送、主题和平台兼容说明
/// 上次更新: 修复_WidgetDataPreview生命周期问题(framework断言错误)增加disposed保护和超时机制
/// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/storage/kv_storage.dart';
import '../../../core/router/app_nav_extension.dart';
import '../../../core/router/app_routes.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/services/data/home_widget_service.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/platform/platform_helper.dart';
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../../../shared/widgets/adaptive/responsive_layout.dart';
import '../../../shared/widgets/feedback/app_toast.dart';
import '../models/widget_type.dart';
import '../providers/widget_provider.dart' as wp;
class WidgetManagementPage extends ConsumerStatefulWidget {
const WidgetManagementPage({super.key});
@override
ConsumerState<WidgetManagementPage> createState() =>
_WidgetManagementPageState();
}
class _WidgetManagementPageState extends ConsumerState<WidgetManagementPage> {
bool _isAdding = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
ref.read(wp.widgetProvider.notifier).loadInstalledWidgets();
_showDevDialogIfNeeded();
});
}
/// 开发中弹窗提示,支持"不再提醒"
void _showDevDialogIfNeeded() {
final dismissed = KvStorage.getBool('widget_dev_dismissed') ?? false;
if (dismissed) return;
showCupertinoDialog<void>(
context: context,
builder: (ctx) {
bool dontRemind = false;
return StatefulBuilder(
builder: (ctx, setDialogState) => CupertinoAlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.hammer,
size: 20,
color: CupertinoColors.systemOrange.resolveFrom(ctx),
),
const SizedBox(width: 8),
const Text('Beta'),
],
),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'桌面小部件功能正在积极开发中,部分功能可能尚未完善或存在不稳定情况。\n\n'
'当前支持:基础小部件显示与数据推送\n'
'即将支持:更多小部件样式、交互操作、跨平台同步',
),
const SizedBox(height: 12),
GestureDetector(
onTap: () => setDialogState(() => dontRemind = !dontRemind),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
dontRemind
? CupertinoIcons.checkmark_square_fill
: CupertinoIcons.square,
size: 20,
color: dontRemind
? CupertinoColors.systemGreen.resolveFrom(ctx)
: CupertinoColors.systemGrey.resolveFrom(ctx),
),
const SizedBox(width: 8),
const Text('不再提醒', style: TextStyle(fontSize: 14)),
],
),
),
],
),
),
actions: [
CupertinoDialogAction(
child: const Text('查看实验功能'),
onPressed: () {
Navigator.pop(ctx);
// 鸿蒙端暂不支持弹出toast提示
if (PlatformHelper.isHarmonyOS) {
AppToast.showInfo('敬请期待');
return;
}
context.appPush(AppRoutes.experimentalFeatures);
},
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
if (dontRemind) {
KvStorage.setBool('widget_dev_dismissed', true);
}
Navigator.pop(ctx);
},
child: const Text('我知道了'),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final wp.WidgetState widgetState = ref.watch(wp.widgetProvider);
final grouped = <int, List<WidgetType>>{};
for (final t in WidgetTypeX.all) {
grouped.putIfAbsent(t.priority, () => []).add(t);
}
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
child: ResponsiveMaxWidth(
maxWidth: 900,
child: SafeArea(
bottom: false,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
CupertinoSliverNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
'桌面小部件',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
largeTitle: Text(
'桌面小部件',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
trailing: _ThemeToggle(ext: ext),
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
border: null,
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: _PlatformCompatibilityCard(ext: ext),
),
),
for (final entry in grouped.entries) ...[
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xs,
),
child: Row(
children: [
_PriorityDot(priority: entry.key),
const SizedBox(width: 6),
Text(
_priorityLabel(entry.key),
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
),
),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final type = entry.value[index];
final isInstalled = widgetState.installedWidgets.contains(
type,
);
return Padding(
key: ValueKey(type),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
child: _WidgetCard(
ext: ext,
type: type,
isInstalled: isInstalled,
isAdding: _isAdding,
onAdd: () => _handleAddWidget(type),
onPin: () => _handlePinWidget(type),
),
);
}, childCount: entry.value.length),
),
],
if (widgetState.error != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Text(
'错误: ${widgetState.error}',
style: AppTypography.footnote.copyWith(
color: CupertinoColors.systemRed,
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 120)),
],
),
),
),
);
}
String _priorityLabel(int p) => switch (p) {
0 => '核心小部件',
1 => '推荐小部件',
2 => '实用小部件',
_ => '趣味小部件',
};
void _handleAddWidget(WidgetType type) async {
// 鸿蒙端点击添加弹出toast提示
if (PlatformHelper.isHarmonyOS) {
AppToast.showInfo('敬请期待');
return;
}
if (_isAdding) return;
_isAdding = true;
try {
final notifier = ref.read(wp.widgetProvider.notifier);
final wasInstalled = ref
.read(wp.widgetProvider)
.installedWidgets
.contains(type);
final result = await notifier.requestPinWidget(type);
if (!mounted) return;
if (result == wp.PinWidgetResult.unsupported) {
_showManualAddGuide(type, isUnsupported: true);
return;
}
if (result == wp.PinWidgetResult.failed) {
_showManualAddGuide(type, isUnsupported: false);
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
if (!mounted) return;
await notifier.loadInstalledWidgets();
final nowInstalled = ref
.read(wp.widgetProvider)
.installedWidgets
.contains(type);
if (nowInstalled && !wasInstalled) {
AppToast.showInfo('${type.title} 已添加到桌面');
} else if (!nowInstalled) {
_showManualAddGuide(type, isUnsupported: false);
}
} finally {
_isAdding = false;
}
}
void _handlePinWidget(WidgetType type) async {
if (_isAdding) return;
_isAdding = true;
try {
final notifier = ref.read(wp.widgetProvider.notifier);
final result = await notifier.requestPinWidget(type);
if (!mounted) return;
if (result == wp.PinWidgetResult.unsupported) {
_showManualAddGuide(type, isUnsupported: true);
return;
}
if (result == wp.PinWidgetResult.failed) {
_showManualAddGuide(type, isUnsupported: false);
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
if (!mounted) return;
await notifier.loadInstalledWidgets();
final nowInstalled = ref
.read(wp.widgetProvider)
.installedWidgets
.contains(type);
if (!nowInstalled) {
_showManualAddGuide(type, isUnsupported: false);
}
} finally {
_isAdding = false;
}
}
void _showManualAddGuide(WidgetType type, {required bool isUnsupported}) {
final ext = AppTheme.ext(context);
final steps = PlatformHelper.isHarmonyOS
? [
'1⃣ 长按桌面空白处',
'2⃣ 选择「服务卡片」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」并添加到桌面',
]
: PlatformHelper.isAndroid
? [
'1⃣ 长按桌面空白处',
'2⃣ 选择「小部件」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」拖动到桌面',
]
: [
'1⃣ 向右滑动到今日视图',
'2⃣ 滚动到底部点击「编辑」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」并添加',
];
final guideReason = isUnsupported
? '当前设备不支持快捷添加,请按以下步骤手动添加到桌面:'
: '快捷添加失败,请按以下步骤手动添加到桌面:';
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.square_grid_2x2_fill,
size: 20,
color: ext.accent,
),
const SizedBox(width: AppSpacing.xs),
Text('添加「${type.title}'),
],
),
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
guideReason,
style: AppTypography.footnote.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.md),
...steps.map(
(s) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
s,
style: AppTypography.subhead.copyWith(color: ext.textPrimary),
),
),
),
],
),
actions: [
CupertinoDialogAction(
child: const Text('推送数据'),
onPressed: () {
Navigator.of(ctx).pop();
ref.read(wp.widgetProvider.notifier).pushDataToWidget(type);
AppToast.showInfo('已推送数据到${type.title}小部件');
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('知道了'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
}
class _ThemeToggle extends ConsumerWidget {
const _ThemeToggle({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () {
ref.read(wp.widgetProvider.notifier).pushThemeToAllWidgets();
AppToast.showInfo('已推送当前主题到小部件');
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.pillBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.paintbrush_fill, size: 14, color: ext.accent),
const SizedBox(width: 4),
Text(
'同步主题',
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
class _PriorityDot extends StatelessWidget {
const _PriorityDot({required this.priority});
final int priority;
@override
Widget build(BuildContext context) {
final color = switch (priority) {
0 => CupertinoColors.systemRed,
1 => CupertinoColors.systemOrange,
2 => CupertinoColors.systemBlue,
_ => CupertinoColors.systemGrey,
};
return Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
}
class _PlatformCompatibilityCard extends StatefulWidget {
const _PlatformCompatibilityCard({required this.ext});
final AppThemeExtension ext;
@override
State<_PlatformCompatibilityCard> createState() =>
_PlatformCompatibilityCardState();
}
class _PlatformCompatibilityCardState
extends State<_PlatformCompatibilityCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final ext = widget.ext;
return GlassContainer(
depth: GlassDepth.elevated,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
behavior: HitTestBehavior.opaque,
child: Row(
children: [
Icon(
CupertinoIcons.info_circle_fill,
size: 18,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
'平台兼容说明',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
AnimatedRotation(
turns: _isExpanded ? 0.0 : -0.25,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: Icon(
CupertinoIcons.chevron_down,
size: 16,
color: ext.textHint,
),
),
],
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(key: ValueKey('collapsed')),
secondChild: Padding(
key: const ValueKey('expanded'),
padding: const EdgeInsets.only(top: AppSpacing.sm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PlatformRow(
ext: ext,
icon: '🤖',
name: 'Android',
desc: '功能不完整,原生侧存在通信问题',
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🍎',
name: 'iOS',
desc: 'WidgetKit + SwiftUI交互需 iOS 17+',
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🔴',
name: '鸿蒙',
desc: 'FormExtension + ArkUI能力受限系统限制刷新频率',
),
const SizedBox(height: AppSpacing.sm),
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.06),
borderRadius: AppRadius.smBorder,
),
child: Row(
children: [
Icon(
CupertinoIcons.lightbulb_fill,
size: 14,
color: ext.accent,
),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件',
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
),
],
),
),
],
),
),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 250),
sizeCurve: Curves.easeInOut,
),
],
),
);
}
}
class _PlatformRow extends StatelessWidget {
const _PlatformRow({
required this.ext,
required this.icon,
required this.name,
required this.desc,
});
final AppThemeExtension ext;
final String icon;
final String name;
final String desc;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$name ',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
TextSpan(
text: desc,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
],
),
),
),
],
);
}
}
class _WidgetCard extends StatelessWidget {
const _WidgetCard({
required this.ext,
required this.type,
required this.isInstalled,
required this.isAdding,
required this.onAdd,
required this.onPin,
});
final AppThemeExtension ext;
final WidgetType type;
final bool isInstalled;
final bool isAdding;
final VoidCallback onAdd;
final VoidCallback onPin;
@override
Widget build(BuildContext context) {
return GlassContainer(
depth: GlassDepth.elevated,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
),
child: Icon(type.icon, size: 22, color: ext.accent),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
type.title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: AppSpacing.xs),
_PriorityBadge(priority: type.priority),
],
),
const SizedBox(height: 2),
Text(
type.subtitle,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
],
),
),
const SizedBox(width: AppSpacing.sm),
if (isInstalled)
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
decoration: BoxDecoration(
color: CupertinoColors.systemGreen.withValues(alpha: 0.15),
borderRadius: AppRadius.pillBorder,
),
child: Text(
'已安装',
style: AppTypography.caption2.copyWith(
color: CupertinoColors.systemGreen,
fontWeight: FontWeight.w600,
),
),
)
else
CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
minimumSize: Size.zero,
borderRadius: AppRadius.pillBorder,
color: ext.accent,
onPressed: isAdding ? null : onAdd,
child: isAdding
? const CupertinoActivityIndicator(radius: 8)
: Text(
'添加',
style: AppTypography.caption2.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Row(
children: [
_PlatformBadges(ext: ext, type: type),
const Spacer(),
_DeepLinkBadge(ext: ext, route: type.deepLinkRoute),
],
),
const SizedBox(height: AppSpacing.sm),
_WidgetDataPreview(ext: ext, type: type),
],
),
);
}
}
class _PriorityBadge extends StatelessWidget {
const _PriorityBadge({required this.priority});
final int priority;
@override
Widget build(BuildContext context) {
final label = switch (priority) {
0 => 'P0',
1 => 'P1',
2 => 'P2',
_ => 'P3',
};
final color = switch (priority) {
0 => CupertinoColors.systemRed,
1 => CupertinoColors.systemOrange,
2 => CupertinoColors.systemBlue,
_ => CupertinoColors.systemGrey,
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: AppRadius.xsBorder,
),
child: Text(
label,
style: AppTypography.caption2.copyWith(
color: color,
fontWeight: FontWeight.w700,
fontSize: 9,
),
),
);
}
}
class _PlatformBadges extends StatelessWidget {
const _PlatformBadges({required this.ext, required this.type});
final AppThemeExtension ext;
final WidgetType type;
@override
Widget build(BuildContext context) {
return Row(
children: [
const _Badge(label: 'Android', supported: true),
const SizedBox(width: 4),
const _Badge(label: 'iOS', supported: true),
const SizedBox(width: 4),
_Badge(label: '鸿蒙', supported: type.supportsOhos),
],
);
}
}
class _Badge extends StatelessWidget {
const _Badge({required this.label, required this.supported});
final String label;
final bool supported;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: supported
? CupertinoColors.systemGreen.withValues(alpha: 0.1)
: CupertinoColors.systemGrey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(3),
),
child: Text(
supported ? label : '$label',
style: AppTypography.caption2.copyWith(
color: supported
? CupertinoColors.systemGreen
: CupertinoColors.systemGrey,
fontSize: 9,
fontWeight: FontWeight.w500,
),
),
);
}
}
class _DeepLinkBadge extends StatelessWidget {
const _DeepLinkBadge({required this.ext, required this.route});
final AppThemeExtension ext;
final String route;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.link, size: 10, color: ext.textSecondary),
const SizedBox(width: 2),
Text(
route,
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
fontSize: 9,
),
),
],
);
}
}
class _WidgetDataPreview extends ConsumerStatefulWidget {
const _WidgetDataPreview({required this.ext, required this.type});
final AppThemeExtension ext;
final WidgetType type;
@override
ConsumerState<_WidgetDataPreview> createState() => _WidgetDataPreviewState();
}
class _WidgetDataPreviewState extends ConsumerState<_WidgetDataPreview> {
Map<String, dynamic> _data = {};
bool _loading = true;
bool _disposed = false;
@override
void initState() {
super.initState();
// 使用addPostFrameCallback确保element完全挂载后再访问ref
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _disposed) return;
_loadPreview();
});
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
Future<void> _loadPreview() async {
if (_disposed || !mounted) return;
try {
final service = ref.read(homeWidgetServiceProvider);
final data = await service.debugGetAllData().timeout(
const Duration(seconds: 5),
);
if (_disposed || !mounted) return;
setState(() {
_data = data;
_loading = false;
});
} on TimeoutException {
if (_disposed || !mounted) return;
setState(() => _loading = false);
Log.w('WidgetDataPreview: 加载预览数据超时');
} catch (e) {
if (_disposed || !mounted) return;
setState(() => _loading = false);
Log.e('WidgetDataPreview: 加载预览数据失败', e);
}
}
Future<void> _refreshData() async {
if (_disposed || !mounted) return;
setState(() => _loading = true);
try {
final service = ref.read(homeWidgetServiceProvider);
await service
.updateWidget(widget.type)
.timeout(const Duration(seconds: 5));
await _loadPreview();
} on TimeoutException {
if (_disposed || !mounted) return;
setState(() => _loading = false);
Log.w('WidgetDataPreview: 刷新数据超时');
} catch (e) {
if (_disposed || !mounted) return;
setState(() => _loading = false);
Log.e('WidgetDataPreview: 刷新数据失败', e);
}
}
String _getPreviewText() {
return switch (widget.type) {
WidgetType.dailySentence => '${_data['daily_sentence'] ?? '暂无数据'}',
WidgetType.readlater => '未读 ${_data['readlater_count'] ?? 0}',
WidgetType.dailyFortune => '${_data['fortune_text'] ?? '暂无数据'}',
WidgetType.countdown => '${_data['countdown_title'] ?? '暂无数据'}',
WidgetType.pomodoro => '剩余 ${_data['pomodoro_remaining'] ?? 0}s',
WidgetType.solarTerm => '${_data['solar_term_name'] ?? '暂无数据'}',
WidgetType.checkin => '连续 ${_data['checkin_days'] ?? 0}',
WidgetType.dailyWithCharacter =>
'${_data['daily_with_character_content'] ?? '暂无数据'}',
WidgetType.dailyCard => '日签卡片',
};
}
@override
Widget build(BuildContext context) {
final ext = widget.ext;
return Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.5),
borderRadius: AppRadius.mdBorder,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.eye_fill, size: 12, color: ext.textSecondary),
const SizedBox(width: 4),
Text(
'数据预览',
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
GestureDetector(
onTap: _loading ? null : _refreshData,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.refresh, size: 12, color: ext.accent),
const SizedBox(width: 2),
Text(
'刷新',
style: AppTypography.caption2.copyWith(
color: ext.accent,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
const SizedBox(height: AppSpacing.xs),
if (_loading)
const CupertinoActivityIndicator(radius: 6)
else
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgCard.withValues(alpha: 0.6),
borderRadius: AppRadius.smBorder,
),
child: Text(
_getPreviewText(),
style: AppTypography.footnote.copyWith(color: ext.textPrimary),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}