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