ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
161
lib/utils/app_update.dart
Normal file
161
lib/utils/app_update.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../services/api_client.dart';
|
||||
import '../services/update_service.dart';
|
||||
import '../utils/i10n.dart';
|
||||
|
||||
/// 全局「已弹过更新提示」标记,避免会话内重复弹窗。
|
||||
bool _promptedThisSession = false;
|
||||
|
||||
/// 全局当前 [PackageInfo] 缓存(首次读取后复用)。
|
||||
PackageInfo? _cachedPackageInfo;
|
||||
|
||||
/// 取得当前 App 包信息(带缓存)。
|
||||
Future<PackageInfo> get currentPackageInfo async =>
|
||||
_cachedPackageInfo ??= await PackageInfo.fromPlatform();
|
||||
|
||||
/// 检测更新并在有新版时弹窗提示。
|
||||
///
|
||||
/// - [silentIfNoUpdate]=true(如启动时):无更新或检测失败均静默;
|
||||
/// - [silentIfNoUpdate]=false(如手动「检查更新」):无更新/失败给出明确反馈。
|
||||
/// - [orgId] 当前登录用户所属组织 ID;传空时与旧行为一致。
|
||||
Future<void> checkAndPromptUpdate(
|
||||
BuildContext context, {
|
||||
bool silentIfNoUpdate = true,
|
||||
String? orgId,
|
||||
}) async {
|
||||
final t = AppLocalizations.of(context);
|
||||
try {
|
||||
final pkg = await currentPackageInfo;
|
||||
final service = UpdateService(packageName: pkg.packageName);
|
||||
final currentCode = int.tryParse(pkg.buildNumber) ?? 0;
|
||||
final result = await service.checkForUpdate(currentCode, orgId: orgId);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (result.hasUpdate && result.latest != null) {
|
||||
if (!_promptedThisSession) {
|
||||
_promptedThisSession = true;
|
||||
showUpdateDialog(context, result, pkg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silentIfNoUpdate && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.updateNoNew)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// 任何异常均被捕获,不抛给调用方;仅非静默模式下给用户一句友好提示。
|
||||
if (!silentIfNoUpdate && context.mounted) {
|
||||
final detail = formatApiError(e, fallback: '网络异常,请稍后重试');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('检测更新失败:$detail')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本更新提示对话框:展示当前 / 最新版本,并提供「立即更新」跳转下载。
|
||||
void showUpdateDialog(
|
||||
BuildContext context,
|
||||
UpdateCheckResult result,
|
||||
PackageInfo pkg,
|
||||
) {
|
||||
final t = AppLocalizations.of(context);
|
||||
final latest = result.latest!;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.system_update_alt, color: Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Text(t.updateAvailable),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_InfoRow(label: t.currentVersion, value: '${pkg.version} (${pkg.buildNumber})'),
|
||||
const SizedBox(height: 6),
|
||||
_InfoRow(
|
||||
label: t.latestVersion,
|
||||
value: '${latest.versionName} (${latest.versionCode})',
|
||||
),
|
||||
if (latest.apkUrl.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
_InfoRow(label: t.downloadUrl, value: latest.apkUrl),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(t.updateLater),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx);
|
||||
final url = latest.apkUrl;
|
||||
if (url.isEmpty) return;
|
||||
final uri = Uri.parse(url);
|
||||
try {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.updateOpenFailed)),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.updateOpenFailed)),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(t.updateNow),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 对话框内的「标签:值」单行。
|
||||
class _InfoRow extends StatelessWidget {
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 84,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
value,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/utils/constants.dart
Normal file
30
lib/utils/constants.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
/// 全局常量定义。
|
||||
class AppConstants {
|
||||
AppConstants._();
|
||||
|
||||
/// 应用名称。
|
||||
static const String appName = '云定制主页运营助手';
|
||||
|
||||
/// 是否使用 mock 数据。
|
||||
///
|
||||
/// - `false`(默认):对接真实「云定制后台」(PHP/CodeIgniter);
|
||||
/// - `true`:由 [MockInterceptor] 本地返回示例数据,供无后端时演示。
|
||||
static const bool useMock = false;
|
||||
|
||||
/// 接口统一前缀。
|
||||
///
|
||||
/// 后端 origin 与环境相关配置已迁移至 `lib/config/*`
|
||||
/// ([AppEnvironment] / [AppConfig] / [TestConfig] / [StagingConfig]),
|
||||
/// 默认锁定测试环境,禁止任何生产切换。
|
||||
static const String apiPrefix = '/api';
|
||||
|
||||
/// mock 登录账号(仅 useMock=true 时演示用)。
|
||||
static const String mockAccount = 'admin';
|
||||
static const String mockPassword = '123456';
|
||||
|
||||
/// 请求超时时间。
|
||||
static const Duration apiTimeout = Duration(seconds: 10);
|
||||
|
||||
/// 设备列表分页默认每页条数。
|
||||
static const int deviceListPageSize = 100;
|
||||
}
|
||||
134
lib/utils/credential_storage.dart
Normal file
134
lib/utils/credential_storage.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 本地凭据持久化(基于 SharedPreferences,跨平台可靠持久化)。
|
||||
///
|
||||
/// 存储内容:
|
||||
/// - 账号 / 密码(仅当用户勾选「记住密码」时保存)
|
||||
/// - token / refreshToken / 用户信息(登录成功后保存,用于免登录恢复)
|
||||
///
|
||||
/// 历史坑:早期版本使用 dart:io 文件存储 + `Directory.current`,但 Android 上
|
||||
/// `Directory.current` 指向不可写路径,会静默退化为「仅内存」存储,导致 App 重启后
|
||||
/// 凭据与登录态全部丢失(表现即「记住账号密码不生效 / 每次都要重新登录」)。
|
||||
/// 现统一改用 SharedPreferences(应用私有、沙箱隔离、必定可写、跨重启持久)。
|
||||
class CredentialStorage {
|
||||
CredentialStorage._();
|
||||
|
||||
/// 单例。
|
||||
static final CredentialStorage instance = CredentialStorage._();
|
||||
|
||||
/// SharedPreferences 实例;[init] 完成后非 null(除非极端初始化失败)。
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
/// 是否已尝试初始化(无论成功失败都置 true,避免重复 await 同一 Future)。
|
||||
bool _initialized = false;
|
||||
|
||||
/// 异步预初始化:必须在 runApp 前 await,使下方同步 getter 可读到持久化数据。
|
||||
static Future<void> init() async {
|
||||
await instance._init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
if (_initialized) return;
|
||||
try {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
} catch (_) {
|
||||
// 极端情况下 SharedPreferences 不可用:退化为内存态,不影响启动与登录。
|
||||
_prefs = null;
|
||||
} finally {
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 账号密码 ──────────────────────────────────────────
|
||||
|
||||
/// 是否记住密码。
|
||||
bool get rememberPassword => _prefs?.getBool(_kRemember) ?? false;
|
||||
|
||||
/// 保存的账号(rememberPassword=true 时有值)。
|
||||
String? get savedAccount => _prefs?.getString(_kAccount);
|
||||
|
||||
/// 保存的密码(rememberPassword=true 时有值)。
|
||||
String? get savedPassword => _prefs?.getString(_kPassword);
|
||||
|
||||
/// 保存账号密码(用户勾选「记住密码」并登录成功后调用)。
|
||||
Future<void> saveCredentials({
|
||||
required String account,
|
||||
required String password,
|
||||
}) async {
|
||||
final prefs = _prefs;
|
||||
if (prefs == null) return;
|
||||
await prefs.setString(_kAccount, account);
|
||||
await prefs.setString(_kPassword, password);
|
||||
await prefs.setBool(_kRemember, true);
|
||||
}
|
||||
|
||||
/// 清除保存的账号密码(用户未勾选「记住密码」时调用)。
|
||||
Future<void> clearCredentials() async {
|
||||
final prefs = _prefs;
|
||||
if (prefs == null) return;
|
||||
await prefs.remove(_kAccount);
|
||||
await prefs.remove(_kPassword);
|
||||
await prefs.setBool(_kRemember, false);
|
||||
}
|
||||
|
||||
// ── Token & 用户信息(用于免登录恢复) ──────────────────
|
||||
|
||||
/// 保存的 token。
|
||||
String? get token => _prefs?.getString(_kToken);
|
||||
|
||||
/// 保存的 refreshToken。
|
||||
String? get refreshToken => _prefs?.getString(_kRefreshToken);
|
||||
|
||||
/// 保存的用户信息 JSON 字符串。
|
||||
String? get userJson => _prefs?.getString(_kUserJson);
|
||||
|
||||
/// 保存登录态(token + refreshToken + 用户信息)。
|
||||
Future<void> saveSession({
|
||||
required String token,
|
||||
String? refreshToken,
|
||||
String? userJson,
|
||||
}) async {
|
||||
final prefs = _prefs;
|
||||
if (prefs == null) return;
|
||||
await prefs.setString(_kToken, token);
|
||||
if (refreshToken != null && refreshToken.isNotEmpty) {
|
||||
await prefs.setString(_kRefreshToken, refreshToken);
|
||||
}
|
||||
if (userJson != null && userJson.isNotEmpty) {
|
||||
await prefs.setString(_kUserJson, userJson);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除登录态(登出或会话失效时调用)。
|
||||
Future<void> clearSession() async {
|
||||
final prefs = _prefs;
|
||||
if (prefs == null) return;
|
||||
await prefs.remove(_kToken);
|
||||
await prefs.remove(_kRefreshToken);
|
||||
await prefs.remove(_kUserJson);
|
||||
}
|
||||
|
||||
/// 是否有已保存的登录态。
|
||||
bool get hasSession => (token ?? '').isNotEmpty;
|
||||
|
||||
/// 从 JSON 恢复用户信息 Map。
|
||||
Map<String, dynamic>? get savedUserMap {
|
||||
final json = userJson;
|
||||
if (json == null || json.isEmpty) return null;
|
||||
try {
|
||||
return jsonDecode(json) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// key 常量(加前缀避免与 StoreLocalCache 等业务 key 冲突)。
|
||||
static const String _kRemember = 'cred_remember';
|
||||
static const String _kAccount = 'cred_account';
|
||||
static const String _kPassword = 'cred_password';
|
||||
static const String _kToken = 'cred_token';
|
||||
static const String _kRefreshToken = 'cred_refreshToken';
|
||||
static const String _kUserJson = 'cred_userJson';
|
||||
}
|
||||
59
lib/utils/formatters.dart
Normal file
59
lib/utils/formatters.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// 通用格式化工具。
|
||||
class Formatters {
|
||||
Formatters._();
|
||||
|
||||
static final DateFormat _dateTimeFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||
static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
/// 格式化日期时间。
|
||||
static String formatDateTime(DateTime value) {
|
||||
return _dateTimeFormat.format(value);
|
||||
}
|
||||
|
||||
/// 仅格式化日期(YYYY-MM-DD),用于门店到期时间等只需日期的场景。
|
||||
static String formatDate(DateTime value) {
|
||||
return _dateFormat.format(value);
|
||||
}
|
||||
|
||||
/// 将百分比值格式化为带一位小数的字符串,如 `72.5%`。
|
||||
static String formatPercent(double ratio) {
|
||||
return '${(ratio * 100).toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
/// 将字节数格式化为可读单位(KB/MB/GB/TB)。
|
||||
static String formatBytes(int bytes) {
|
||||
if (bytes < 1024) {
|
||||
return '$bytes B';
|
||||
}
|
||||
const units = ['KB', 'MB', 'GB', 'TB'];
|
||||
double value = bytes.toDouble();
|
||||
int unitIndex = -1;
|
||||
do {
|
||||
value /= 1024;
|
||||
unitIndex++;
|
||||
} while (value >= 1024 && unitIndex < units.length - 1);
|
||||
return '${value.toStringAsFixed(1)} ${units[unitIndex]}';
|
||||
}
|
||||
|
||||
/// 将秒数格式化为运行时长字符串,如 `3天 5小时 12分`。
|
||||
static String formatUptime(int seconds) {
|
||||
final duration = Duration(seconds: seconds);
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours % 24;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
|
||||
final parts = <String>[];
|
||||
if (days > 0) {
|
||||
parts.add('$days天');
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.add('$hours小时');
|
||||
}
|
||||
if (minutes > 0 || parts.isEmpty) {
|
||||
parts.add('$minutes分');
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
}
|
||||
666
lib/utils/i10n.dart
Normal file
666
lib/utils/i10n.dart
Normal file
@@ -0,0 +1,666 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../providers/locale_provider.dart';
|
||||
|
||||
/// 轻量级国际化助手。
|
||||
///
|
||||
/// 覆盖底部导航、首页、设备、门店、管理、设置、激活码、用户、公司介绍等
|
||||
/// 高频界面文案。后续业务模块继续在此补充。
|
||||
///
|
||||
/// 用法:
|
||||
/// ```dart
|
||||
/// final t = AppLocalizations.of(context);
|
||||
/// Text(t.home);
|
||||
/// ```
|
||||
class AppLocalizations {
|
||||
AppLocalizations(this._locale);
|
||||
|
||||
final AppLocale _locale;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
final locale = Localizations.of<AppLocale>(context, AppLocale);
|
||||
return AppLocalizations(locale ?? AppLocale.zh);
|
||||
}
|
||||
|
||||
String _pick(String zh, String en) => _locale == AppLocale.en ? en : zh;
|
||||
|
||||
/// 当前是否为英文模式。
|
||||
bool get isEnglish => _locale == AppLocale.en;
|
||||
|
||||
// ============ 底部导航 ============
|
||||
String get home => _pick('首页', 'Home');
|
||||
String get devices => _pick('设备', 'Devices');
|
||||
String get stores => _pick('门店', 'Stores');
|
||||
String get admin => _pick('管理', 'Admin');
|
||||
String get profile => _pick('我的', 'Profile');
|
||||
|
||||
// ============ 设置 ============
|
||||
String get orientationMode => _pick('显示方向', 'Display Orientation');
|
||||
String get orientationSystem => _pick('跟随系统', 'System');
|
||||
String get orientationPortrait => _pick('竖屏', 'Portrait');
|
||||
String get orientationLandscape => _pick('横屏', 'Landscape');
|
||||
|
||||
// ============ 首页 ============
|
||||
/// AppBar / 顶部栏大标题。
|
||||
String get appTitle => _pick('云定制主页运营平台', '云定制主页运营平台');
|
||||
|
||||
/// 首页品牌横幅 / 登录页主标题。
|
||||
String get appHeroTitle => _pick('云定制主页运营助手', '云定制主页运营助手');
|
||||
|
||||
/// 登录页副标题。
|
||||
String get appSubtitle => _pick('Mobile Management', 'Mobile Management');
|
||||
String get totalDevices => _pick('设备总数', 'Total Devices');
|
||||
String get totalStores => _pick('门店总数', 'Total Stores');
|
||||
String get onlineDevices => _pick('在线设备', 'Online Devices');
|
||||
String get offlineDevices => _pick('离线设备', 'Offline Devices');
|
||||
String get quickActions => _pick('快捷操作', 'Quick Actions');
|
||||
String get systemManagement => _pick('系统管理', 'System Management');
|
||||
String get mediaManagement => _pick('媒资管理', 'Media Management');
|
||||
|
||||
// ============ 通用 ============
|
||||
String get loading => _pick('加载中…', 'Loading…');
|
||||
String get retry => _pick('重试', 'Retry');
|
||||
String get save => _pick('保存', 'Save');
|
||||
String get cancel => _pick('取消', 'Cancel');
|
||||
String get confirm => _pick('确认', 'Confirm');
|
||||
String get reset => _pick('重置', 'Reset');
|
||||
String get edit => _pick('编辑', 'Edit');
|
||||
String get delete => _pick('删除', 'Delete');
|
||||
String get add => _pick('新增', 'Add');
|
||||
String get search => _pick('搜索', 'Search');
|
||||
String get back => _pick('返回', 'Back');
|
||||
String get yes => _pick('是', 'Yes');
|
||||
String get no => _pick('否', 'No');
|
||||
String get unknown => _pick('未知', 'Unknown');
|
||||
String get none => _pick('无', 'None');
|
||||
String get notGrouped => _pick('未分组', 'Not Grouped');
|
||||
String get all => _pick('全部', 'All');
|
||||
String get noData => _pick('暂无数据', 'No data');
|
||||
String get noMoreData => _pick('没有更多数据', 'No more data');
|
||||
String get saved => _pick('保存成功', 'Saved');
|
||||
String get saveFailed => _pick('保存失败', 'Save failed');
|
||||
String get deleted => _pick('删除成功', 'Deleted');
|
||||
String get deleteFailed => _pick('删除失败', 'Delete failed');
|
||||
String get copied => _pick('已复制', 'Copied');
|
||||
String get pleaseSelect => _pick('请选择', 'Please select');
|
||||
|
||||
// ============ 设置页 ============
|
||||
String get settings => _pick('设置', 'Settings');
|
||||
String get themeMode => _pick('主题模式', 'Theme');
|
||||
String get language => _pick('语言', 'Language');
|
||||
String get changePassword => _pick('修改密码', 'Change Password');
|
||||
String get about => _pick('关于', 'About');
|
||||
String get logout => _pick('退出登录', 'Log Out');
|
||||
String get account => _pick('账号', 'Account');
|
||||
String get themeDark => _pick('深色', 'Dark');
|
||||
String get themeLight => _pick('浅色', 'Light');
|
||||
String get themeSystem => _pick('跟随系统', 'System');
|
||||
String get passwordDialogTitle => _pick('修改登录密码', 'Change Login Password');
|
||||
String get oldPassword => _pick('原密码', 'Old Password');
|
||||
String get newPassword => _pick('新密码', 'New Password');
|
||||
String get confirmNewPassword => _pick('确认新密码', 'Confirm New Password');
|
||||
String get pleaseFillPassword =>
|
||||
_pick('请填写原密码和新密码', 'Please fill in old and new passwords');
|
||||
String get passwordsDoNotMatch =>
|
||||
_pick('两次输入的新密码不一致', 'New passwords do not match');
|
||||
String get passwordChangeSuccess =>
|
||||
_pick('密码修改成功,请重新登录', 'Password changed, please log in again');
|
||||
String passwordChangeFailed(String error) =>
|
||||
_pick('修改失败:$error', 'Change failed: $error');
|
||||
|
||||
// ============ 管理页菜单 ============
|
||||
String get deviceManagement => _pick('设备管理', 'Device Management');
|
||||
String get activationCode => _pick('激活码', 'Activation Code');
|
||||
String get deviceLimit => _pick('设备上限', 'Device Limit');
|
||||
String get units => _pick('台', 'Units');
|
||||
String get storeConfig => _pick('门店配置', 'Store Config');
|
||||
String get userManagement => _pick('用户管理', 'User Management');
|
||||
String get companyIntro => _pick('公司介绍', 'Company Intro');
|
||||
|
||||
// ============ 设备管理 ============
|
||||
String get deviceList => _pick('设备列表', 'Device List');
|
||||
String get deviceDetails => _pick('设备详情', 'Device Details');
|
||||
String get searchDeviceHint => _pick('搜索设备MAC / 房号', 'Search MAC / room number');
|
||||
String get searchModelHint => _pick('型号', 'Model');
|
||||
String get scanQueryDevice => _pick('扫码查询设备', 'Scan to Query');
|
||||
String get scanActivateDevice => _pick('扫码激活新设备', 'Scan to Activate');
|
||||
String get scanBindDevice => _pick('扫码绑定设备', 'Scan to Bind');
|
||||
String get scanTitleActivate => _pick('扫码激活新设备', 'Scan to Activate Device');
|
||||
String get scanTitleBind => _pick('扫码绑定设备', 'Scan to Bind Device');
|
||||
String get scanTitleQuery => _pick('扫码查询设备', 'Scan to Query Device');
|
||||
String get scanHintActivate => _pick('将新设备二维码放入框内以激活',
|
||||
'Place the new device QR code in the frame to activate');
|
||||
String get scanHintBind => _pick('将二维码放入框内以绑定设备',
|
||||
'Place the QR code in the frame to bind the device');
|
||||
String get scanHintQuery => _pick('将设备二维码放入框内以查询',
|
||||
'Place the device QR code in the frame to query');
|
||||
String get cameraPermissionDenied => _pick('未获得相机权限', 'Camera permission denied');
|
||||
String get cameraPermissionDeniedHint =>
|
||||
_pick('请在系统设置中开启后重试',
|
||||
'Please enable camera permission in system settings and try again');
|
||||
String get cameraInitializing => _pick('正在初始化相机…', 'Initializing camera…');
|
||||
String get cameraUnavailable => _pick('相机不可用', 'Camera unavailable');
|
||||
String cameraOpenFailed(String code) =>
|
||||
_pick('相机打开失败:$code', 'Failed to open camera: $code');
|
||||
String get scanConfirmActivateTitle => _pick('确认激活设备', 'Confirm Device Activation');
|
||||
String get scanConfirmBindTitle => _pick('确认绑定', 'Confirm Binding');
|
||||
String scanConfirmContent(String storeName, String groupId, String payload) =>
|
||||
_pick(
|
||||
'将设备绑定到门店「$storeName」'
|
||||
'${groupId.isNotEmpty ? ' / 分组 $groupId' : ''}?\n\n'
|
||||
'二维码内容:$payload',
|
||||
'Bind device to store "$storeName"'
|
||||
'${groupId.isNotEmpty ? ' / Group $groupId' : ''}?\n\n'
|
||||
'QR code content: $payload',
|
||||
);
|
||||
String get scanActivate => _pick('激活', 'Activate');
|
||||
String get scanBind => _pick('绑定', 'Bind');
|
||||
String get scanDeviceNotFound => _pick('未找到设备,请确认二维码内容',
|
||||
'Device not found, please check the QR code content');
|
||||
String get scanPayloadUnrecognized => _pick(
|
||||
'无法识别该二维码,请扫描设备二维码或确认二维码内容',
|
||||
'Unrecognized QR code. Please scan a device QR code');
|
||||
String scanDeviceNotReady(String identifier) => _pick(
|
||||
'未找到对应设备($identifier)。设备可能尚未联网或同步,请确认设备已开机联网、已激活并归属当前账号后重试',
|
||||
'No matching device found ($identifier). The device may not be online or synced yet. Please confirm it is powered on, connected, activated and assigned to this account, then retry');
|
||||
String scanQueryFailed(String error) =>
|
||||
_pick('查询失败:$error', 'Query failed: $error');
|
||||
String scanLoadStoresFailed(String error) =>
|
||||
_pick('加载门店失败:$error', 'Failed to load stores: $error');
|
||||
String get scanNoStores => _pick('暂无可选门店,请先在门店管理中创建',
|
||||
'No stores available. Please create one in Store Management first');
|
||||
String get scanSelectStoreTitle => _pick('选择目标门店', 'Select Target Store');
|
||||
String get scanActivateSuccess => _pick('设备已激活', 'Device activated');
|
||||
String get scanBindSuccess => _pick('设备已绑定', 'Device bound');
|
||||
String get scanPayloadNoMac =>
|
||||
_pick('未能从二维码中识别到 MAC 地址,请检查二维码内容',
|
||||
'Could not find MAC address in QR code');
|
||||
String scanLoadGroupsFailed(String error) =>
|
||||
_pick('加载分组失败:$error', 'Failed to load groups: $error');
|
||||
String scanNoGroups(String storeName) =>
|
||||
_pick('门店「$storeName」暂无设备分组,请先在设备分组中创建',
|
||||
'Store "$storeName" has no device groups. Please create one first');
|
||||
String get scanSelectGroupTitle => _pick('选择目标分组', 'Select Target Group');
|
||||
String scanBindFailed(String error) =>
|
||||
_pick('绑定失败:$error', 'Binding failed: $error');
|
||||
// ============ 扫码绑定表单(页面内表单) ============
|
||||
String get scanGroupLabel => _pick('门店分组', 'Store Group');
|
||||
String get scanStoreRequired => _pick('请先选择门店', 'Please select a store first');
|
||||
String get scanGroupRequired => _pick('请选择门店分组', 'Please select a store group');
|
||||
String get scanRoomRequired => _pick('请输入房号', 'Please enter room number');
|
||||
String get scanRoomInvalid => _pick(
|
||||
'房号格式不正确(仅限中英文、数字、连字符、斜杠,长度 1-20)',
|
||||
'Invalid room number (letters/digits/hyphen/slash only, 1-20 chars)');
|
||||
// 房号改为选填:标签与提示都标明「选填」,避免用户误以为必填。
|
||||
String get roomNoOptional => _pick('房号(选填)', 'Room Number (optional)');
|
||||
String get scanRoomHint => _pick('选填,如 801、A101、3楼', 'Optional, e.g. 801, A101, 3rd floor');
|
||||
String get scanRescan => _pick('重新扫码', 'Scan Again');
|
||||
String get scanFormInvalid =>
|
||||
_pick('请完善门店与门店分组后再提交', 'Please complete store and store group');
|
||||
String get scanFormDeviceTitle => _pick('已识别设备', 'Recognized Device');
|
||||
String get sendCommand => _pick('下发指令', 'Send Command');
|
||||
String get editGroup => _pick('修改分组', 'Edit Group');
|
||||
String get macAddress => _pick('MAC 地址', 'MAC Address');
|
||||
String get ipAddress => _pick('IP 地址', 'IP Address');
|
||||
String get belongStore => _pick('所属门店', 'Store');
|
||||
String get belongGroup => _pick('所属分组', 'Group');
|
||||
String get roomNo => _pick('房号', 'Room No.');
|
||||
String deviceModel(String model) => _pick('型号:$model', 'Model: $model');
|
||||
String deviceSubtitleStore(String name) => _pick('门店:$name', 'Store: $name');
|
||||
String deviceSubtitleMac(String mac) => _pick('MAC:$mac', 'MAC: $mac');
|
||||
String deviceSubtitleRoom(String room) => _pick('房号:$room', 'Room: $room');
|
||||
String get deviceSubtitleNotSet => _pick('未设置', 'Not set');
|
||||
String get location => _pick('位置', 'Location');
|
||||
String get operatingSystem => _pick('操作系统', 'OS');
|
||||
String get uptime => _pick('运行时长', 'Uptime');
|
||||
// ============ 设备详情区块 ============
|
||||
String get basicInfo => _pick('基本信息', 'Basic Info');
|
||||
String get configParams => _pick('配置参数', 'Config Params');
|
||||
String get networkInfo => _pick('网络信息', 'Network Info');
|
||||
String get runtimeData => _pick('运行时数据', 'Runtime Data');
|
||||
String get deviceName => _pick('设备名称', 'Device Name');
|
||||
String get deviceId => _pick('设备 ID', 'Device ID');
|
||||
String get modelLabel => _pick('型号', 'Model');
|
||||
String get deviceSnLabel => _pick('设备序列号', 'Device SN');
|
||||
String get deviceUidLabel => _pick('设备 UID', 'Device UID');
|
||||
String get lastActiveTime => _pick('最后活跃时间', 'Last Active Time');
|
||||
String get minutesAgo => _pick('分钟前', 'min ago');
|
||||
String get cpuUsage => _pick('CPU 使用率', 'CPU Usage');
|
||||
String get memoryUsage => _pick('内存使用率', 'Memory Usage');
|
||||
String get diskUsage => _pick('磁盘使用率', 'Disk Usage');
|
||||
String get networkRxTx => _pick('网络(收/发)', 'Network (Rx/Tx)');
|
||||
String get showRuntimeMetrics => _pick('显示运行指标', 'Show Runtime Metrics');
|
||||
String get showRuntimeMetricsHint =>
|
||||
_pick('开启后在设备详情页展示 CPU、内存、磁盘、网络指标',
|
||||
'Show CPU, memory, disk and network metrics on device detail');
|
||||
String deviceListLoadFailed(String error) =>
|
||||
_pick('设备列表加载失败:$error', 'Failed to load device list: $error');
|
||||
String get noMatchedDevices => _pick('没有匹配的设备', 'No matched devices');
|
||||
String get allStores => _pick('全部门店', 'All Stores');
|
||||
String get selectStore => _pick('选择门店', 'Select Store');
|
||||
String get groupId => _pick('所属分组 ID', 'Group ID');
|
||||
String get groupIdHint => _pick('填写设备分组标识,如 g-core',
|
||||
'Enter device group identifier, e.g. g-core');
|
||||
String get saveSuccess => _pick('已保存', 'Saved successfully');
|
||||
String get groupListLoading => _pick('加载分组列表…', 'Loading groups…');
|
||||
|
||||
// ============ 设备状态 ============
|
||||
String get statusOnline => _pick('在线', 'Online');
|
||||
String get statusOffline => _pick('离线', 'Offline');
|
||||
String get statusWarning => _pick('告警', 'Warning');
|
||||
String get statusNormal => _pick('正常', 'Normal');
|
||||
String get statusDisabled => _pick('禁用', 'Disabled');
|
||||
|
||||
String deviceStatusLabel(DeviceStatusKey status) {
|
||||
switch (status) {
|
||||
case DeviceStatusKey.online:
|
||||
return statusOnline;
|
||||
case DeviceStatusKey.offline:
|
||||
return statusOffline;
|
||||
case DeviceStatusKey.warning:
|
||||
return statusWarning;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 指令下发 ============
|
||||
String get sendCommandTitle => _pick('下发指令', 'Send Command');
|
||||
String get sendOrgCommandTitle => _pick('下发门店指令', 'Send Store Command');
|
||||
String get commandType => _pick('指令类型', 'Command Type');
|
||||
String get commandConfirmTitle => _pick('确认下发', 'Confirm Send');
|
||||
String commandConfirmContent(String label) =>
|
||||
_pick('确定要向该设备下发「$label」指令吗?',
|
||||
'Are you sure you want to send the "$label" command?');
|
||||
String commandSent(String target, String label) =>
|
||||
_pick('已向$target下发「$label」指令(后端已接收,不代表设备已执行)',
|
||||
'Command "$label" sent to $target (accepted by server, not yet confirmed by device)');
|
||||
String get commandSendFailed => _pick('指令下发失败', 'Failed to send command');
|
||||
String get pleaseInput => _pick('请输入', 'Please enter');
|
||||
String get volumeRangeHint => _pick('请输入 0~100 的整数音量',
|
||||
'Please enter an integer volume between 0 and 100');
|
||||
String get maxVolume => _pick('最大音量', 'Max Volume');
|
||||
String get commandOfflineWarning =>
|
||||
_pick('该设备当前离线(或心跳已超时),指令可能不会立即生效;设备重新上报心跳后才会执行。',
|
||||
'This device is offline (or its heartbeat timed out). The command may not take effect immediately; it will run after the device reconnects.');
|
||||
String get commandOfflineConfirm => _pick('仍然下发', 'Send Anyway');
|
||||
String get commandMacMissing =>
|
||||
_pick('该设备缺少 MAC 地址,无法按 MAC 下发指令,请改用门店批量下发。',
|
||||
'This device has no MAC address, so a MAC-targeted command cannot be sent. Use store-level delivery instead.');
|
||||
|
||||
String deviceCommandLabel(String cmd) {
|
||||
return _pick(
|
||||
const {
|
||||
'powerOff': '关机',
|
||||
'reboot': '重启',
|
||||
'screenshot': '截屏',
|
||||
'themeUpdate': '主题更新',
|
||||
'versionUpdate': '版本更新',
|
||||
'marqueeUpdate': '滚动字幕更新',
|
||||
'weatherUpdate': '更新天气信息',
|
||||
'mediaZoneVol': '媒体区域音量',
|
||||
'defaultVol': '默认音量',
|
||||
'themeClear': '清除主题缓存',
|
||||
'cacheClear': '清除缓存',
|
||||
'storeExpire': '门店到期',
|
||||
'storeDisable': '门店禁用',
|
||||
'passwordUpdate': '修改设置密码',
|
||||
'worldAd': '获取世界广告',
|
||||
}[cmd] ??
|
||||
cmd,
|
||||
const {
|
||||
'powerOff': 'Power Off',
|
||||
'reboot': 'Reboot',
|
||||
'screenshot': 'Screenshot',
|
||||
'themeUpdate': 'Theme Update',
|
||||
'versionUpdate': 'Version Update',
|
||||
'marqueeUpdate': 'Marquee Update',
|
||||
'weatherUpdate': 'Update Weather',
|
||||
'mediaZoneVol': 'Media Zone Volume',
|
||||
'defaultVol': 'Default Volume',
|
||||
'themeClear': 'Clear Theme Cache',
|
||||
'cacheClear': 'Clear Cache',
|
||||
'storeExpire': 'Store Expired',
|
||||
'storeDisable': 'Store Disabled',
|
||||
'passwordUpdate': 'Change Settings Password',
|
||||
'worldAd': 'Fetch World Ads',
|
||||
}[cmd] ??
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
||||
String deviceCommandInputLabel(String cmd) {
|
||||
if (cmd == 'passwordUpdate') return _pick('新密码', 'New Password');
|
||||
return _pick('音量(0-100)', 'Volume (0-100)');
|
||||
}
|
||||
|
||||
// ============ 门店管理 ============
|
||||
String get storeManagement => _pick('门店管理', 'Store Management');
|
||||
String get storeCommands => _pick('门店指令', 'Store Commands');
|
||||
String get searchStoreHint => _pick('搜索门店名称', 'Search store name');
|
||||
String get storeDeleteConfirmTitle => _pick('删除门店', 'Delete Store');
|
||||
String storeDeleteConfirmContent(String name) =>
|
||||
_pick('确定要删除门店「$name」吗?', 'Are you sure you want to delete store "$name"?');
|
||||
String get storeDeleted => _pick('门店已删除', 'Store deleted');
|
||||
String get loadFailed => _pick('加载失败', 'Load failed');
|
||||
String get storeId => _pick('门店ID', 'Store ID');
|
||||
String get address => _pick('地址', 'Address');
|
||||
String get contactPhone => _pick('联系电话', 'Contact Phone');
|
||||
String get expireTime => _pick('到期时间', 'Expire Time');
|
||||
String get expired => _pick('已过期', 'Expired');
|
||||
String get noExpireTime => _pick('无到期时间', 'No expire date');
|
||||
String get createTime => _pick('创建时间', 'Create Time');
|
||||
String get storeName => _pick('门店名称', 'Store Name');
|
||||
String get storeNameRequired => _pick('请输入门店名称', 'Please enter store name');
|
||||
String get region => _pick('所在地区', 'Region');
|
||||
String get regionHint => _pick('请选择省 / 市 / 区', 'Please select province / city / district');
|
||||
String get addressDetail => _pick('详细地址', 'Detailed Address');
|
||||
String get addressDetailHint => _pick('街道 / 门牌号等', 'Street / house number');
|
||||
String get interfaceConfig => _pick('界面配置', 'Interface Config');
|
||||
String get themeSkin => _pick('主题皮肤', 'Theme Skin');
|
||||
String get themeCurrent => _pick('当前', 'Current');
|
||||
String get themeUnnamed => _pick('未命名主题', 'Unnamed Theme');
|
||||
String get noInterfaceConfig => _pick('当前门店暂无设备分组主题',
|
||||
'No device group themes for this store');
|
||||
String get editStore => _pick('编辑门店', 'Edit Store');
|
||||
String get addStore => _pick('新增门店', 'Add Store');
|
||||
String get create => _pick('创建', 'Create');
|
||||
String get sampleStore => _pick('示例门店', 'Sample Store');
|
||||
String get parentOrg => _pick('所属组织', 'Parent Organization');
|
||||
String get parentOrgRequired => _pick('请选择所属组织', 'Please select parent organization');
|
||||
String get regionRequired => _pick('请选择所在地区', 'Please select region');
|
||||
String get storeUpdatedPlaceholder => _pick('门店已更新(本地占位)',
|
||||
'Store updated (local placeholder)');
|
||||
String get storeCreatedPlaceholder => _pick('门店已创建(本地占位)',
|
||||
'Store created (local placeholder)');
|
||||
|
||||
// ============ 激活码管理 ============
|
||||
String get activationCodeManagement => _pick('激活码管理', 'Activation Code Management');
|
||||
String get noStoreAvailable => _pick('暂无可选门店', 'No stores available');
|
||||
String get pleaseSelectStoreFirst => _pick('请先选择门店', 'Please select a store first');
|
||||
String get currentActivationCode => _pick('当前激活码', 'Current Activation Code');
|
||||
String get noActivationCode => _pick('暂无激活码信息', 'No activation code info');
|
||||
String get generateActivationCode => _pick('生成激活码', 'Generate Activation Code');
|
||||
String get resetActivationCode => _pick('重置激活码', 'Reset Activation Code');
|
||||
String get resetActivationCodeConfirm => _pick('重置后原激活码将失效,确定要继续吗?',
|
||||
'The original code will become invalid after reset. Continue?');
|
||||
String get activationCodeGenerated => _pick('激活码已生成', 'Activation code generated');
|
||||
String get activationCodeReset => _pick('激活码已重置', 'Activation code reset');
|
||||
String get generateFailed => _pick('生成失败', 'Generate failed');
|
||||
String get resetFailed => _pick('重置失败', 'Reset failed');
|
||||
String get activationCodeResetSuccess =>
|
||||
_pick('激活码已重置', 'Activation code reset');
|
||||
String activationCodeResetFailed(String error) =>
|
||||
_pick('激活码重置失败:$error', 'Activation code reset failed: $error');
|
||||
String get maxActivateCount => _pick('最大激活数', 'Max Activations');
|
||||
String get activatedCount => _pick('已激活数', 'Activated Count');
|
||||
String get copy => _pick('复制', 'Copy');
|
||||
String get generate => _pick('生成', 'Generate');
|
||||
String get activationCodeCopied => _pick('激活码已复制', 'Activation code copied');
|
||||
String get generateActivationCodeHint => _pick(
|
||||
'将为当前门店生成新的激活码,原有激活码将被替换。',
|
||||
'A new activation code will be generated for this store, replacing the existing one.');
|
||||
String activationCodeLoadFailed(String error) =>
|
||||
_pick('激活码加载失败:$error', 'Failed to load activation code: $error');
|
||||
String generateFailedWithError(String error) =>
|
||||
_pick('生成失败:$error', 'Generate failed: $error');
|
||||
String get activationCodeNoCodeHint => _pick('无激活码,请点击「生成激活码」',
|
||||
'No activation code, please tap "Generate Activation Code"');
|
||||
String get maxActivationCountLabel => _pick('最大激活次数', 'Max Activation Count');
|
||||
String get maxActivationCountHint => _pick('该激活码最多可激活的设备数量',
|
||||
'Maximum number of devices this code can activate');
|
||||
String get positiveIntegerRequired => _pick('请输入大于 0 的整数',
|
||||
'Please enter an integer greater than 0');
|
||||
|
||||
// 门店表单中的激活码/终端数
|
||||
String get terminalCountLabel => _pick('终端数', 'Terminal Count');
|
||||
String get terminalCountHint => _pick('该门店允许激活的终端数量', 'Number of terminals allowed');
|
||||
String get maxTerminalCount => _pick('最大终端数', 'Max Terminals');
|
||||
String get usedTerminalCount => _pick('已用终端数', 'Used Terminals');
|
||||
String get createActivationCode => _pick('创建激活码', 'Create Activation Code');
|
||||
String get activationCodeWillAutoGenerate => _pick('保存门店后将自动生成激活码',
|
||||
'Activation code will be auto-generated after saving the store');
|
||||
String get activeActivationCode => _pick('当前激活码', 'Current Activation Code');
|
||||
String get noActiveActivationCode => _pick('暂无正在使用的激活码',
|
||||
'No activation code currently in use');
|
||||
String get activationCodeGenerateHint => _pick('每次点击仅生成一个对应激活码',
|
||||
'Each tap generates one activation code');
|
||||
String storeSavedActivationFailed(String error) =>
|
||||
_pick('门店已保存,但激活码生成失败:$error',
|
||||
'Store saved, but activation code generation failed: $error');
|
||||
String storeListLoadFailed(String error) =>
|
||||
_pick('门店列表加载失败:$error', 'Failed to load store list: $error');
|
||||
|
||||
// ============ 用户管理 ============
|
||||
String get username => _pick('用户名', 'Username');
|
||||
String get usernameRequired => _pick('请输入用户名', 'Please enter username');
|
||||
String get email => _pick('邮箱', 'Email');
|
||||
String get mobile => _pick('手机号', 'Mobile');
|
||||
String get password => _pick('密码', 'Password');
|
||||
String get passwordEditHint => _pick('不修改请留空', 'Leave blank to keep unchanged');
|
||||
String get passwordRequired => _pick('请输入密码', 'Please enter password');
|
||||
String get confirmPassword => _pick('确认密码', 'Confirm Password');
|
||||
String get confirmPasswordHint => _pick('请再次输入密码', 'Please enter the password again');
|
||||
String get confirmPasswordRequired => _pick('请输入确认密码', 'Please enter confirm password');
|
||||
String get role => _pick('角色', 'Role');
|
||||
String get roleRequired => _pick('请选择角色', 'Please select a role');
|
||||
String get org => _pick('所属组织', 'Organization');
|
||||
String get selectOrg => _pick('请选择所属组织', 'Please select an organization');
|
||||
String get noOrgHint => _pick('暂无可选组织,保留原归属',
|
||||
'No organizations available, keep original');
|
||||
String get status => _pick('状态', 'Status');
|
||||
String get unassigned => _pick('未分配', 'Unassigned');
|
||||
String get searchUsernameHint => _pick('搜索用户名', 'Search username');
|
||||
String get searchOrgHint => _pick('搜索组织名称', 'Search organization name');
|
||||
String get noMatchingOrg => _pick('未找到匹配组织', 'No matching organization');
|
||||
String get roleLabel => _pick('角色', 'Role');
|
||||
String get orgLabel => _pick('所属组织', 'Organization');
|
||||
String get currentAccountInfo => _pick('当前账号信息', 'Current Account Info');
|
||||
String get passwordNotSaved => _pick('未保存', 'Not saved');
|
||||
String get noSubordinateAccount =>
|
||||
_pick('当前账号暂无下级账号', 'No subordinate accounts');
|
||||
String get showPassword => _pick('显示密码', 'Show password');
|
||||
String get hidePassword => _pick('隐藏密码', 'Hide password');
|
||||
String get editUser => _pick('编辑用户', 'Edit User');
|
||||
String get addUser => _pick('新增用户', 'Add User');
|
||||
String get deleteUserConfirmTitle => _pick('确认删除', 'Confirm Delete');
|
||||
String deleteUserConfirmContent(String name) =>
|
||||
_pick('确定要删除用户「$name」吗?', 'Are you sure you want to delete user "$name"?');
|
||||
String get rolesLoadFailed => _pick('角色列表加载失败', 'Failed to load roles');
|
||||
|
||||
// ============ 滚动字幕管理 ============
|
||||
String get marqueeManagement => _pick('滚动字幕管理', 'Scrolling Subtitle Management');
|
||||
String get addMarquee => _pick('新增滚动字幕', 'Add Scrolling Subtitle');
|
||||
String get editMarquee => _pick('编辑滚动字幕', 'Edit Scrolling Subtitle');
|
||||
String get marqueeContent => _pick('字幕内容', 'Subtitle Content');
|
||||
String get marqueeContentRequired => _pick('请输入字幕内容', 'Please enter subtitle content');
|
||||
String get marqueeSpeed => _pick('滚动速度', 'Scroll Speed');
|
||||
String get marqueeSpeedHint => _pick('像素/秒,建议 30~120', 'pixels/sec, recommended 30-120');
|
||||
String get marqueeSpeedInvalid => _pick('请选择滚动速度', 'Please select scroll speed');
|
||||
String get marqueeFontSizeInvalid => _pick('请输入大于 0 的字体大小', 'Please enter a font size greater than 0');
|
||||
String get marqueeSpeedSlow => _pick('慢', 'Slow');
|
||||
String get marqueeSpeedSlower => _pick('较慢', 'Slower');
|
||||
String get marqueeSpeedMedium => _pick('中', 'Medium');
|
||||
String get marqueeSpeedFaster => _pick('较快', 'Faster');
|
||||
String get marqueeSpeedFast => _pick('快', 'Fast');
|
||||
String get marqueeGradeDanger => _pick('危险', 'Danger');
|
||||
String get marqueeGradeUrgent => _pick('紧急', 'Urgent');
|
||||
String get marqueeGradeNormal => _pick('普通', 'Normal');
|
||||
String get targetStore => _pick('目标门店', 'Target Store');
|
||||
String get filterByStore => _pick('按门店筛选', 'Filter by Store');
|
||||
String get searchMarqueeHint => _pick('搜索字幕内容或门店', 'Search subtitle content or store');
|
||||
String get noMarqueeData => _pick('暂无滚动字幕', 'No scrolling subtitles');
|
||||
String get marqueeStatus => _pick('状态', 'Status');
|
||||
String get marqueeStatusEnabled => _pick('启用', 'Enabled');
|
||||
String get marqueeStatusDisabled => _pick('禁用', 'Disabled');
|
||||
String get marqueeStatusPending => _pick('待下发', 'Pending');
|
||||
String get marqueeStatusDispatched => _pick('已下发', 'Dispatched');
|
||||
String get dispatch => _pick('下发', 'Dispatch');
|
||||
String get cancelDispatch => _pick('取消下发', 'Cancel Dispatch');
|
||||
String get marqueeDispatched => _pick('字幕已下发', 'Subtitle dispatched');
|
||||
String get marqueeDispatchFailed => _pick('字幕下发失败', 'Failed to dispatch subtitle');
|
||||
String get marqueeDispatchCancelled => _pick('已取消下发', 'Dispatch cancelled');
|
||||
String get marqueeCancelFailed => _pick('取消下发失败', 'Failed to cancel dispatch');
|
||||
String get marqueeFontColor => _pick('字体颜色', 'Font Color');
|
||||
String get marqueeBackgroundColor => _pick('背景颜色', 'Background Color');
|
||||
String get marqueeFontSize => _pick('字体大小', 'Font Size');
|
||||
String get marqueeLevel => _pick('级别', 'Level');
|
||||
String get marqueeStartTime => _pick('开始时间', 'Start Time');
|
||||
String get marqueeEndTime => _pick('结束时间', 'End Time');
|
||||
String get marqueeHour => _pick('时', 'Hour');
|
||||
String get marqueeMinute => _pick('分', 'Minute');
|
||||
String get marqueeSecond => _pick('秒', 'Second');
|
||||
String get marqueeContentPreview => _pick('字幕内容', 'Subtitle Content');
|
||||
|
||||
// ============ 公司介绍 ============
|
||||
String get introCategoryTitle => _pick('公司介绍', 'Company Intro');
|
||||
String get introStoreFilter => _pick('门店名称', 'Store Name');
|
||||
String get introAllStores => _pick('全部门店', 'All Stores');
|
||||
String get introCategoryName => _pick('名称', 'Name');
|
||||
String get introCategoryTypeCode => _pick('类型编码', 'Type Code');
|
||||
String get introCategoryEdit => _pick('编辑大类', 'Edit Category');
|
||||
String get introCategoryAdd => _pick('新增大类', 'Add Category');
|
||||
String get introSubcategoryEdit => _pick('编辑小类', 'Edit Subcategory');
|
||||
String get introSubcategoryAdd => _pick('新增小类', 'Add Subcategory');
|
||||
String get introDetailEdit => _pick('编辑内容', 'Edit Content');
|
||||
String get introDetailAdd => _pick('新增内容', 'Add Content');
|
||||
String get introTitle => _pick('标题', 'Title');
|
||||
String get introTitleRequired => _pick('请输入标题', 'Please enter the title');
|
||||
String get introDescription => _pick('描述', 'Description');
|
||||
String get introMediaType => _pick('媒体类型', 'Media Type');
|
||||
String get introMediaUrl => _pick('媒体地址', 'Media URL');
|
||||
String get introMediaUrlRequired =>
|
||||
_pick('请上传或填写媒体地址', 'Please upload or enter the media URL');
|
||||
String get introImage => _pick('图片', 'Image');
|
||||
String get introVideo => _pick('视频', 'Video');
|
||||
String get introTypeLabel => _pick('类型', 'Type');
|
||||
String get introStoreLabel => _pick('门店', 'Store');
|
||||
String get introFilterStores => _pick('筛选门店', 'Filter Stores');
|
||||
String get introSelectStoresHint => _pick('可多选门店', 'Select multiple stores');
|
||||
String get introCategoryStore => _pick('所属门店', 'Store');
|
||||
String get introStoreRequired => _pick('请选择所属门店', 'Please select a store');
|
||||
String get introCategoryNameRequired =>
|
||||
_pick('请输入名称', 'Please enter the name');
|
||||
String get introCategoryTypeRequired =>
|
||||
_pick('请输入类型编码', 'Please enter the type code');
|
||||
String get introCategoryTypeNoChinese =>
|
||||
_pick('类型编码禁止包含中文', 'Type code must not contain Chinese characters');
|
||||
String get introStoreLoadFailed =>
|
||||
_pick('门店列表加载失败,请下拉刷新后重试', 'Failed to load stores, please refresh and retry');
|
||||
String get introSortOrder => _pick('排序号', 'Sort Order');
|
||||
String get introEnabled => _pick('启用', 'Enabled');
|
||||
String get introDisabled => _pick('禁用', 'Disabled');
|
||||
String get introUploadImage => _pick('上传图片', 'Upload Image');
|
||||
String get introPickImage => _pick('选择图片', 'Pick Image');
|
||||
String get introImagePreview => _pick('图片预览', 'Image Preview');
|
||||
String get introUploading => _pick('上传中…', 'Uploading…');
|
||||
String get introUploadFailed => _pick('图片上传失败', 'Image upload failed');
|
||||
String get introInsertImage => _pick('插入图片', 'Insert Image');
|
||||
String get introSubImageHint =>
|
||||
_pick('图片不超过 300KB,建议分辨率 1920×1080', 'Image ≤ 300KB, suggested 1920×1080');
|
||||
String get introDetailImageHint => _pick('图片不超过 3MB', 'Image ≤ 3MB');
|
||||
String get introDetailVideoHint => _pick('视频不超过 30MB', 'Video ≤ 30MB');
|
||||
String get introImageTypeInvalid =>
|
||||
_pick('请上传 jpg、png、gif 或 webp 图片', 'Please upload jpg, png, gif or webp image');
|
||||
String get introVideoTypeInvalid =>
|
||||
_pick('请上传 mp4、webm、mov 或 m4v 视频', 'Please upload mp4, webm, mov or m4v video');
|
||||
String get introFileTooLarge => _pick('文件大小超过限制', 'File exceeds the size limit');
|
||||
|
||||
// ============ 地区选择器 ============
|
||||
String get selectProvince => _pick('选择省份', 'Select Province');
|
||||
String get selectCity => _pick('选择城市', 'Select City');
|
||||
String get selectDistrict => _pick('选择区县', 'Select District');
|
||||
String get selectRegion => _pick('选择地区', 'Select Region');
|
||||
String get previousLevel => _pick('上一级', 'Previous');
|
||||
|
||||
// ============ 运行时长 ============
|
||||
String formatUptime(int seconds) {
|
||||
final duration = Duration(seconds: seconds);
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours % 24;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
|
||||
final parts = <String>[];
|
||||
if (days > 0) parts.add('$days${_pick('天', 'd')}');
|
||||
if (hours > 0) parts.add('$hours${_pick('小时', 'h')}');
|
||||
if (minutes > 0 || parts.isEmpty) {
|
||||
parts.add('$minutes${_pick('分', 'm')}');
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// ============ 通用状态/操作(后置兼容) ============
|
||||
String get noOrgId => _pick('该门店缺少组织 ID,无法下发指令',
|
||||
'This store lacks an organization ID and cannot receive commands');
|
||||
|
||||
// ============ 通用删除确认 ============
|
||||
String get confirmDelete => _pick('确认删除', 'Confirm Delete');
|
||||
String deleteConfirmContent(String name) =>
|
||||
_pick('确定要删除「$name」吗?', 'Are you sure you want to delete "$name"?');
|
||||
|
||||
// ============ 渠道商管理 ============
|
||||
String get channelManagement => _pick('渠道商管理', 'Channel Management');
|
||||
String get addChannel => _pick('新增渠道', 'Add Channel');
|
||||
String get editChannel => _pick('编辑渠道商', 'Edit Channel');
|
||||
String get addSubChannel => _pick('新增子级', 'Add Child');
|
||||
String get channelName => _pick('名称', 'Name');
|
||||
String get channelType => _pick('类型', 'Type');
|
||||
String get channelSort => _pick('排序', 'Sort');
|
||||
String get channelStatus => _pick('状态', 'Status');
|
||||
String get channelContactPhone => _pick('联系电话', 'Contact Phone');
|
||||
String get channelAddress => _pick('地址', 'Address');
|
||||
String get channelOrgCode => _pick('组织编码', 'Org Code');
|
||||
String get channelTypeChannel => _pick('渠道', 'Channel');
|
||||
String get channelTypeAgent => _pick('代理', 'Agent');
|
||||
String get channelEnabled => _pick('启用', 'Enabled');
|
||||
String get channelDisabled => _pick('禁用', 'Disabled');
|
||||
String get channelParent => _pick('上级', 'Parent');
|
||||
String get channelNameRequired => _pick('请输入名称', 'Please enter a name');
|
||||
String get searchChannelHint => _pick('搜索渠道/代理商名称', 'Search channel/agent name');
|
||||
String get channelDetail => _pick('渠道详情', 'Channel Detail');
|
||||
String get channelExpireTime => _pick('到期时间', 'Expire Time');
|
||||
String get channelCannotDeleteWithChildren =>
|
||||
_pick('存在子集时不允许删除', 'Cannot delete a node that has children');
|
||||
String get channelFilterStatus => _pick('状态', 'Status');
|
||||
String get channelSwipeToDelete => _pick('左滑删除', 'Swipe left to delete');
|
||||
|
||||
// ============ 渠道版本管理 ============
|
||||
/// 渠道/代理节点下钻的 App 版本管理入口。
|
||||
String get channelVersionManage => _pick('版本管理', 'Version');
|
||||
String get versionName => _pick('版本名', 'Version Name');
|
||||
String get versionCode => _pick('版本号', 'Version Code');
|
||||
String get downloadUrlHint => _pick('下载地址', 'Download URL');
|
||||
String get forceUpdate => _pick('强制更新', 'Force Update');
|
||||
String get showPopup => _pick('弹窗提示', 'Popup');
|
||||
String get forceUpdateYes => _pick('是', 'Yes');
|
||||
String get forceUpdateNo => _pick('否', 'No');
|
||||
String get addVersion => _pick('新增版本', 'Add Version');
|
||||
String get editVersion => _pick('编辑版本', 'Edit Version');
|
||||
String get versionNameRequired => _pick('请输入版本名', 'Please enter a version name');
|
||||
String get versionCodeRequired =>
|
||||
_pick('请输入正整数版本号', 'Please enter a positive integer version code');
|
||||
String get versionUrlRequired => _pick('请输入下载地址', 'Please enter a download URL');
|
||||
String get versionUrlInvalid =>
|
||||
_pick('请输入以 http(s) 开头的下载地址', 'URL must start with http(s)');
|
||||
String get versionDeleteConfirm => _pick(
|
||||
'确定删除该版本配置?删除后可重新新增。',
|
||||
'Delete this version config? You can add it again afterward.');
|
||||
String get versionDeleted => _pick('版本已删除', 'Version deleted');
|
||||
String get uploadFile => _pick('上传文件', 'Upload');
|
||||
String get uploading => _pick('上传中…', 'Uploading…');
|
||||
String get uploadSuccess => _pick('文件上传成功', 'Uploaded');
|
||||
String get uploadFailed => _pick('文件上传失败', 'Upload failed');
|
||||
String get pleaseSelectApk => _pick('请选择 .apk 文件', 'Please select an .apk file');
|
||||
|
||||
// ============ 版本更新 ============
|
||||
String get checkUpdate => _pick('检查更新', 'Check Update');
|
||||
String get updateAvailable => _pick('发现新版本', 'Update Available');
|
||||
String get currentVersion => _pick('当前版本', 'Current');
|
||||
String get latestVersion => _pick('最新版本', 'Latest');
|
||||
String get downloadUrl => _pick('下载地址', 'Download');
|
||||
String get updateNow => _pick('立即更新', 'Update Now');
|
||||
String get updateLater => _pick('稍后', 'Later');
|
||||
String get updateNoNew => _pick('已是最新版本', 'Already up to date');
|
||||
String get updateOpenFailed => _pick('无法打开下载地址', 'Failed to open download link');
|
||||
}
|
||||
|
||||
/// 设备状态键(避免在模型层引入 BuildContext)。
|
||||
enum DeviceStatusKey { online, offline, warning }
|
||||
69
lib/utils/permissions.dart
Normal file
69
lib/utils/permissions.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// 账号类别与功能模块权限判定。
|
||||
//
|
||||
// 作为菜单可见性与路由可达性的**单一来源**:AdminHubPage 据此决定模块是否
|
||||
// 显示在界面,router.dart 的 redirect 据此决定是否拦截深链/返回键,二者保持
|
||||
// 完全一致,避免「界面看不到却能路由进入」或「点进去被弹回」的不一致。
|
||||
//
|
||||
// 判定依据:登录用户 [User] 的 [User.superAdmin] / [User.orgId] / [User.orgType]。
|
||||
// - superAdmin=true 或 orgId 为空 → 管理员(可见全部模块);
|
||||
// - orgType=1(渠道)→ 渠道商;orgType=2(代理商)→ 代理商;
|
||||
// - 受限但类型未知 → 按渠道商兜底,确保「渠道商管理」对渠道商账号可开放
|
||||
// (fail-open,符合「渠道商账号点击渠道商管理应正常打开」的修复目标)。
|
||||
//
|
||||
// 模块可见性规则:
|
||||
// - 用户管理:所有已登录账号可见(管理员/渠道商/代理商各看自己范围内用户);
|
||||
// - 渠道商管理:管理员 + 渠道商可见,代理商不可见;
|
||||
// - 其余模块(设备/激活码/公司介绍/滚动字幕):所有账号可见。
|
||||
import '../models/user.dart';
|
||||
|
||||
enum AccountCategory {
|
||||
/// 管理员(超级管理员 / 无组织归属)。
|
||||
admin,
|
||||
|
||||
/// 渠道商(orgType=1)。
|
||||
channelMerchant,
|
||||
|
||||
/// 代理商(orgType=2)。
|
||||
agent,
|
||||
|
||||
/// 未知(理论上不会在 UI 中出现,作为兜底占位)。
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// 功能模块权限判定工具。
|
||||
class SessionPermissions {
|
||||
SessionPermissions._();
|
||||
|
||||
/// 依据当前登录用户推导账号类别。
|
||||
static AccountCategory category(User? user) {
|
||||
if (user == null) return AccountCategory.unknown;
|
||||
if (user.superAdmin) return AccountCategory.admin;
|
||||
if (user.orgId.isEmpty) return AccountCategory.admin;
|
||||
switch (user.orgType) {
|
||||
case 0:
|
||||
return AccountCategory.admin; // 平台
|
||||
case 1:
|
||||
return AccountCategory.channelMerchant; // 渠道
|
||||
case 2:
|
||||
return AccountCategory.agent; // 代理商
|
||||
default:
|
||||
// 受限但类型未知:按渠道商兜底,开放「渠道商管理」。
|
||||
return AccountCategory.channelMerchant;
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否管理员(可访问系统管理 / 用户管理等全部模块)。
|
||||
static bool isAdmin(User? user) => category(user) == AccountCategory.admin;
|
||||
|
||||
/// 是否可见 / 可访问「用户管理」(所有已登录账号)。
|
||||
///
|
||||
/// 管理员看全量,渠道商/代理商看自己组织范围内的用户;具体数据范围由
|
||||
/// [UserPage] 调用后端时按会话权限收敛,不再在菜单层一刀切隐藏。
|
||||
static bool canViewUserManagement(User? user) => user != null;
|
||||
|
||||
/// 是否可见 / 可访问「渠道商管理」(管理员 + 渠道商;代理商不可)。
|
||||
static bool canViewChannelManagement(User? user) {
|
||||
final c = category(user);
|
||||
return c == AccountCategory.admin || c == AccountCategory.channelMerchant;
|
||||
}
|
||||
}
|
||||
55
lib/utils/responsive.dart
Normal file
55
lib/utils/responsive.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 响应式断点工具:用 [Size.shortestSide] 而非 width 判定窗口尺寸,
|
||||
/// 使手机横屏(1600×900 → shortSide=900)正确落入 medium 档。
|
||||
///
|
||||
/// 详见 docs/手机端UI优化方案-v2.md §7.4。
|
||||
enum AppWindowSize { compact, medium, expanded }
|
||||
|
||||
class AppResponsive {
|
||||
AppResponsive(this.size, this.orientation);
|
||||
|
||||
final Size size;
|
||||
final Orientation orientation;
|
||||
|
||||
factory AppResponsive.of(BuildContext context) => AppResponsive(
|
||||
MediaQuery.sizeOf(context),
|
||||
MediaQuery.orientationOf(context),
|
||||
);
|
||||
|
||||
bool get isLandscape => orientation == Orientation.landscape;
|
||||
|
||||
AppWindowSize get windowSize => switch (size.shortestSide) {
|
||||
< 600 => AppWindowSize.compact,
|
||||
< 840 => AppWindowSize.medium, // ★ 手机横屏 900 落这里
|
||||
_ => AppWindowSize.expanded,
|
||||
};
|
||||
|
||||
/// 横屏且宽度够时启用主从(双栏)布局。
|
||||
bool get useMasterDetail => size.width >= 840 && isLandscape;
|
||||
|
||||
/// 自适应网格列数(列表 / 卡片网格)。
|
||||
int gridCount({double minTileWidth = 320}) =>
|
||||
(size.width / minTileWidth).floor().clamp(2, 6);
|
||||
|
||||
/// 内容最大宽度(限宽居中,避免横屏拉爆)。
|
||||
double get contentMaxWidth => switch (windowSize) {
|
||||
AppWindowSize.compact => double.infinity,
|
||||
AppWindowSize.medium => 560,
|
||||
AppWindowSize.expanded => 720,
|
||||
};
|
||||
|
||||
/// 栅格间距。
|
||||
double get gutter => switch (windowSize) {
|
||||
AppWindowSize.compact => 12,
|
||||
AppWindowSize.medium => 24,
|
||||
_ => 32,
|
||||
};
|
||||
|
||||
/// 卡片内边距(横屏更宽松)。
|
||||
double get cardPadding => windowSize == AppWindowSize.compact ? 16 : 20;
|
||||
}
|
||||
|
||||
extension AppResponsiveX on BuildContext {
|
||||
AppResponsive get responsive => AppResponsive.of(this);
|
||||
}
|
||||
37
lib/utils/session_org_scope.dart
Normal file
37
lib/utils/session_org_scope.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 当前登录用户的「数据权限范围」——其所属组织子树的根节点 ID。
|
||||
///
|
||||
/// 用途:在客户端对组织树 / 门店列表做二次收敛,确保非管理员用户只能看到
|
||||
/// 自己所属组织及下属门店的数据,防止后端 `/sys/org/tree` 返回全量树时被
|
||||
/// 越权展示其他组织的数据。
|
||||
///
|
||||
/// 由 [AuthNotifier] 在登录成功与会话恢复时写入 [setCurrentOrg],登出 /
|
||||
/// 会话失效时 [clear]。读取方([OrgService])仅在 [enabled] 为 true 时过滤,
|
||||
/// [currentOrgId] 为空(管理员 / 无法识别)时不做任何裁剪,行为与后端一致。
|
||||
class SessionOrgScope {
|
||||
SessionOrgScope._();
|
||||
static final SessionOrgScope instance = SessionOrgScope._();
|
||||
|
||||
/// 当前用户所属组织 ID。空字符串表示「未限定」(管理员或无法识别),
|
||||
/// 此时客户端不过滤,完全信任后端返回。
|
||||
String currentOrgId = '';
|
||||
|
||||
/// 是否启用客户端数据权限收敛。
|
||||
bool get enabled => currentOrgId.isNotEmpty;
|
||||
|
||||
/// 写入当前用户组织范围。
|
||||
void setCurrentOrg(String? orgId) {
|
||||
currentOrgId = orgId ?? '';
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[SessionOrgScope] currentOrgId=${currentOrgId.isEmpty ? "(未限定/管理员)" : currentOrgId}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空(登出 / 会话失效)。
|
||||
void clear() {
|
||||
currentOrgId = '';
|
||||
}
|
||||
}
|
||||
139
lib/utils/store_local_cache.dart
Normal file
139
lib/utils/store_local_cache.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 门店扩展字段本地持久化(SharedPreferences 封装)。
|
||||
///
|
||||
/// 用于「前端先占位」阶段:主题、品牌(启动图/Logo)、省市区归属等
|
||||
/// 全部只写本地缓存,不发起真实请求。key 以 `orgId` 隔离,切换组织后互不影响。
|
||||
///
|
||||
/// TODO(后端): 上述字段后续由 [OrgService.setStoreTheme] /
|
||||
/// [OrgService.setStoreBranding] 对接 POST /sys/store/* 覆盖,本地缓存可作为离线兜底。
|
||||
class StoreLocalCache {
|
||||
StoreLocalCache._();
|
||||
|
||||
/// 全局单例。
|
||||
static final StoreLocalCache instance = StoreLocalCache._();
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
/// 懒初始化 SharedPreferences(仅在首次访问时调用)。
|
||||
Future<SharedPreferences> get _instance async =>
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
|
||||
/// 外部预初始化(在 [main] 中 await,使同步读取可用)。
|
||||
Future<void> initialize() async {
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
/// 主题 key:`store_theme_$orgId`。
|
||||
String _themeKey(String orgId) => 'store_theme_$orgId';
|
||||
|
||||
/// 品牌 key:`store_branding_$orgId`。
|
||||
String _brandingKey(String orgId) => 'store_branding_$orgId';
|
||||
|
||||
/// 省市区归属 key:`store_region_$orgId`。
|
||||
String _regionKey(String orgId) => 'store_region_$orgId';
|
||||
|
||||
/// 设备绑定关系 key:`device_binding_$deviceId`。
|
||||
String _bindingKey(String deviceId) => 'device_binding_$deviceId';
|
||||
|
||||
/// 保存门店主题。
|
||||
Future<void> saveTheme(String orgId, String themeId) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(_themeKey(orgId), themeId);
|
||||
}
|
||||
|
||||
/// 读取门店主题 id;未设置返回 null。
|
||||
Future<String?> getTheme(String orgId) async {
|
||||
final prefs = await _instance;
|
||||
return prefs.getString(_themeKey(orgId));
|
||||
}
|
||||
|
||||
/// 保存门店品牌(启动图 + Logo 本地路径或远程 URL)。
|
||||
Future<void> saveBranding(
|
||||
String orgId,
|
||||
String splashUrl,
|
||||
String logoUrl,
|
||||
) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setStringList(
|
||||
_brandingKey(orgId),
|
||||
[splashUrl, logoUrl],
|
||||
);
|
||||
}
|
||||
|
||||
/// 读取门店品牌;返回 `[splashUrl, logoUrl]`,未设置时元素为空字符串。
|
||||
Future<List<String>> getBranding(String orgId) async {
|
||||
final prefs = await _instance;
|
||||
final raw = prefs.getStringList(_brandingKey(orgId));
|
||||
if (raw == null || raw.length < 2) {
|
||||
return const ['', ''];
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// 保存门店省市区归属与街道详情。
|
||||
Future<void> saveRegion(
|
||||
String orgId,
|
||||
String provinceCode,
|
||||
String cityCode,
|
||||
String districtCode,
|
||||
String addressDetail,
|
||||
) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setStringList(
|
||||
_regionKey(orgId),
|
||||
[provinceCode, cityCode, districtCode, addressDetail],
|
||||
);
|
||||
}
|
||||
|
||||
/// 读取门店省市区归属;返回 `[provinceCode, cityCode, districtCode, addressDetail]`
|
||||
/// 未设置时元素为空字符串。
|
||||
Future<List<String>> getRegion(String orgId) async {
|
||||
final prefs = await _instance;
|
||||
final raw = prefs.getStringList(_regionKey(orgId));
|
||||
if (raw == null || raw.length < 4) {
|
||||
return const ['', '', '', ''];
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// 保存设备绑定关系(扫码绑定占位)。
|
||||
Future<void> saveDeviceBinding(
|
||||
String deviceId,
|
||||
String storeId,
|
||||
String storeName,
|
||||
String? groupId,
|
||||
) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setStringList(
|
||||
_bindingKey(deviceId),
|
||||
[storeId, storeName, groupId ?? ''],
|
||||
);
|
||||
}
|
||||
|
||||
/// 读取设备绑定关系;返回 `[storeId, storeName, groupId]`,
|
||||
/// 未设置时元素为空字符串。
|
||||
Future<List<String>> getDeviceBinding(String deviceId) async {
|
||||
final prefs = await _instance;
|
||||
final raw = prefs.getStringList(_bindingKey(deviceId));
|
||||
if (raw == null || raw.length < 3) {
|
||||
return const ['', '', ''];
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
// ================ 通用 String 存取(用于语言、主题等非门店隔离配置) ================
|
||||
|
||||
/// 保存字符串。
|
||||
Future<void> setString(String key, String value) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(key, value);
|
||||
}
|
||||
|
||||
/// 读取字符串;不存在返回 null。
|
||||
String? getString(String key) {
|
||||
final prefs = _prefs;
|
||||
if (prefs == null) return null;
|
||||
return prefs.getString(key);
|
||||
}
|
||||
}
|
||||
17
lib/utils/uuid.dart
Normal file
17
lib/utils/uuid.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// 生成与 web 前端一致的 UUID v4 字符串。
|
||||
///
|
||||
/// 模板:`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
|
||||
String generateUuid() {
|
||||
final random = Random.secure();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAllMapped(
|
||||
RegExp('[xy]'),
|
||||
(match) {
|
||||
final r = random.nextInt(16);
|
||||
final c = match.group(0)!;
|
||||
final v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toRadixString(16);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user