AI 接入 V1.0

UI更新
This commit is contained in:
2026-09-24 14:24:34 +08:00
parent 34bc2e04b0
commit 9a144f159f
65 changed files with 1195 additions and 196 deletions
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
/// 双色图标组件。
///
/// 图标是 SVG 资源(`assets/icons/*.svg`),由 `tools/make_icons.py` 生成 ——
/// 改形状或配色请改那个脚本再重跑,不要手工编辑 SVG 文件。
///
/// 每个图标自带一套配色(浅色填充 + 深色描边)。需要单色时传 [tint],
/// 它会把整个图标压成一种颜色 —— 比如放在焦糖色主按钮上的白色图标。
class CuteIcon extends StatelessWidget {
const CuteIcon(this.name, {super.key, this.size = 22, this.tint});
/// 图标名,见 [cuteIconNames]
final String name;
final double size;
/// 传了就压成单色(丢失双色效果,但能适配任意背景)
final Color? tint;
@override
Widget build(BuildContext context) {
final resolved = resolveIconName(name);
return SvgPicture.asset(
'assets/icons/$resolved.svg',
width: size,
height: size,
colorFilter:
tint == null ? null : ColorFilter.mode(tint!, BlendMode.srcIn),
);
}
}
/// 所有合法的图标名(和 tools/make_icons.py 里的 ICONS 一一对应)
const cuteIconNames = <String>{
// 记账分类
'food', 'transport', 'shopping', 'entertainment', 'medical', 'education',
'housing', 'utilities', 'phone', 'other', 'salary', 'bonus', 'invest',
'parttime',
// 模块
'bill', 'note', 'assistant',
// 界面
'expense', 'income', 'camera', 'search', 'user', 'lock', 'eye', 'image',
'file', 'folder', 'folder-open', 'check', 'pin', 'target', 'inbox', 'error',
};
/// 老数据的兼容映射:emoji → 图标名。
///
/// 分类图标以前是 emoji 存在数据库里(`category.icon`),现在换成了图标名。
/// 这份映射有两个作用:
/// ① 迁移还没跑的时候,老数据照样能显示
/// ② 迁移跑完之后,用户自己建分类时随手挑个 emoji 也能兜住
/// 都不认识就退到 `other`,不会出现空白图标。
///
/// 公开出来是为了能写测试(映射表里写错一个名字会静默退化成兜底图标,
/// 不测的话很难发现)。
const cuteEmojiAliases = <String, String>{
'🍔': 'food', '🍜': 'food', '🍚': 'food', '🍕': 'food', '☕': 'food',
'🚗': 'transport', '🚌': 'transport', '🚇': 'transport', '✈': 'transport',
'🛍': 'shopping', '🛒': 'shopping', '👕': 'shopping', '🎁': 'bonus',
'🎬': 'entertainment', '🎮': 'entertainment', '🎵': 'entertainment',
'🏥': 'medical', '💊': 'medical', '💉': 'medical',
'📚': 'education', '🎓': 'education', '📖': 'education', '✏': 'education',
'🏠': 'housing', '🏡': 'housing', '🛏': 'housing',
'💡': 'utilities', '💧': 'utilities', '⚡': 'utilities', '🔌': 'utilities',
'📱': 'phone', '☎': 'phone', '💻': 'phone',
'💰': 'salary', '💵': 'salary', '🏦': 'salary', '💴': 'salary',
'📈': 'invest', '📊': 'invest', '💹': 'invest', '🪙': 'invest',
'💼': 'parttime', '🧑‍💻': 'parttime', '🔧': 'parttime',
'📦': 'other', '📌': 'other', '🏷': 'other', '❓': 'other',
};
/// 把任意来源的图标值(图标名 / emoji / 空)解析成一个合法的图标名
String resolveIconName(String? raw) {
final value = (raw ?? '').trim();
if (value.isEmpty) return 'other';
if (cuteIconNames.contains(value)) return value;
// emoji 里可能带变体选择符(U+FE0F),去掉再查一次
final stripped = value.replaceAll('\uFE0F', '');
return cuteEmojiAliases[value] ?? cuteEmojiAliases[stripped] ?? 'other';
}