Files
xianyan/lib/features/source/presentation/source_widgets.dart
Developer 544f77c0ce chore: 完成v2.4.7版本迭代更新
本次更新包含多项功能优化与兼容性修复:
1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷
2. 新增学习计划设置页路由与国际化支持
3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器
4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈
5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案
6. 优化搜索快捷方式跳转逻辑,避免白屏问题
7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译
8. 修复节气日期表排序与跨年边界问题
9. 优化设备信息页面显示,新增系统版本号展示
10. 重构文件传输二维码逻辑,使用纯URL提升兼容性
11. 优化设置项布局,避免文本溢出问题
12. 修复登录页记住账户功能,新增隐私协议守卫
13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
2026-06-17 08:45:34 +08:00

249 lines
7.1 KiB
Dart

/// ============================================================
/// 闲言APP — 句子来源通用组件
/// 创建时间: 2026-05-01
/// 更新时间: 2026-05-13
/// 作用: 句子来源页面的通用小组件和工具方法
/// 上次更新: MixModeOption.icon改为IconData, 添加SettingRow组件
/// ============================================================
import 'package:flutter/cupertino.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/utils/ui/interaction_animations.dart';
import '../../home/feed_model.dart';
/// 混合模式定义
class MixModeOption {
const MixModeOption({
required this.key,
required this.icon,
required this.name,
required this.desc,
});
final String key;
final IconData icon;
final String name;
final String desc;
}
/// 全局混合模式列表
const mixModes = [
MixModeOption(key: 'uniform', icon: CupertinoIcons.arrow_left_right, name: '均匀交叉', desc: '各分类轮流出场'),
MixModeOption(key: 'ratio', icon: CupertinoIcons.chart_bar, name: '比例混合', desc: '按权重比例分配'),
MixModeOption(key: 'specific', icon: CupertinoIcons.scope, name: '仅指定分类', desc: '只从勾选分类获取'),
MixModeOption(key: 'random', icon: CupertinoIcons.shuffle, name: '随机混排', desc: '完全随机获取'),
MixModeOption(key: 'group', icon: CupertinoIcons.arrow_2_circlepath, name: '分组循环', desc: '每分类连续N条后切换'),
];
/// 根据key获取混合模式
MixModeOption getMixMode(String key) {
return mixModes.firstWhere((m) => m.key == key, orElse: () => mixModes[3]);
}
/// 设置行组件
class SettingRow extends StatelessWidget {
const SettingRow({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.iconColor,
this.trailing,
this.onTap,
});
final IconData icon;
final String title;
final String? subtitle;
final Color? iconColor;
final Widget? trailing;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final effectiveIconColor = iconColor ?? ext.accent;
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: effectiveIconColor.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Icon(icon, size: 18, color: effectiveIconColor),
),
const SizedBox(width: AppSpacing.sm),
// 使用 Flexible 让标题列在空间不足时收缩,避免 trailing 溢出
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (subtitle != null)
Text(
subtitle!,
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
if (trailing != null) ...[
const SizedBox(width: AppSpacing.xs),
trailing!,
],
],
),
),
);
}
}
/// 频道卡片组件
class ChannelCard extends StatelessWidget {
const ChannelCard({
super.key,
required this.channel,
required this.enabled,
required this.ext,
required this.onToggle,
required this.onTap,
});
final FeedChannel channel;
final bool enabled;
final AppThemeExtension ext;
final VoidCallback onToggle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final typeColor = FeedTypeColor.getColor(channel.key);
return PressableCard(
onTap: onTap,
child: Opacity(
opacity: enabled ? 1.0 : 0.45,
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: typeColor.withValues(alpha: 0.15),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Text(channel.icon, style: const TextStyle(fontSize: 22)),
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
channel.name,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
'${channel.count}',
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
),
CupertinoSwitch(
value: enabled,
onChanged: (_) => onToggle(),
activeTrackColor: ext.accent,
),
],
),
),
);
}
}
/// 详情统计项组件
class DetailStatItem extends StatelessWidget {
const DetailStatItem({
super.key,
required this.label,
required this.value,
required this.color,
});
final String label;
final String value;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
value,
style: AppTypography.title3.copyWith(
color: color,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
label,
style: AppTypography.caption2.copyWith(
color: AppTheme.ext(context).textSecondary,
),
),
],
);
}
}
/// 格式化数量
String fmtCount(int n) {
if (n >= 10000) return '${(n / 10000).toStringAsFixed(1)}w';
if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}k';
return '$n';
}
/// 格式化浏览量
String fmtViews(int n) {
if (n >= 10000) return '${(n / 10000).toStringAsFixed(1)}w';
if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}k';
return '$n';
}