创建完整的 Flutter 项目结构,包含 Android 和 iOS 平台配置、核心网络层、路由、状态管理及业务模块(订单、收益、认证),适配原 uni-app 项目功能逻辑
159 lines
8.4 KiB
Dart
159 lines
8.4 KiB
Dart
import 'package:flutter/widgets.dart';
|
||
import 'package:flutter_svg/flutter_svg.dart';
|
||
|
||
import '../../core/theme/app_colors.dart';
|
||
|
||
/// 图标组件,对应 uni-app 版页面里的 `<text class="iconfont icon-xxx">`。
|
||
///
|
||
/// **为什么是混合方案**:源工程只提供了 8 个真实字形(`iconfont.ttf`),而页面里
|
||
/// 引用的 `icon-lightning` / `icon-location` / `icon-star` 等**在字体里并不存在**,
|
||
/// 源工程实际渲染为空白。为保证复刻后的界面可读可用,这里:
|
||
/// - 8 个真实字形继续用搬运过来的 `iconfont.ttf` 渲染,保持逐像素一致;
|
||
/// - 其余语义图标改用 SVG 路径绘制,线条风格与源设计语言保持一致。
|
||
class XIcon extends StatelessWidget {
|
||
const XIcon(
|
||
this.name, {
|
||
super.key,
|
||
this.size = 20,
|
||
this.color = AppColors.textMuted,
|
||
});
|
||
|
||
final String name;
|
||
final double size;
|
||
final Color color;
|
||
|
||
/// `iconfont.ttf` 中真实存在的字形 -> Unicode 码点(取自源 `iconfont.css`)。
|
||
static const Map<String, int> glyphs = {
|
||
'shandian': 0xe6a4,
|
||
'tongzhi': 0xe60a,
|
||
'qianbao': 0xe60b,
|
||
'yaoqing': 0x100a5,
|
||
'dingdan': 0xe62a,
|
||
'weixin': 0xe665,
|
||
'wode': 0xe607,
|
||
'shouyi': 0x10186,
|
||
};
|
||
|
||
/// 语义图标的 SVG 路径(24×24 视图框,`currentColor` 会被替换成实际色值)。
|
||
static const Map<String, String> paths = {
|
||
'bolt': '<path d="M13 2L4.5 13.5H11l-2 8.5L19 10h-6.5z"/>',
|
||
'location':
|
||
'<path d="M12 2C8.1 2 5 5.1 5 9c0 5.2 7 13 7 13s7-7.8 7-13c0-3.9-3.1-7-7-7z"/><circle cx="12" cy="9" r="2.5"/>',
|
||
'clock':
|
||
'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>',
|
||
'filter': '<path d="M4 5h16l-7 8v5l-2 1v-6z"/>',
|
||
'map':
|
||
'<path d="M9 4L3 7v13l6-3 6 3 6-3V4l-6 3z"/><path d="M9 4v13M15 7v13" stroke="currentColor" stroke-width="1.5" fill="none"/>',
|
||
'refresh':
|
||
'<path d="M20 11a8 8 0 1 0-1.5 5.6" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/><path d="M20 5v6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'check':
|
||
'<path d="M4 12.5l5 5L20 6.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'forward':
|
||
'<path d="M9 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'back':
|
||
'<path d="M15 5l-7 7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'arrow-down':
|
||
'<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'star': '<path d="M12 3l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.8 6.2 20.9l1.1-6.5L2.6 9.8l6.5-.9z"/>',
|
||
'verified':
|
||
'<path d="M12 2l2.5 2.5 3.5-.5 1 3.4 3 2-1 3.6 1 3.6-3 2-1 3.4-3.5-.5L12 22l-3.5-1-3.5.5-.5-3-3-2 1-4-.5-3H3l3-2 1-3.4 3.5.5z"/><path d="M8 12l3 3 5-6" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'notification':
|
||
'<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zM18 16v-5.5c0-3.1-1.6-5.6-4.5-6.3V3.5a1.5 1.5 0 0 0-3 0v.7C7.6 4.9 6 7.4 6 10.5V16l-2 2v1h16v-1l-2-2z"/>',
|
||
'share':
|
||
'<path d="M18 16.1c-.8 0-1.5.3-2 .8l-7.1-4.1c.1-.3.1-.5.1-.8s0-.5-.1-.8l7-4.1c.5.5 1.3.8 2.1.8 1.7 0 3-1.3 3-3s-1.3-3-3-3-3 1.3-3 3c0 .3 0 .5.1.8L8.1 9.8C7.6 9.3 6.9 9 6 9c-1.7 0-3 1.3-3 3s1.3 3 3 3c.9 0 1.6-.3 2.1-.8l7.1 4.1c-.1.2-.1.5-.1.7 0 1.6 1.3 3 3 3s3-1.3 3-3-1.4-2.9-3.1-2.9z"/>',
|
||
'navigation':
|
||
'<path d="M12 2l10 10-10 10L2 12z"/><path d="M12 8.6l3.4 3.4-3.4 3.4" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'call':
|
||
'<path d="M6.6 3c.6 0 1.2.4 1.4 1l1 3c.2.5.1 1.1-.3 1.5L7 9.6c1 1.9 2.5 3.4 4.4 4.4l1.1-1.7c.4-.4 1-.5 1.5-.3l3 1c.6.2 1 .8 1 1.4V18c0 1-1 2-2 2C8 20 4 16 4 5c0-1 1-2 2-2z"/>',
|
||
'add-photo':
|
||
'<path d="M4 8h3l1.5-2h5L15 8h3a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z"/><circle cx="10" cy="13.5" r="3"/><path d="M19 2v5M16.5 4.5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>',
|
||
'info':
|
||
'<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 11v6M12 7.4v.2" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>',
|
||
'warning': '<path d="M12 3L2 20h20zM12 9v5M12 17.5v.1"/>',
|
||
'wallet':
|
||
'<rect x="3" y="6" width="18" height="13" rx="2.5"/><path d="M3 10.5h18" stroke="#ffffff" stroke-width="1.6" fill="none"/><circle cx="16.6" cy="14.4" r="1.3" fill="#ffffff"/>',
|
||
'list':
|
||
'<rect x="4" y="3" width="16" height="18" rx="2.5"/><path d="M8 8h8M8 12h8M8 16h5" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" fill="none"/>',
|
||
'settings':
|
||
'<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></g>',
|
||
'trend-up':
|
||
'<path d="M4 17l5-5 3 3 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M13 9h5v5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
||
'close':
|
||
'<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>',
|
||
'person': '<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.5-6.5 8-6.5s8 2.5 8 6.5v1H4z"/>',
|
||
'camera':
|
||
'<rect x="3" y="6" width="18" height="14" rx="2"/><path d="M8 6l1.5-2h5L16 6"/><circle cx="12" cy="13" r="3.5"/>',
|
||
};
|
||
|
||
/// 源里两种命名风格混用(uni-app 的 `icon-xxx` 与 Material Symbols 名),
|
||
/// 这里统一做别名收敛,调用方两种写法都能渲染。
|
||
static const Map<String, String> _aliases = {
|
||
'lightning': 'bolt',
|
||
'time': 'clock',
|
||
'cert': 'verified',
|
||
'notify': 'notification',
|
||
'arrow-right': 'forward',
|
||
'order': 'list',
|
||
'earnings': 'wallet',
|
||
'chevron_left': 'back',
|
||
'location_on': 'location',
|
||
'directions': 'navigation',
|
||
'add_a_photo': 'add-photo',
|
||
};
|
||
|
||
static final Map<String, String> _svgCache = {};
|
||
|
||
static String _hex(Color color) =>
|
||
'#${(color.toARGB32() & 0xFFFFFF).toRadixString(16).padLeft(6, '0')}';
|
||
|
||
/// 归一化后的语义名:别名 -> 目标名。
|
||
static String resolve(String name) => _aliases[name] ?? name;
|
||
|
||
/// 生成 (图标, 颜色) 组合对应的 SVG 字符串,带缓存避免重复拼接。
|
||
static String svgFor(String name, Color color) {
|
||
final key = '$name|${color.toARGB32()}';
|
||
return _svgCache.putIfAbsent(key, () {
|
||
final body = (paths[resolve(name)] ?? paths['close']!).replaceAll(
|
||
'currentColor',
|
||
_hex(color),
|
||
);
|
||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" '
|
||
'fill="${_hex(color)}">$body</svg>';
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final glyph = glyphs[name];
|
||
if (glyph != null) {
|
||
// 源里的彩色字形:直接交给 iconfont.ttf 渲染,保留原字形配色。
|
||
return SizedBox(
|
||
width: size,
|
||
height: size,
|
||
child: Center(
|
||
child: Text(
|
||
String.fromCharCode(glyph),
|
||
style: TextStyle(
|
||
fontFamily: 'iconfont',
|
||
fontSize: size,
|
||
height: 1,
|
||
color: color,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return SizedBox(
|
||
width: size,
|
||
height: size,
|
||
child: SvgPicture.string(
|
||
svgFor(name, color),
|
||
width: size,
|
||
height: size,
|
||
fit: BoxFit.contain,
|
||
),
|
||
);
|
||
}
|
||
}
|