feat: phase1 UI prep — 设备ID系统传递 + 国家本地化展示 + 继续观看直进播放页

设备标识: Android SSAID/ iOS IDFV 原生通道 → Dart DeviceIdProvider
国家解析: 注册/登录从后端 IP → 国家/语言/时区/币种 自动落库
账号资料: 全部本地化展示 (account_display.dart)
我的页: 继续观看卡片点击直接进播放页
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-27 15:45:20 +08:00
1 parent 1b19513efb
commit a3f3481f4f
15 files changed
+468 -25

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ class AppConfig {
/// 开发调试默认服务地址,当前指向本机局域网 IP。
///
/// 换网络时只改这一行,地址不要带结尾斜杠。
static const String devHostBaseUrl = 'http://10.43.47.1:8081';
static const String devHostBaseUrl = 'http://192.168.31.243:8081';
/// 本机后端地址:通过 `adb reverse tcp:8081 tcp:8081` 映射到开发机,
/// 探测可达时 App 直连本机服务;不可达时回落到公网地址。
+4 -1
View File
@@ -56,6 +56,7 @@ class AuthApi {
required String target,
required String code,
required String deviceId,
String countryCode = '',
String platform = '',
String osVersion = '',
String appVersion = '',
@@ -66,6 +67,7 @@ class AuthApi {
'targetType': 'PHONE',
'target': target,
'code': code,
'countryCode': countryCode,
'deviceId': deviceId,
'platform': platform,
'osVersion': osVersion,
@@ -79,6 +81,7 @@ class AuthApi {
required String email,
required String password,
required String deviceId,
String countryCode = '',
String platform = '',
String osVersion = '',
String appVersion = '',
@@ -89,7 +92,7 @@ class AuthApi {
'targetType': 'EMAIL',
'target': email,
'password': password,
'countryCode': 'ID',
'countryCode': countryCode,
'deviceId': deviceId,
'platform': platform,
'osVersion': osVersion,
@@ -48,6 +48,7 @@ class AuthController extends ChangeNotifier {
email: email,
password: password,
deviceId: await _store.deviceId(),
countryCode: await _store.deviceRegion(),
platform: _platform,
osVersion: _osVersion,
);
@@ -95,6 +96,7 @@ class AuthController extends ChangeNotifier {
target: target,
code: code,
deviceId: await _store.deviceId(),
countryCode: await _store.deviceRegion(),
platform: _platform,
osVersion: _osVersion,
);
@@ -30,6 +30,7 @@ class AuthSessionStore {
static const _kDeviceId = 'device.id';
String? _cachedDeviceId;
String? _cachedRegion;
Future<String> deviceId() async {
if (_cachedDeviceId != null) return _cachedDeviceId!;
@@ -39,6 +40,14 @@ class AuthSessionStore {
return value;
}
/// 设备区域(ISO-2),取不到返回空串;只在注册时作为服务端 IP 解析失败后的兜底。
Future<String> deviceRegion() async {
if (_cachedRegion != null) return _cachedRegion!;
final value = await _deviceIdProvider.deviceRegion() ?? '';
_cachedRegion = value;
return value;
}
/// 系统标识取不到时的兜底:本地 UUIDv4,生成一次后固定下来。
Future<String> _localDeviceId() async {
final stored = await _preferences.getString(_kDeviceId) ?? '';
@@ -33,4 +33,18 @@ class DeviceIdProvider {
}
return null;
}
/// 设备所在区域(ISO-2),仅用于服务端 IP 解析失败时兜底注册国家。
/// Android 依次取网络运营商 / SIM 卡 / 系统语言的国家,iOS 取系统 region。
Future<String?> deviceRegion() async {
if (kIsWeb) return null;
try {
final region = await _channel.invokeMethod<String>('region');
final value = region?.trim().toUpperCase() ?? '';
if (value.length == 2) return value;
} catch (_) {
// 原生通道不可用(widget 测试)时返回 null,由服务端兜底
}
return null;
}
}
+239
View File
@@ -0,0 +1,239 @@
// Author: fruit.com.cn
/// 账号资料页只读字段的展示格式化。
///
/// 后端存的是 ISO 国家码、BCP-47 语言标签和 IANA 时区(`ID` / `id-ID` / `Asia/Jakarta`),
/// 直接展示对用户没有意义,这里统一转成当前界面语言的名称。
/// 国家与城市名只维护中文与英文两套:界面语言为印尼语时回退英文名,
/// 语言名与账号状态则三种界面语言都有。
library;
const _countryNamesZh = {
'ID': '印度尼西亚',
'MY': '马来西亚',
'SG': '新加坡',
'TH': '泰国',
'VN': '越南',
'PH': '菲律宾',
'KH': '柬埔寨',
'MM': '缅甸',
'LA': '老挝',
'BN': '文莱',
'CN': '中国',
'TW': '中国台湾',
'HK': '中国香港',
'MO': '中国澳门',
'JP': '日本',
'KR': '韩国',
'IN': '印度',
'PK': '巴基斯坦',
'BD': '孟加拉国',
'LK': '斯里兰卡',
'NP': '尼泊尔',
'US': '美国',
'CA': '加拿大',
'MX': '墨西哥',
'BR': '巴西',
'AR': '阿根廷',
'CL': '智利',
'CO': '哥伦比亚',
'PE': '秘鲁',
'VE': '委内瑞拉',
'EC': '厄瓜多尔',
'GT': '危地马拉',
'DO': '多米尼加',
'GB': '英国',
'DE': '德国',
'FR': '法国',
'ES': '西班牙',
'IT': '意大利',
'NL': '荷兰',
'PL': '波兰',
'RU': '俄罗斯',
'UA': '乌克兰',
'TR': '土耳其',
'SA': '沙特阿拉伯',
'AE': '阿联酋',
'IL': '以色列',
'EG': '埃及',
'NG': '尼日利亚',
'ZA': '南非',
'KE': '肯尼亚',
'MA': '摩洛哥',
'AU': '澳大利亚',
'NZ': '新西兰',
};
const _countryNamesEn = {
'ID': 'Indonesia',
'MY': 'Malaysia',
'SG': 'Singapore',
'TH': 'Thailand',
'VN': 'Vietnam',
'PH': 'Philippines',
'KH': 'Cambodia',
'MM': 'Myanmar',
'LA': 'Laos',
'BN': 'Brunei',
'CN': 'China',
'TW': 'Taiwan',
'HK': 'Hong Kong',
'MO': 'Macau',
'JP': 'Japan',
'KR': 'South Korea',
'IN': 'India',
'PK': 'Pakistan',
'BD': 'Bangladesh',
'LK': 'Sri Lanka',
'NP': 'Nepal',
'US': 'United States',
'CA': 'Canada',
'MX': 'Mexico',
'BR': 'Brazil',
'AR': 'Argentina',
'CL': 'Chile',
'CO': 'Colombia',
'PE': 'Peru',
'VE': 'Venezuela',
'EC': 'Ecuador',
'GT': 'Guatemala',
'DO': 'Dominican Republic',
'GB': 'United Kingdom',
'DE': 'Germany',
'FR': 'France',
'ES': 'Spain',
'IT': 'Italy',
'NL': 'Netherlands',
'PL': 'Poland',
'RU': 'Russia',
'UA': 'Ukraine',
'TR': 'Turkey',
'SA': 'Saudi Arabia',
'AE': 'United Arab Emirates',
'IL': 'Israel',
'EG': 'Egypt',
'NG': 'Nigeria',
'ZA': 'South Africa',
'KE': 'Kenya',
'MA': 'Morocco',
'AU': 'Australia',
'NZ': 'New Zealand',
};
const _timezoneNamesZh = {
'Asia/Jakarta': '雅加达',
'Asia/Kuala_Lumpur': '吉隆坡',
'Asia/Singapore': '新加坡',
'Asia/Bangkok': '曼谷',
'Asia/Ho_Chi_Minh': '胡志明市',
'Asia/Manila': '马尼拉',
'Asia/Phnom_Penh': '金边',
'Asia/Yangon': '仰光',
'Asia/Vientiane': '万象',
'Asia/Brunei': '斯里巴加湾市',
'Asia/Shanghai': '上海',
'Asia/Taipei': '台北',
'Asia/Hong_Kong': '香港',
'Asia/Macau': '澳门',
'Asia/Tokyo': '东京',
'Asia/Seoul': '首尔',
'Asia/Kolkata': '加尔各答',
'Asia/Karachi': '卡拉奇',
'Asia/Dhaka': '达卡',
'Asia/Colombo': '科伦坡',
'Asia/Kathmandu': '加德满都',
'America/New_York': '纽约',
'America/Toronto': '多伦多',
'America/Mexico_City': '墨西哥城',
'America/Sao_Paulo': '圣保罗',
'America/Argentina/Buenos_Aires': '布宜诺斯艾利斯',
'America/Santiago': '圣地亚哥',
'America/Bogota': '波哥大',
'America/Lima': '利马',
'America/Caracas': '加拉加斯',
'America/Guayaquil': '瓜亚基尔',
'America/Guatemala': '危地马拉城',
'America/Santo_Domingo': '圣多明各',
'Europe/London': '伦敦',
'Europe/Berlin': '柏林',
'Europe/Paris': '巴黎',
'Europe/Madrid': '马德里',
'Europe/Rome': '罗马',
'Europe/Amsterdam': '阿姆斯特丹',
'Europe/Warsaw': '华沙',
'Europe/Moscow': '莫斯科',
'Europe/Kyiv': '基辅',
'Europe/Istanbul': '伊斯坦布尔',
'Asia/Riyadh': '利雅得',
'Asia/Dubai': '迪拜',
'Asia/Jerusalem': '耶路撒冷',
'Africa/Cairo': '开罗',
'Africa/Lagos': '拉各斯',
'Africa/Johannesburg': '约翰内斯堡',
'Africa/Nairobi': '内罗毕',
'Africa/Casablanca': '卡萨布兰卡',
'Australia/Sydney': '悉尼',
'Pacific/Auckland': '奥克兰',
};
const _languageNames = {
'id': {'zh': '印度尼西亚语', 'en': 'Indonesian', 'id': 'Bahasa Indonesia'},
'en': {'zh': '英语', 'en': 'English', 'id': 'Inggris'},
'zh': {'zh': '中文', 'en': 'Chinese', 'id': 'Mandarin'},
};
const _statusNames = {
'normal': {'zh': '正常', 'en': 'Active', 'id': 'Aktif'},
'limited': {'zh': '受限', 'en': 'Limited', 'id': 'Dibatasi'},
'banned': {'zh': '已封禁', 'en': 'Banned', 'id': 'Diblokir'},
'deactivated': {'zh': '已注销', 'en': 'Deactivated', 'id': 'Dinonaktifkan'},
};
String _pick(Map<String, String> names, String localeName) =>
names[localeName] ?? names['en']!;
/// 国家码 → 国家名;未收录的码原样返回。
String countryName(String code, String localeName) {
final key = code.trim().toUpperCase();
if (key.isEmpty) return '';
final names = localeName == 'zh' ? _countryNamesZh : _countryNamesEn;
return names[key] ?? key;
}
/// IANA 时区 → 城市名;中文界面查表,其余语言取时区里的城市段。
String timezoneName(String timezone, String localeName) {
final value = timezone.trim();
if (value.isEmpty) return '';
if (localeName == 'zh') {
final name = _timezoneNamesZh[value];
if (name != null) return name;
}
final city = value.split('/').last.replaceAll('_', ' ');
return city.isEmpty ? value : city;
}
/// BCP-47 语言标签 → 语言名,按主语言归类(`zh-TW` 也算中文)。
String languageName(String language, String localeName) {
final value = language.trim();
if (value.isEmpty) return '';
final primary = value.split(RegExp('[-_]')).first.toLowerCase();
final names = _languageNames[primary];
return names == null ? value : _pick(names, localeName);
}
/// 账号状态 → 展示文案;未知状态原样返回。
String accountStatusText(String status, String localeName) {
final value = status.trim().toLowerCase();
final names = _statusNames[value];
return names == null ? status : _pick(names, localeName);
}
/// VIP 等级:0 表示未开通,避免用户看到一个孤零零的 0。
String vipLevelText(int level, String localeName) {
if (level > 0) return 'VIP $level';
return switch (localeName) {
'zh' => '未开通',
'id' => 'Belum aktif',
_ => 'Not subscribed',
};
}