ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
95
lib/models/alert.dart
Normal file
95
lib/models/alert.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
/// 告警级别枚举。
|
||||
enum AlertLevel {
|
||||
/// 严重。
|
||||
critical,
|
||||
|
||||
/// 警告。
|
||||
warning,
|
||||
|
||||
/// 提示。
|
||||
info;
|
||||
|
||||
/// 级别中文名称。
|
||||
String get label {
|
||||
switch (this) {
|
||||
case AlertLevel.critical:
|
||||
return '严重';
|
||||
case AlertLevel.warning:
|
||||
return '警告';
|
||||
case AlertLevel.info:
|
||||
return '提示';
|
||||
}
|
||||
}
|
||||
|
||||
/// 从字符串解析级别。
|
||||
static AlertLevel fromString(String value) {
|
||||
return AlertLevel.values.firstWhere(
|
||||
(level) => level.name == value,
|
||||
orElse: () => AlertLevel.info,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警模型。
|
||||
class Alert {
|
||||
const Alert({
|
||||
required this.id,
|
||||
required this.deviceId,
|
||||
required this.deviceName,
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.level,
|
||||
required this.occurredAt,
|
||||
required this.acknowledged,
|
||||
});
|
||||
|
||||
/// 告警唯一标识。
|
||||
final String id;
|
||||
|
||||
/// 关联设备 ID。
|
||||
final String deviceId;
|
||||
|
||||
/// 关联设备名称。
|
||||
final String deviceName;
|
||||
|
||||
/// 告警标题。
|
||||
final String title;
|
||||
|
||||
/// 告警详情。
|
||||
final String message;
|
||||
|
||||
/// 告警级别。
|
||||
final AlertLevel level;
|
||||
|
||||
/// 发生时间。
|
||||
final DateTime occurredAt;
|
||||
|
||||
/// 是否已确认。
|
||||
final bool acknowledged;
|
||||
|
||||
factory Alert.fromJson(Map<String, dynamic> json) {
|
||||
return Alert(
|
||||
id: json['id'] as String,
|
||||
deviceId: json['deviceId'] as String,
|
||||
deviceName: json['deviceName'] as String,
|
||||
title: json['title'] as String,
|
||||
message: json['message'] as String,
|
||||
level: AlertLevel.fromString(json['level'] as String),
|
||||
occurredAt: DateTime.parse(json['occurredAt'] as String),
|
||||
acknowledged: json['acknowledged'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'deviceId': deviceId,
|
||||
'deviceName': deviceName,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'level': level.name,
|
||||
'occurredAt': occurredAt.toIso8601String(),
|
||||
'acknowledged': acknowledged,
|
||||
};
|
||||
}
|
||||
}
|
||||
95
lib/models/app_version.dart
Normal file
95
lib/models/app_version.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
/// APP 版本模型。
|
||||
///
|
||||
/// 对应后端 /device/orgGroupAppVersion/* 接口的版本实体。
|
||||
class AppVersion {
|
||||
const AppVersion({
|
||||
required this.id,
|
||||
required this.orgId,
|
||||
required this.groupId,
|
||||
required this.appName,
|
||||
required this.packageName,
|
||||
required this.versionCode,
|
||||
required this.versionName,
|
||||
required this.apkUrl,
|
||||
required this.status,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 版本 ID。
|
||||
final String id;
|
||||
|
||||
/// 组织 ID。
|
||||
final String orgId;
|
||||
|
||||
/// 设备分组 ID。
|
||||
final String groupId;
|
||||
|
||||
/// 应用名称。
|
||||
final String appName;
|
||||
|
||||
/// 包名。
|
||||
final String packageName;
|
||||
|
||||
/// 版本号(整数)。
|
||||
final int versionCode;
|
||||
|
||||
/// 版本名。
|
||||
final String versionName;
|
||||
|
||||
/// APK 下载地址。
|
||||
final String apkUrl;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory AppVersion.fromJson(Map<String, dynamic> json) {
|
||||
return AppVersion(
|
||||
id: json['id']?.toString() ?? '',
|
||||
orgId: json['orgId']?.toString() ?? '',
|
||||
groupId: json['groupId']?.toString() ?? '',
|
||||
appName: json['appName']?.toString() ?? '',
|
||||
// 包名:后端可能返回 packageName / package_name(与既有 AppVersionPage 一致)。
|
||||
packageName: json['packageName']?.toString() ??
|
||||
json['package_name']?.toString() ??
|
||||
'',
|
||||
versionCode: int.tryParse(json['versionCode']?.toString() ?? '0') ?? 0,
|
||||
versionName: json['versionName']?.toString() ?? '',
|
||||
// 下载地址:对齐 Web 端 normalizeAppVersion(downloadUrl/download_url),
|
||||
// 同时兼容移动端既有 AppVersionPage 使用的 apkUrl——后端不同部署字段名不一,
|
||||
// 取首个非空值,避免「立即更新」因 url 为空而失效。
|
||||
apkUrl: json['apkUrl']?.toString() ??
|
||||
json['downloadUrl']?.toString() ??
|
||||
json['download_url']?.toString() ??
|
||||
'',
|
||||
status: int.tryParse(json['status']?.toString() ?? '0') ?? 0,
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'orgId': orgId,
|
||||
'groupId': groupId,
|
||||
'appName': appName,
|
||||
'packageName': packageName,
|
||||
'versionCode': versionCode,
|
||||
'versionName': versionName,
|
||||
'apkUrl': apkUrl,
|
||||
'status': status,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
622
lib/models/device.dart
Normal file
622
lib/models/device.dart
Normal file
@@ -0,0 +1,622 @@
|
||||
/// 设备状态枚举。
|
||||
enum DeviceStatus {
|
||||
/// 在线。
|
||||
online,
|
||||
|
||||
/// 离线。
|
||||
offline,
|
||||
|
||||
/// 告警(指标异常)。
|
||||
warning;
|
||||
|
||||
/// 状态中文名称。
|
||||
String get label {
|
||||
switch (this) {
|
||||
case DeviceStatus.online:
|
||||
return '在线';
|
||||
case DeviceStatus.offline:
|
||||
return '离线';
|
||||
case DeviceStatus.warning:
|
||||
return '告警';
|
||||
}
|
||||
}
|
||||
|
||||
/// 从后端状态字符串解析。
|
||||
///
|
||||
/// 规则(与「爱视后台」约定一致):`offline` → 离线、`warning` → 告警、
|
||||
/// 其余(含 `online` 与未知值)→ 在线。
|
||||
static DeviceStatus fromString(String? value) {
|
||||
final status = (value ?? '').toLowerCase();
|
||||
if (status == 'offline') {
|
||||
return DeviceStatus.offline;
|
||||
}
|
||||
if (status == 'warning') {
|
||||
return DeviceStatus.warning;
|
||||
}
|
||||
return DeviceStatus.online;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备指令常量(对应 /device/command/sendByMac|sendByOrg 的 command 字段)。
|
||||
class DeviceCommand {
|
||||
DeviceCommand._();
|
||||
|
||||
static const String powerOff = 'powerOff';
|
||||
static const String reboot = 'reboot';
|
||||
static const String screenshot = 'screenshot';
|
||||
static const String themeUpdate = 'themeUpdate';
|
||||
static const String versionUpdate = 'versionUpdate';
|
||||
static const String marqueeUpdate = 'marqueeUpdate';
|
||||
static const String weatherUpdate = 'weatherUpdate';
|
||||
static const String mediaZoneVol = 'mediaZoneVol';
|
||||
static const String defaultVol = 'defaultVol';
|
||||
static const String themeClear = 'themeClear';
|
||||
static const String cacheClear = 'cacheClear';
|
||||
static const String storeExpire = 'storeExpire';
|
||||
static const String storeDisable = 'storeDisable';
|
||||
static const String passwordUpdate = 'passwordUpdate';
|
||||
static const String worldAd = 'worldAd';
|
||||
}
|
||||
|
||||
/// 设备运行指标(CPU / 内存 / 磁盘 / 网络)。
|
||||
///
|
||||
/// 注意:真实后端不提供该类指标,仅 mock 模式有值;
|
||||
/// 由 [Device.hasMetrics] 标识当前数据是否包含指标。
|
||||
class DeviceMetrics {
|
||||
const DeviceMetrics({
|
||||
required this.cpuUsage,
|
||||
required this.memoryUsage,
|
||||
required this.diskUsage,
|
||||
required this.networkRxKbps,
|
||||
required this.networkTxKbps,
|
||||
});
|
||||
|
||||
/// 全零指标(真实模式下的占位值,配合 hasMetrics=false 使用)。
|
||||
const DeviceMetrics.zero()
|
||||
: cpuUsage = 0,
|
||||
memoryUsage = 0,
|
||||
diskUsage = 0,
|
||||
networkRxKbps = 0,
|
||||
networkTxKbps = 0;
|
||||
|
||||
/// CPU 使用率(0.0 ~ 1.0)。
|
||||
final double cpuUsage;
|
||||
|
||||
/// 内存使用率(0.0 ~ 1.0)。
|
||||
final double memoryUsage;
|
||||
|
||||
/// 磁盘使用率(0.0 ~ 1.0)。
|
||||
final double diskUsage;
|
||||
|
||||
/// 网络下行速率(KB/s)。
|
||||
final double networkRxKbps;
|
||||
|
||||
/// 网络上行速率(KB/s)。
|
||||
final double networkTxKbps;
|
||||
|
||||
/// 从后端 JSON 解析运行指标。
|
||||
///
|
||||
/// 兼容嵌套 `metrics` 对象与顶层扁平字段两种形态,并对百分比做 0~100→0~1
|
||||
/// 归一化;任意字段缺失时安全回退为 0,保证 UI 不崩溃。
|
||||
factory DeviceMetrics.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceMetrics(
|
||||
cpuUsage: _normalizePercent(
|
||||
json['cpuUsage'] ?? json['cpu_usage'] ?? json['cpu'],
|
||||
),
|
||||
memoryUsage: _normalizePercent(
|
||||
json['memoryUsage'] ??
|
||||
json['memory_usage'] ??
|
||||
json['mem'] ??
|
||||
json['memUsage'] ??
|
||||
json['memory'],
|
||||
),
|
||||
diskUsage: _normalizePercent(
|
||||
json['diskUsage'] ?? json['disk_usage'] ?? json['disk'],
|
||||
),
|
||||
networkRxKbps: _toDouble(
|
||||
json['networkRxKbps'] ?? json['rxKbps'] ?? json['networkRx'],
|
||||
),
|
||||
networkTxKbps: _toDouble(
|
||||
json['networkTxKbps'] ?? json['txKbps'] ?? json['networkTx'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'cpuUsage': cpuUsage,
|
||||
'memoryUsage': memoryUsage,
|
||||
'diskUsage': diskUsage,
|
||||
'networkRxKbps': networkRxKbps,
|
||||
'networkTxKbps': networkTxKbps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 将后端返回的百分数字段归一化为 0.0~1.0。
|
||||
///
|
||||
/// 兼容后端可能返回 0~100(如 87)或 0~1(如 0.87)两种形态,避免 UI 进度条
|
||||
/// 与百分比文本越界。
|
||||
double _normalizePercent(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
final n = value is num
|
||||
? value.toDouble()
|
||||
: double.tryParse(value.toString());
|
||||
if (n == null) return 0;
|
||||
return n > 1 ? n / 100 : n;
|
||||
}
|
||||
|
||||
/// 将动态值安全转换为 double,无法解析时返回 0。
|
||||
double _toDouble(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
return (value is num
|
||||
? value.toDouble()
|
||||
: double.tryParse(value.toString())) ??
|
||||
0;
|
||||
}
|
||||
|
||||
/// 设备模型。
|
||||
///
|
||||
/// 字段对齐真实后端 DeviceInfo(id / mac / name / deviceSn / deviceUid /
|
||||
/// status / groupId / lastSyncTime / createTime),并保留 UI 展示兼容字段
|
||||
/// (ip / location / os / uptimeSeconds / metrics / storeId),真实模式下取默认值。
|
||||
class Device {
|
||||
const Device({
|
||||
required this.id,
|
||||
required this.mac,
|
||||
required this.name,
|
||||
required this.model,
|
||||
required this.deviceSn,
|
||||
required this.deviceUid,
|
||||
required this.status,
|
||||
required this.groupId,
|
||||
this.storeId = '',
|
||||
required this.lastSyncTime,
|
||||
required this.createTime,
|
||||
this.ip = '',
|
||||
this.location = '',
|
||||
this.storeName = '',
|
||||
this.os = '',
|
||||
this.roomNo = '',
|
||||
this.uptimeSeconds = 0,
|
||||
this.hasMetrics = false,
|
||||
this.metrics = const DeviceMetrics.zero(),
|
||||
});
|
||||
|
||||
/// 设备唯一标识。
|
||||
final String id;
|
||||
|
||||
/// 设备 MAC 地址(指令下发的目标标识)。
|
||||
final String mac;
|
||||
|
||||
/// 设备名称(后端可能返回 MAC、SN 或自定义别名)。
|
||||
final String name;
|
||||
|
||||
/// 设备型号(如 rk3566_r、p321)。
|
||||
///
|
||||
/// 优先从后端 model/modelName/deviceModel/productModel 等字段解析;
|
||||
/// 缺失时降级为 [name](若 name 是 MAC 地址则跳过),再降级为 [deviceSn],
|
||||
/// 最终兜底 [mac],确保不会把 MAC 误当成型号展示。
|
||||
final String model;
|
||||
|
||||
/// 设备序列号。
|
||||
final String deviceSn;
|
||||
|
||||
/// 设备 UID。
|
||||
final String deviceUid;
|
||||
|
||||
/// 当前状态。
|
||||
final DeviceStatus status;
|
||||
|
||||
/// 分组 ID。
|
||||
final String groupId;
|
||||
|
||||
/// 所属门店 ID(用于拉取设备分组列表;后端命名多样,按常见别名解析)。
|
||||
final String storeId;
|
||||
|
||||
/// 房号(房间号,如「802」「A栋301」)。
|
||||
final String roomNo;
|
||||
|
||||
/// 最近一次同步时间。
|
||||
final DateTime? lastSyncTime;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
/// IP 地址(真实后端无此字段,默认回退为 MAC)。
|
||||
final String ip;
|
||||
|
||||
/// 机房 / 位置(真实后端无此字段,默认为空)。
|
||||
final String location;
|
||||
|
||||
/// 所属门店名称(真实后端可能返回 storeName / orgName)。
|
||||
final String storeName;
|
||||
|
||||
/// 操作系统描述(真实后端无此字段,默认为空)。
|
||||
final String os;
|
||||
|
||||
/// 已连续运行时长(秒)(真实后端无此字段,默认为 0)。
|
||||
final int uptimeSeconds;
|
||||
|
||||
/// 是否包含运行指标(仅 mock 模式为 true)。
|
||||
final bool hasMetrics;
|
||||
|
||||
/// 运行指标。
|
||||
final DeviceMetrics metrics;
|
||||
|
||||
/// 创建副本并覆盖指定字段。
|
||||
Device copyWith({
|
||||
String? id,
|
||||
String? mac,
|
||||
String? name,
|
||||
String? model,
|
||||
String? deviceSn,
|
||||
String? deviceUid,
|
||||
DeviceStatus? status,
|
||||
String? groupId,
|
||||
String? storeId,
|
||||
String? roomNo,
|
||||
DateTime? lastSyncTime,
|
||||
DateTime? createTime,
|
||||
String? ip,
|
||||
String? location,
|
||||
String? storeName,
|
||||
String? os,
|
||||
int? uptimeSeconds,
|
||||
bool? hasMetrics,
|
||||
DeviceMetrics? metrics,
|
||||
}) {
|
||||
return Device(
|
||||
id: id ?? this.id,
|
||||
mac: mac ?? this.mac,
|
||||
name: name ?? this.name,
|
||||
model: model ?? this.model,
|
||||
deviceSn: deviceSn ?? this.deviceSn,
|
||||
deviceUid: deviceUid ?? this.deviceUid,
|
||||
status: status ?? this.status,
|
||||
groupId: groupId ?? this.groupId,
|
||||
storeId: storeId ?? this.storeId,
|
||||
roomNo: roomNo ?? this.roomNo,
|
||||
lastSyncTime: lastSyncTime ?? this.lastSyncTime,
|
||||
createTime: createTime ?? this.createTime,
|
||||
ip: ip ?? this.ip,
|
||||
location: location ?? this.location,
|
||||
storeName: storeName ?? this.storeName,
|
||||
os: os ?? this.os,
|
||||
uptimeSeconds: uptimeSeconds ?? this.uptimeSeconds,
|
||||
hasMetrics: hasMetrics ?? this.hasMetrics,
|
||||
metrics: metrics ?? this.metrics,
|
||||
);
|
||||
}
|
||||
|
||||
/// 设备标识:统一为「MAC / 房号」组合。
|
||||
/// 优先 MAC + 房号;任一缺失则只用存在的那项;两者皆空时逐级降级到
|
||||
/// name → model → mac,确保永不展示空白。
|
||||
String get macRoomLabel {
|
||||
final m = mac.trim();
|
||||
final r = roomNo.trim();
|
||||
if (m.isNotEmpty && r.isNotEmpty) return '$m / $r';
|
||||
if (m.isNotEmpty) return m;
|
||||
if (r.isNotEmpty) return r;
|
||||
if (name.isNotEmpty) return name;
|
||||
if (model.isNotEmpty) return model;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// 仅展示 MAC 地址(房号改为独立「房号:XXX」列示,不再用「/」把两者粘在一起)。
|
||||
/// MAC 为空时逐级降级到 name → model → mac,确保标题不空白。
|
||||
String get macLabel {
|
||||
final m = mac.trim();
|
||||
if (m.isNotEmpty) return m;
|
||||
if (name.isNotEmpty) return name;
|
||||
if (model.isNotEmpty) return model;
|
||||
return m;
|
||||
}
|
||||
|
||||
factory Device.fromJson(Map<String, dynamic> json) {
|
||||
// 兼容详情接口可能再包一层 {device|info|data:{...}} 的形态,先兜底解包。
|
||||
final Map<String, dynamic> data = _unwrapDeviceJson(json);
|
||||
final mac = data['mac']?.toString() ?? '';
|
||||
final metricsJson = data['metrics'];
|
||||
// 合并嵌套 metrics 与顶层扁平遥测字段(详情接口可能直接返回 cpuUsage /
|
||||
// cpu_usage / cpu 等)。任一使用率字段存在即视为有指标数据,便于详情页展示。
|
||||
final Map<String, dynamic> mergedMetrics = <String, dynamic>{};
|
||||
if (metricsJson is Map<String, dynamic>) {
|
||||
mergedMetrics.addAll(metricsJson);
|
||||
}
|
||||
final flatCpu = _firstNum(data, [
|
||||
'cpuUsage',
|
||||
'cpu_usage',
|
||||
'cpu',
|
||||
'cpuPercent',
|
||||
'cpu_percent',
|
||||
'cpuRate',
|
||||
'cpu_rate',
|
||||
]);
|
||||
if (flatCpu != null) {
|
||||
mergedMetrics.putIfAbsent('cpuUsage', () => flatCpu);
|
||||
}
|
||||
final flatMem = _firstNum(data, [
|
||||
'memoryUsage',
|
||||
'memory_usage',
|
||||
'mem',
|
||||
'memUsage',
|
||||
'memory',
|
||||
'memoryPercent',
|
||||
'mem_percent',
|
||||
]);
|
||||
if (flatMem != null) {
|
||||
mergedMetrics.putIfAbsent('memoryUsage', () => flatMem);
|
||||
}
|
||||
final flatDisk = _firstNum(data, [
|
||||
'diskUsage',
|
||||
'disk_usage',
|
||||
'disk',
|
||||
'diskPercent',
|
||||
'disk_percent',
|
||||
]);
|
||||
if (flatDisk != null) {
|
||||
mergedMetrics.putIfAbsent('diskUsage', () => flatDisk);
|
||||
}
|
||||
// 爱视后台列表字段名为 deviceName / device_name,name 字段常缺失或存 MAC/IP。
|
||||
final name = data['deviceName']?.toString() ??
|
||||
data['device_name']?.toString() ??
|
||||
data['name']?.toString() ??
|
||||
'';
|
||||
// IP 可能落在 ip / ipAddress / ip_address / deviceIp / device_ip 等多个字段。
|
||||
final ip = data['ip']?.toString() ??
|
||||
data['ipAddress']?.toString() ??
|
||||
data['ip_address']?.toString() ??
|
||||
data['deviceIp']?.toString() ??
|
||||
data['device_ip']?.toString() ??
|
||||
'';
|
||||
final deviceSn = data['deviceSn']?.toString() ?? '';
|
||||
// 型号字段后端命名多样,按常见别名逐级解析;解析不到时按规则降级。
|
||||
final rawModel = _firstString(data, [
|
||||
'model',
|
||||
'modelName',
|
||||
'model_name',
|
||||
'deviceModel',
|
||||
'device_model',
|
||||
'productModel',
|
||||
'product_model',
|
||||
'productName',
|
||||
'product_name',
|
||||
'board',
|
||||
'hardware',
|
||||
'brand',
|
||||
'deviceType',
|
||||
'device_type',
|
||||
'type',
|
||||
]) ??
|
||||
'';
|
||||
final model = _resolveModel(rawModel, name, deviceSn, mac);
|
||||
return Device(
|
||||
id: data['id']?.toString() ?? '',
|
||||
mac: mac,
|
||||
name: name,
|
||||
model: model,
|
||||
deviceSn: deviceSn,
|
||||
deviceUid: data['deviceUid']?.toString() ?? '',
|
||||
status: DeviceStatus.fromString(data['status']?.toString()),
|
||||
groupId: data['groupId']?.toString() ?? '',
|
||||
storeId: _resolveStoreId(data),
|
||||
// Web 端设备列表把最近心跳时间存为 heartbeatTime / heartbeat_time,
|
||||
// 详情接口多为 lastSyncTime;兼容两种命名,避免最后活跃时间解析为空。
|
||||
lastSyncTime: _parseDateTime(
|
||||
data['lastSyncTime'] ??
|
||||
data['last_sync_time'] ??
|
||||
data['heartbeatTime'] ??
|
||||
data['heartbeat_time'] ??
|
||||
data['lastHeartbeat'] ??
|
||||
data['last_heartbeat'],
|
||||
),
|
||||
createTime: _parseDateTime(data['createTime']),
|
||||
// UI 兼容字段:真实数据缺失时回退。
|
||||
ip: ip,
|
||||
location: _firstString(data, [
|
||||
'location',
|
||||
'locationDesc',
|
||||
'loc',
|
||||
'position',
|
||||
'place',
|
||||
'area',
|
||||
'region',
|
||||
]) ??
|
||||
'',
|
||||
storeName: data['storeName']?.toString() ??
|
||||
data['orgName']?.toString() ??
|
||||
data['store']?.toString() ?? '',
|
||||
os: _firstString(data, [
|
||||
'os',
|
||||
'osName',
|
||||
'system',
|
||||
'systemVersion',
|
||||
'osVersion',
|
||||
'platform',
|
||||
'osInfo',
|
||||
'sysVersion',
|
||||
]) ??
|
||||
'',
|
||||
// 房号/房间号字段后端命名极不统一,按常见别名逐级回退。
|
||||
roomNo: _firstString(data, [
|
||||
'roomNo',
|
||||
'room',
|
||||
'roomNoStr',
|
||||
'roomNumber',
|
||||
'room_number',
|
||||
'roomNum',
|
||||
'roomNumStr',
|
||||
'roomId',
|
||||
'room_id',
|
||||
'roomName',
|
||||
'houseNumber',
|
||||
'house_number',
|
||||
'houseNo',
|
||||
'houseNoStr',
|
||||
'roomCode',
|
||||
'room_code',
|
||||
]) ??
|
||||
'',
|
||||
uptimeSeconds: _firstNum(data, [
|
||||
'uptimeSeconds',
|
||||
'uptime',
|
||||
'uptimeSec',
|
||||
'uptimeSecs',
|
||||
'runTime',
|
||||
'runningTime',
|
||||
'onlineTime',
|
||||
'bootTime',
|
||||
'upTime',
|
||||
'up_time',
|
||||
])?.toInt() ?? 0,
|
||||
hasMetrics: mergedMetrics.isNotEmpty,
|
||||
metrics: mergedMetrics.isNotEmpty
|
||||
? DeviceMetrics.fromJson(mergedMetrics)
|
||||
: const DeviceMetrics.zero(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兜底解包设备 JSON。
|
||||
///
|
||||
/// 后端 /device/info/info/{id} 详情接口可能并未把设备对象直接作为响应体,
|
||||
/// 而是再包一层 `{device|info|data: {...}}`;若顶层没有 `id` 但某嵌套字段
|
||||
/// 是含 `id` 的 Map,则返回该嵌套 Map,否则原样返回。
|
||||
/// 这样可避免详情页所有遥测字段因解析不到而显示为空。
|
||||
static Map<String, dynamic> _unwrapDeviceJson(Map<String, dynamic> json) {
|
||||
if (json.containsKey('id')) return json;
|
||||
for (final key in const ['device', 'info', 'data', 'result', 'record']) {
|
||||
final nested = json[key];
|
||||
if (nested is Map<String, dynamic> &&
|
||||
(nested.containsKey('id') || nested.containsKey('mac'))) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// 从 [json] 中按 [keys] 顺序取第一个非空数值。
|
||||
///
|
||||
/// 兼容后端返回字符串或数字两种形态;任意匹配键取到有效数值即返回,
|
||||
/// 否则返回 null(交由调用处按默认 0 处理,避免 UI 崩溃)。
|
||||
static num? _firstNum(
|
||||
Map<String, dynamic> json,
|
||||
List<String> keys,
|
||||
) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value == null) continue;
|
||||
if (value is num) return value;
|
||||
final n = double.tryParse(value.toString());
|
||||
if (n != null) return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 从 [json] 中按 [keys] 顺序取第一个非空字符串值。
|
||||
static String? _firstString(
|
||||
Map<String, dynamic> json,
|
||||
List<String> keys,
|
||||
) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value == null) continue;
|
||||
final text = value.toString().trim();
|
||||
if (text.isNotEmpty) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 解析设备所属门店 ID。
|
||||
///
|
||||
/// 后端字段命名极不统一:可能直接在顶层返回 `storeId` / `orgId`,也可能嵌套在
|
||||
/// `store` / `storeInfo` / `org` 对象中。本方法先尝试顶层字段,再尝试嵌套对象
|
||||
/// 中的 `id` / `orgId` / `storeId`,尽力避免分组下拉因取不到门店 ID 而无法加载。
|
||||
static String _resolveStoreId(Map<String, dynamic> json) {
|
||||
final topLevel = _firstString(json, [
|
||||
'storeId',
|
||||
'storeOrgId',
|
||||
'orgId',
|
||||
'store_id',
|
||||
'org_id',
|
||||
'storeIdStr',
|
||||
]);
|
||||
if (topLevel != null && topLevel.isNotEmpty) return topLevel;
|
||||
|
||||
for (final nestedKey in const ['store', 'storeInfo', 'org', 'organization']) {
|
||||
final nested = json[nestedKey];
|
||||
if (nested is Map<String, dynamic>) {
|
||||
final id = _firstString(nested, [
|
||||
'id',
|
||||
'orgId',
|
||||
'storeId',
|
||||
'store_id',
|
||||
'org_id',
|
||||
]);
|
||||
if (id != null && id.isNotEmpty) return id;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// 解析最终展示的型号文本。
|
||||
///
|
||||
/// 优先级:
|
||||
/// 1. 后端明确返回的 [rawModel]
|
||||
/// 2. [name](仅当非空且不是 MAC 地址时)
|
||||
/// 3. [deviceSn]
|
||||
/// 4. [mac](最终兜底,避免空标题)
|
||||
static String _resolveModel(
|
||||
String rawModel,
|
||||
String name,
|
||||
String deviceSn,
|
||||
String mac,
|
||||
) {
|
||||
if (rawModel.isNotEmpty) return rawModel;
|
||||
if (name.isNotEmpty && !_isMacAddress(name)) return name;
|
||||
if (deviceSn.isNotEmpty) return deviceSn;
|
||||
return mac;
|
||||
}
|
||||
|
||||
/// 判断 [value] 是否为 MAC 地址格式(xx:xx:xx:xx:xx:xx 或 xx-xx-xx-xx-xx-xx)。
|
||||
static bool _isMacAddress(String value) {
|
||||
return RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
|
||||
.hasMatch(value.trim());
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'mac': mac,
|
||||
'name': name,
|
||||
'model': model,
|
||||
'deviceSn': deviceSn,
|
||||
'deviceUid': deviceUid,
|
||||
'status': status.name,
|
||||
'groupId': groupId,
|
||||
'storeId': storeId,
|
||||
'lastSyncTime': lastSyncTime?.toIso8601String(),
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
'ip': ip,
|
||||
'location': location,
|
||||
'storeName': storeName,
|
||||
'os': os,
|
||||
'roomId': roomNo,
|
||||
'uptimeSeconds': uptimeSeconds,
|
||||
'metrics': hasMetrics ? metrics.toJson() : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
281
lib/models/intro_category.dart
Normal file
281
lib/models/intro_category.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
/// 介绍页分类模型。
|
||||
///
|
||||
/// 对应后端 /company/introCategory/* 接口。
|
||||
///
|
||||
/// 后端实际字段:
|
||||
/// - id, orgId, orgName, categoryType, name, subCategories,
|
||||
/// createTime, updateTime
|
||||
/// 注:后端分类列表不返回 sort/status,兼容处理为默认值。
|
||||
class IntroCategory {
|
||||
const IntroCategory({
|
||||
required this.id,
|
||||
required this.orgId,
|
||||
required this.orgName,
|
||||
required this.categoryType,
|
||||
required this.name,
|
||||
required this.sort,
|
||||
required this.status,
|
||||
required this.createTime,
|
||||
this.subCategories,
|
||||
});
|
||||
|
||||
/// 分类 ID。
|
||||
final String id;
|
||||
|
||||
/// 所属门店/组织 ID。
|
||||
final String orgId;
|
||||
|
||||
/// 所属门店/组织名称(用于门店筛选展示)。
|
||||
final String orgName;
|
||||
|
||||
/// 分类类型编码(如 gongsi/gsjs/hotel 等)。
|
||||
final String categoryType;
|
||||
|
||||
/// 分类名称。
|
||||
final String name;
|
||||
|
||||
/// 排序号(后端可能缺失,缺省 0)。
|
||||
final int sort;
|
||||
|
||||
/// 状态(1=启用/正常,0=禁用),与 Web 后端保持一致。
|
||||
final int status;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
/// 后端可能嵌入的子分类原始数据(暂不直接解析,用于后续扩展)。
|
||||
final List<dynamic>? subCategories;
|
||||
|
||||
factory IntroCategory.fromJson(Map<String, dynamic> json) {
|
||||
return IntroCategory(
|
||||
id: json['id']?.toString() ?? '',
|
||||
orgId: json['orgId']?.toString() ?? '',
|
||||
orgName: json['orgName']?.toString() ?? '',
|
||||
categoryType: json['categoryType']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
sort: int.tryParse(json['sortOrder']?.toString() ??
|
||||
json['sort']?.toString() ??
|
||||
'0') ??
|
||||
0,
|
||||
status: _parseStatus(json['status']),
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
subCategories: json['subCategories'] is List
|
||||
? List<dynamic>.from(json['subCategories'] as List)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 与 Web 后端保持一致:1=启用/正常,0=禁用;缺失/null 视为启用。
|
||||
static int _parseStatus(dynamic value) {
|
||||
if (value == null) return 1;
|
||||
if (value is bool) return value ? 1 : 0;
|
||||
final text = value.toString().trim().toLowerCase();
|
||||
if (text == '1' || text == 'active' || text == '正常' || text == '启用' ||
|
||||
text == 'enabled' || text == 'true') {
|
||||
return 1;
|
||||
}
|
||||
if (text == '0' ||
|
||||
text == 'inactive' ||
|
||||
text == '禁用' ||
|
||||
text == '停用' ||
|
||||
text == 'disabled' ||
|
||||
text == 'false') {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'orgId': orgId,
|
||||
'orgName': orgName,
|
||||
'categoryType': categoryType,
|
||||
'name': name,
|
||||
'sort': sort,
|
||||
'status': status,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
'subCategories': subCategories,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 介绍页子分类模型。
|
||||
///
|
||||
/// 对应后端 /company/introSubcategory/* 接口。
|
||||
///
|
||||
/// 后端实际字段:
|
||||
/// - id, categoryId, categoryType, mediaType, mediaUrl,
|
||||
/// title, description, status, sortOrder, contents, createTime, updateTime
|
||||
class IntroSubcategory {
|
||||
const IntroSubcategory({
|
||||
required this.id,
|
||||
required this.categoryId,
|
||||
required this.categoryType,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.mediaType,
|
||||
required this.mediaUrl,
|
||||
required this.sort,
|
||||
required this.status,
|
||||
this.contents,
|
||||
});
|
||||
|
||||
/// 子分类 ID。
|
||||
final String id;
|
||||
|
||||
/// 所属分类 ID。
|
||||
final String categoryId;
|
||||
|
||||
/// 分类类型编码。
|
||||
final String categoryType;
|
||||
|
||||
/// 子分类标题(后端用 title 而非 name)。
|
||||
final String title;
|
||||
|
||||
/// 描述/简介。
|
||||
final String description;
|
||||
|
||||
/// 媒体类型:image / video。
|
||||
final String mediaType;
|
||||
|
||||
/// 媒体地址。
|
||||
final String mediaUrl;
|
||||
|
||||
/// 排序号(后端字段 sortOrder)。
|
||||
final int sort;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 后端可能嵌入的内容原始数据。
|
||||
final List<dynamic>? contents;
|
||||
|
||||
factory IntroSubcategory.fromJson(Map<String, dynamic> json) {
|
||||
return IntroSubcategory(
|
||||
id: json['id']?.toString() ?? '',
|
||||
categoryId: json['categoryId']?.toString() ?? '',
|
||||
categoryType: json['categoryType']?.toString() ?? '',
|
||||
title: json['title']?.toString() ??
|
||||
json['name']?.toString() ??
|
||||
json['subcategoryTitle']?.toString() ??
|
||||
'',
|
||||
description: json['description']?.toString() ?? '',
|
||||
mediaType: json['mediaType']?.toString() ?? '',
|
||||
mediaUrl: json['mediaUrl']?.toString() ?? '',
|
||||
sort: int.tryParse(json['sortOrder']?.toString() ??
|
||||
json['sort']?.toString() ??
|
||||
'0') ??
|
||||
0,
|
||||
status: IntroCategory._parseStatus(json['status']),
|
||||
contents: json['contents'] is List
|
||||
? List<dynamic>.from(json['contents'] as List)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'categoryId': categoryId,
|
||||
'categoryType': categoryType,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'mediaType': mediaType,
|
||||
'mediaUrl': mediaUrl,
|
||||
'sortOrder': sort,
|
||||
'status': status,
|
||||
'contents': contents,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 介绍页详情模型。
|
||||
///
|
||||
/// 对应后端 /company/introDetail/* 接口。
|
||||
///
|
||||
/// 后端实际字段:
|
||||
/// - id, subcategoryId, subcategoryTitle, mediaType, mediaUrl,
|
||||
/// title, description, status, sortOrder, createTime, updateTime
|
||||
class IntroDetail {
|
||||
const IntroDetail({
|
||||
required this.id,
|
||||
required this.subcategoryId,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.mediaType,
|
||||
required this.mediaUrl,
|
||||
required this.sort,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
/// 详情 ID。
|
||||
final String id;
|
||||
|
||||
/// 所属子分类 ID。
|
||||
final String subcategoryId;
|
||||
|
||||
/// 标题。
|
||||
final String title;
|
||||
|
||||
/// 描述/内容。
|
||||
final String description;
|
||||
|
||||
/// 媒体类型:image / video。
|
||||
final String mediaType;
|
||||
|
||||
/// 媒体地址。
|
||||
final String mediaUrl;
|
||||
|
||||
/// 排序号(后端字段 sortOrder)。
|
||||
final int sort;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
factory IntroDetail.fromJson(Map<String, dynamic> json) {
|
||||
return IntroDetail(
|
||||
id: json['id']?.toString() ?? '',
|
||||
subcategoryId: json['subcategoryId']?.toString() ?? '',
|
||||
title: json['title']?.toString() ??
|
||||
json['name']?.toString() ??
|
||||
json['subcategoryTitle']?.toString() ??
|
||||
'',
|
||||
description: json['description']?.toString() ??
|
||||
json['content']?.toString() ??
|
||||
'',
|
||||
mediaType: json['mediaType']?.toString() ?? '',
|
||||
mediaUrl: json['mediaUrl']?.toString() ??
|
||||
json['imageUrl']?.toString() ??
|
||||
json['videoUrl']?.toString() ??
|
||||
'',
|
||||
sort: int.tryParse(json['sortOrder']?.toString() ??
|
||||
json['sort']?.toString() ??
|
||||
'0') ??
|
||||
0,
|
||||
status: IntroCategory._parseStatus(json['status']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'subcategoryId': subcategoryId,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'mediaType': mediaType,
|
||||
'mediaUrl': mediaUrl,
|
||||
'sortOrder': sort,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
}
|
||||
132
lib/models/marquee_config.dart
Normal file
132
lib/models/marquee_config.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
/// 滚动字幕配置参数:速度档位与级别。
|
||||
///
|
||||
/// 与 Web 后台 `/views/modules/theme/scrolling-text.data.ts` 保持一致,
|
||||
/// 移动端仅展示档位文案,落库仍是原始数值,便于与 Web 端互相识别。
|
||||
///
|
||||
/// 速度(scrollSpeed,单位 ms/像素,数值越小滚动越快):
|
||||
/// - 慢 = 200
|
||||
/// - 较慢 = 100
|
||||
/// - 中 = 50
|
||||
/// - 较快 = 30
|
||||
/// - 快 = 10
|
||||
///
|
||||
/// 级别(level):
|
||||
/// - 危险 = 50
|
||||
/// - 紧急 = 40
|
||||
/// - 普通 = 20
|
||||
class MarqueeConfig {
|
||||
const MarqueeConfig({
|
||||
this.speed = MarqueeSpeed.medium,
|
||||
this.grade = MarqueeGrade.normal,
|
||||
});
|
||||
|
||||
final MarqueeSpeed speed;
|
||||
final MarqueeGrade grade;
|
||||
|
||||
/// 将任意历史 speed 值吸附到最近档位(兼容 Web 端精确值)。
|
||||
static MarqueeSpeed speedFromValue(int? value) {
|
||||
if (value == null) return MarqueeSpeed.medium;
|
||||
final target = MarqueeSpeed.values.fold<MarqueeSpeed>(
|
||||
MarqueeSpeed.values.first,
|
||||
(closest, current) {
|
||||
final diffClosest = (closest.value - value).abs();
|
||||
final diffCurrent = (current.value - value).abs();
|
||||
return diffCurrent < diffClosest ? current : closest;
|
||||
},
|
||||
);
|
||||
return target;
|
||||
}
|
||||
|
||||
/// 将任意历史 level 值吸附到最近级别。
|
||||
static MarqueeGrade gradeFromValue(int? value) {
|
||||
if (value == null) return MarqueeGrade.normal;
|
||||
final target = MarqueeGrade.values.fold<MarqueeGrade>(
|
||||
MarqueeGrade.values.first,
|
||||
(closest, current) {
|
||||
final diffClosest = (closest.value - value).abs();
|
||||
final diffCurrent = (current.value - value).abs();
|
||||
return diffCurrent < diffClosest ? current : closest;
|
||||
},
|
||||
);
|
||||
return target;
|
||||
}
|
||||
|
||||
MarqueeConfig setSpeed(MarqueeSpeed speed) =>
|
||||
MarqueeConfig(speed: speed, grade: grade);
|
||||
|
||||
MarqueeConfig setGrade(MarqueeGrade grade) =>
|
||||
MarqueeConfig(speed: speed, grade: grade);
|
||||
|
||||
MarqueeConfig copyWith({
|
||||
MarqueeSpeed? speed,
|
||||
MarqueeGrade? grade,
|
||||
}) =>
|
||||
MarqueeConfig(
|
||||
speed: speed ?? this.speed,
|
||||
grade: grade ?? this.grade,
|
||||
);
|
||||
}
|
||||
|
||||
/// 滚动速度档位。
|
||||
enum MarqueeSpeed {
|
||||
slow(200, '慢'),
|
||||
slower(100, '较慢'),
|
||||
medium(50, '中'),
|
||||
faster(30, '较快'),
|
||||
fast(10, '快');
|
||||
|
||||
const MarqueeSpeed(this.value, this.labelZh);
|
||||
|
||||
/// 后端落库值。
|
||||
final int value;
|
||||
|
||||
/// 中文展示文案。
|
||||
final String labelZh;
|
||||
|
||||
/// 英文展示文案。
|
||||
String get labelEn {
|
||||
switch (this) {
|
||||
case MarqueeSpeed.slow:
|
||||
return 'Slow';
|
||||
case MarqueeSpeed.slower:
|
||||
return 'Slower';
|
||||
case MarqueeSpeed.medium:
|
||||
return 'Medium';
|
||||
case MarqueeSpeed.faster:
|
||||
return 'Faster';
|
||||
case MarqueeSpeed.fast:
|
||||
return 'Fast';
|
||||
}
|
||||
}
|
||||
|
||||
String label(bool useEnglish) => useEnglish ? labelEn : labelZh;
|
||||
}
|
||||
|
||||
/// 滚动字幕级别。
|
||||
enum MarqueeGrade {
|
||||
danger(50, '危险'),
|
||||
urgent(40, '紧急'),
|
||||
normal(20, '普通');
|
||||
|
||||
const MarqueeGrade(this.value, this.labelZh);
|
||||
|
||||
/// 后端落库值。
|
||||
final int value;
|
||||
|
||||
/// 中文展示文案。
|
||||
final String labelZh;
|
||||
|
||||
/// 英文展示文案。
|
||||
String get labelEn {
|
||||
switch (this) {
|
||||
case MarqueeGrade.danger:
|
||||
return 'Danger';
|
||||
case MarqueeGrade.urgent:
|
||||
return 'Urgent';
|
||||
case MarqueeGrade.normal:
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
|
||||
String label(bool useEnglish) => useEnglish ? labelEn : labelZh;
|
||||
}
|
||||
211
lib/models/marquee_message.dart
Normal file
211
lib/models/marquee_message.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
/// 滚动字幕消息模型。
|
||||
///
|
||||
/// 对应 Web 后台「滚动字幕」表(tv_scrolling_text / theme_scrolling_text)
|
||||
/// 以及管理模块「滚动字幕管理」中的单条字幕记录。
|
||||
///
|
||||
/// 字段映射:
|
||||
/// - Web 端返回 `orgId` / `orgName`,本模型内部使用 `storeId` / `storeName`。
|
||||
/// - Web 端返回 `scrollSpeed`,本模型内部使用 `speed`。
|
||||
/// - Web 端返回 `bgColor`,本模型内部使用 `backgroundColor`。
|
||||
/// - `status` 沿用 Web 端语义:`1=启用`,`0=禁用`。
|
||||
class MarqueeMessage {
|
||||
const MarqueeMessage({
|
||||
required this.id,
|
||||
required this.storeId,
|
||||
required this.storeName,
|
||||
required this.content,
|
||||
required this.status,
|
||||
required this.speed,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
this.fontColor,
|
||||
this.backgroundColor,
|
||||
this.fontSize,
|
||||
this.level,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
});
|
||||
|
||||
/// 字幕 ID。
|
||||
final String id;
|
||||
|
||||
/// 所属门店 ID(对应 Web 端 orgId)。
|
||||
final String storeId;
|
||||
|
||||
/// 所属门店名称(对应 Web 端 orgName)。
|
||||
final String storeName;
|
||||
|
||||
/// 字幕内容。
|
||||
final String content;
|
||||
|
||||
/// 状态:`1=启用`,`0=禁用`。
|
||||
final int status;
|
||||
|
||||
/// 滚动速度(对应 Web 端 scrollSpeed,单位 ms/像素,越小越快)。
|
||||
///
|
||||
/// 档位映射:慢=200 / 较慢=100 / 中=50 / 较快=30 / 快=10。
|
||||
final int speed;
|
||||
|
||||
/// 字体颜色(Hex,如 `#FFFFFF`)。
|
||||
final String? fontColor;
|
||||
|
||||
/// 背景颜色(Hex,如 `#3B82F6`,对应 Web 端 bgColor)。
|
||||
final String? backgroundColor;
|
||||
|
||||
/// 字体大小(px,如 32)。
|
||||
final int? fontSize;
|
||||
|
||||
/// 字幕级别/优先级。
|
||||
final int? level;
|
||||
|
||||
/// 开始生效时间(Web 端为 `HH:mm:ss` 字符串,本地存为时间)。
|
||||
final DateTime? startTime;
|
||||
|
||||
/// 结束生效时间(Web 端为 `HH:mm:ss` 字符串,本地存为时间)。
|
||||
final DateTime? endTime;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
/// 更新时间。
|
||||
final DateTime? updateTime;
|
||||
|
||||
bool get isEnabled => status == 1;
|
||||
bool get isDisabled => status == 0;
|
||||
|
||||
factory MarqueeMessage.fromJson(Map<String, dynamic> json) {
|
||||
return MarqueeMessage(
|
||||
id: (json['id'] ?? json['marqueeId'])?.toString() ?? '',
|
||||
storeId: json['storeId']?.toString() ??
|
||||
json['orgId']?.toString() ??
|
||||
json['org_id']?.toString() ??
|
||||
'',
|
||||
storeName: json['storeName']?.toString() ??
|
||||
json['orgName']?.toString() ??
|
||||
json['org_name']?.toString() ??
|
||||
'',
|
||||
content: json['content']?.toString() ?? '',
|
||||
status: _parseInt(json['status'], fallback: 0) ?? 0,
|
||||
speed: _parseInt(json['speed'], fallback: 50) ??
|
||||
_parseInt(json['scrollSpeed'], fallback: 50) ??
|
||||
_parseInt(json['scroll_speed'], fallback: 50) ??
|
||||
50,
|
||||
fontColor: json['fontColor']?.toString() ?? json['font_color']?.toString(),
|
||||
backgroundColor: json['backgroundColor']?.toString() ??
|
||||
json['bgColor']?.toString() ??
|
||||
json['bg_color']?.toString(),
|
||||
fontSize: _parseInt(json['fontSize'] ?? json['font_size']),
|
||||
level: _parseInt(json['level']),
|
||||
startTime: _parseDateTime(json['startTime'] ?? json['start_time']),
|
||||
endTime: _parseDateTime(json['endTime'] ?? json['end_time']),
|
||||
createTime: _parseDateTime(json['createTime'] ?? json['create_time']),
|
||||
updateTime: _parseDateTime(json['updateTime'] ?? json['update_time']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为本地缓存使用的 JSON。
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'storeId': storeId,
|
||||
'storeName': storeName,
|
||||
'content': content,
|
||||
'status': status,
|
||||
'speed': speed,
|
||||
if (fontColor != null) 'fontColor': fontColor,
|
||||
if (backgroundColor != null) 'backgroundColor': backgroundColor,
|
||||
if (fontSize != null) 'fontSize': fontSize,
|
||||
if (level != null) 'level': level,
|
||||
if (startTime != null) 'startTime': _formatTime(startTime!),
|
||||
if (endTime != null) 'endTime': _formatTime(endTime!),
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
'updateTime': updateTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为 Web 后台可接收的字段名(用于新增/编辑请求)。
|
||||
Map<String, dynamic> toWebJson() {
|
||||
return {
|
||||
if (id.isNotEmpty) 'id': int.tryParse(id),
|
||||
'orgId': int.tryParse(storeId) ?? storeId,
|
||||
'orgName': storeName,
|
||||
'content': content,
|
||||
'scrollSpeed': speed,
|
||||
if (fontColor != null) 'fontColor': fontColor,
|
||||
if (backgroundColor != null) 'bgColor': backgroundColor,
|
||||
if (fontSize != null) 'fontSize': fontSize,
|
||||
if (level != null) 'level': level,
|
||||
'status': status,
|
||||
if (startTime != null) 'startTime': _formatTime(startTime!),
|
||||
if (endTime != null) 'endTime': _formatTime(endTime!),
|
||||
};
|
||||
}
|
||||
|
||||
MarqueeMessage copyWith({
|
||||
String? id,
|
||||
String? storeId,
|
||||
String? storeName,
|
||||
String? content,
|
||||
int? status,
|
||||
int? speed,
|
||||
String? fontColor,
|
||||
String? backgroundColor,
|
||||
int? fontSize,
|
||||
int? level,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
DateTime? createTime,
|
||||
DateTime? updateTime,
|
||||
}) {
|
||||
return MarqueeMessage(
|
||||
id: id ?? this.id,
|
||||
storeId: storeId ?? this.storeId,
|
||||
storeName: storeName ?? this.storeName,
|
||||
content: content ?? this.content,
|
||||
status: status ?? this.status,
|
||||
speed: speed ?? this.speed,
|
||||
fontColor: fontColor ?? this.fontColor,
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
level: level ?? this.level,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
createTime: createTime ?? this.createTime,
|
||||
updateTime: updateTime ?? this.updateTime,
|
||||
);
|
||||
}
|
||||
|
||||
/// 颜色字段兜底:当 Web 端未返回颜色时返回默认半透明色。
|
||||
static const String defaultFontColor = '#FFFFFF';
|
||||
static const String defaultBackgroundColor = '#3B82F6';
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss`、ISO 8601 以及纯 `HH:mm:ss` 时间字符串。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
// 纯时间:补齐一个固定日期用于本地 DateTime 存储。
|
||||
if (RegExp(r'^\d{2}:\d{2}:\d{2}$').hasMatch(text)) {
|
||||
return DateTime.tryParse('2000-01-01 $text');
|
||||
}
|
||||
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
/// 时间字段序列化为 `HH:mm:ss`,与 Web 后台保持一致。
|
||||
static String _formatTime(DateTime value) {
|
||||
return '${value.hour.toString().padLeft(2, '0')}:'
|
||||
'${value.minute.toString().padLeft(2, '0')}:'
|
||||
'${value.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 解析整数字段,兼容 int / String / null。
|
||||
static int? _parseInt(dynamic value, {int? fallback}) {
|
||||
if (value == null) return fallback;
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value.trim()) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
128
lib/models/media_asset.dart
Normal file
128
lib/models/media_asset.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
/// 媒体资源模型。
|
||||
///
|
||||
/// 对应后端 /sys/mediaAsset/* 接口的媒体实体。
|
||||
class MediaAsset {
|
||||
const MediaAsset({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.coverUrl,
|
||||
required this.playUrl,
|
||||
required this.duration,
|
||||
required this.status,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 媒体 ID。
|
||||
final String id;
|
||||
|
||||
/// 媒体编码。
|
||||
final String code;
|
||||
|
||||
/// 媒体名称。
|
||||
final String name;
|
||||
|
||||
/// 媒体类型(video / image / audio 等)。
|
||||
final String type;
|
||||
|
||||
/// 封面图地址。
|
||||
final String coverUrl;
|
||||
|
||||
/// 播放地址。
|
||||
final String playUrl;
|
||||
|
||||
/// 时长(秒)。
|
||||
final int duration;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory MediaAsset.fromJson(Map<String, dynamic> json) {
|
||||
return MediaAsset(
|
||||
id: json['id']?.toString() ?? '',
|
||||
code: json['code']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
type: json['type']?.toString() ?? '',
|
||||
coverUrl: json['coverUrl']?.toString() ?? '',
|
||||
playUrl: json['playUrl']?.toString() ?? '',
|
||||
duration: int.tryParse(json['duration']?.toString() ?? '0') ?? 0,
|
||||
status: int.tryParse(json['status']?.toString() ?? '0') ?? 0,
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'coverUrl': coverUrl,
|
||||
'playUrl': playUrl,
|
||||
'duration': duration,
|
||||
'status': status,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 媒体排行榜条目模型。
|
||||
///
|
||||
/// 对应后端 /sys/mediaAssetRank/* 接口的排行实体。
|
||||
class MediaRankItem {
|
||||
const MediaRankItem({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.mediaName,
|
||||
required this.sortOrder,
|
||||
required this.coverUrl,
|
||||
});
|
||||
|
||||
/// 条目 ID。
|
||||
final String id;
|
||||
|
||||
/// 媒体编码。
|
||||
final String code;
|
||||
|
||||
/// 媒体名称。
|
||||
final String mediaName;
|
||||
|
||||
/// 排序序号。
|
||||
final int sortOrder;
|
||||
|
||||
/// 封面图地址。
|
||||
final String coverUrl;
|
||||
|
||||
factory MediaRankItem.fromJson(Map<String, dynamic> json) {
|
||||
return MediaRankItem(
|
||||
id: json['id']?.toString() ?? '',
|
||||
code: (json['code'] ?? json['mediaCode'])?.toString() ?? '',
|
||||
mediaName: json['mediaName']?.toString() ?? '',
|
||||
sortOrder: int.tryParse(json['sortOrder']?.toString() ?? '0') ?? 0,
|
||||
coverUrl: json['coverUrl']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'mediaName': mediaName,
|
||||
'sortOrder': sortOrder,
|
||||
'coverUrl': coverUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
273
lib/models/org_node.dart
Normal file
273
lib/models/org_node.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
/// 组织节点模型。
|
||||
///
|
||||
/// 对应后端 /sys/org/* 接口的组织实体,支持树形结构。
|
||||
/// type 取值:0=平台 1=渠道 2=代理商 3=门店 4=设备分组。
|
||||
class OrgNode {
|
||||
const OrgNode({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.parentId,
|
||||
this.children = const [],
|
||||
required this.address,
|
||||
required this.contactPhone,
|
||||
required this.activationCode,
|
||||
this.province = '',
|
||||
this.city = '',
|
||||
this.themeId,
|
||||
DateTime? createdAt,
|
||||
this.expireTime,
|
||||
int? groupLimit,
|
||||
this.orderNo = 0,
|
||||
this.status = 'ACTIVE',
|
||||
this.mpath = '',
|
||||
}) : createdAt = createdAt,
|
||||
groupLimit = groupLimit;
|
||||
|
||||
/// 组织节点 ID。
|
||||
final String id;
|
||||
|
||||
/// 组织名称。
|
||||
final String name;
|
||||
|
||||
/// 类型(0=平台 1=渠道 2=代理商 3=门店 4=设备分组)。
|
||||
final int type;
|
||||
|
||||
/// 父级 ID。
|
||||
final String parentId;
|
||||
|
||||
/// 子节点列表(树形结构)。
|
||||
final List<OrgNode> children;
|
||||
|
||||
/// 地址。
|
||||
final String address;
|
||||
|
||||
/// 联系电话。
|
||||
final String contactPhone;
|
||||
|
||||
/// 省(新接口 /sys/org/tree 返回 province)。
|
||||
final String province;
|
||||
|
||||
/// 市(新接口 /sys/org/tree 返回 city)。
|
||||
final String city;
|
||||
|
||||
/// 主题 ID(新接口 /sys/org/tree 返回 themeId)。
|
||||
final int? themeId;
|
||||
|
||||
/// 激活码。
|
||||
final String activationCode;
|
||||
|
||||
/// 创建时间(组织树接口返回 createdAt,ISO8601 字符串)。
|
||||
///
|
||||
/// 门店列表需按建店时间倒序排列,故从组织树节点透传该字段;
|
||||
/// 老记录可能缺省(null),此时排序回退到 orgId 数值倒序。
|
||||
final DateTime? createdAt;
|
||||
|
||||
/// 到期时间(组织树/门店接口可能返回 expireTime/expire_time/expiredTime)。
|
||||
///
|
||||
/// 门店预览列表需展示到期状态(已过期 / 无到期时间),故从组织树节点透传该字段。
|
||||
final DateTime? expireTime;
|
||||
|
||||
/// 设备总数上限(组织树接口返回 groupLimit)。
|
||||
///
|
||||
/// 「建店时设置的设备上限」需回显并随门店保存,故从组织树节点透传。
|
||||
final int? groupLimit;
|
||||
|
||||
/// 排序号(组织接口返回 orderNo,渠道商管理页按此排序展示)。
|
||||
final int orderNo;
|
||||
|
||||
/// 状态:'ACTIVE'=启用,'DISABLED'=禁用。
|
||||
///
|
||||
/// 后端可能返回字符串(ACTIVE/DISABLED)或整型(0=正常/1=禁用,
|
||||
/// 与 [AppVersion.status] 约定一致),统一规整为字符串便于 UI 判断。
|
||||
final String status;
|
||||
|
||||
/// 组织编码(mpath 路径编码,渠道商管理页「组织编码」列展示)。
|
||||
///
|
||||
/// 后端未返回时回退为空串,UI 层再降级显示节点 id。
|
||||
final String mpath;
|
||||
|
||||
/// 后端组织类型字符串到数字的映射。
|
||||
/// 旧接口用 int:0=平台 1=渠道 2=代理商 3=门店 4=设备分组。
|
||||
/// 新接口 /sys/org/tree 用字符串 type:CHANNEL / SERVICE_PROVIDER / STORE / GROUP。
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601(含时区、毫秒)两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
/// 解析可空整型(设备上限 groupLimit 等)。
|
||||
static int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
return int.tryParse(value.toString());
|
||||
}
|
||||
|
||||
static int _parseType(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
final s = value.toString().toUpperCase();
|
||||
switch (s) {
|
||||
case 'CHANNEL':
|
||||
case '渠道':
|
||||
return 1;
|
||||
case 'SERVICE_PROVIDER':
|
||||
case '代理商':
|
||||
return 2;
|
||||
case 'STORE':
|
||||
case '门店':
|
||||
return 3;
|
||||
case 'GROUP':
|
||||
case '设备分组':
|
||||
return 4;
|
||||
default:
|
||||
return int.tryParse(s) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析组织状态,统一规整为 'ACTIVE' / 'DISABLED'。
|
||||
///
|
||||
/// 兼容后端两种形态:字符串(ACTIVE/DISABLED、启用/禁用)与整型
|
||||
/// (与 [AppVersion.status] 一致:0=正常 1=禁用)。
|
||||
static String _parseStatus(dynamic value) {
|
||||
if (value == null) return 'ACTIVE';
|
||||
if (value is int) return value == 0 ? 'ACTIVE' : 'DISABLED';
|
||||
final s = value.toString().toUpperCase();
|
||||
if (s == 'DISABLED' || s == '禁用') return 'DISABLED';
|
||||
return 'ACTIVE';
|
||||
}
|
||||
|
||||
factory OrgNode.fromJson(Map<String, dynamic> json) {
|
||||
final childrenRaw = json['list'] ?? json['children'];
|
||||
return OrgNode(
|
||||
// 对齐 Web 端 normalizeOrg:id 兼容 orgId / org_id,parentId 兼容 parent_id,
|
||||
// 避免后端偶发返回 orgId/parent_id 时节点 id 为空导致展开/编辑/删除失效。
|
||||
id: json['id']?.toString() ??
|
||||
json['orgId']?.toString() ??
|
||||
json['org_id']?.toString() ??
|
||||
'',
|
||||
name: json['name']?.toString() ?? '',
|
||||
type: _parseType(json['type']),
|
||||
parentId: json['parentId']?.toString() ??
|
||||
json['parent_id']?.toString() ??
|
||||
'',
|
||||
children: childrenRaw is List
|
||||
? childrenRaw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(OrgNode.fromJson)
|
||||
.toList()
|
||||
: const <OrgNode>[],
|
||||
address: json['address']?.toString() ?? '',
|
||||
contactPhone: json['contactPhone']?.toString() ?? json['phone']?.toString() ?? '',
|
||||
activationCode: json['activationCode']?.toString() ?? '',
|
||||
createdAt: _parseDateTime(
|
||||
json['createdAt'] ??
|
||||
json['createTime'] ??
|
||||
json['created_time'] ??
|
||||
json['created_at'],
|
||||
),
|
||||
expireTime: _parseDateTime(
|
||||
json['expireTime'] ?? json['expire_time'] ?? json['expiredTime'],
|
||||
),
|
||||
groupLimit: _parseInt(
|
||||
json['groupLimit'] ?? json['deviceLimit'] ?? json['device_limit'],
|
||||
),
|
||||
province: json['province']?.toString() ?? '',
|
||||
city: json['city']?.toString() ?? '',
|
||||
themeId: json['themeId'] is int
|
||||
? json['themeId'] as int
|
||||
: int.tryParse(json['themeId']?.toString() ?? ''),
|
||||
orderNo: _parseInt(json['orderNo'] ?? json['order_no']) ?? 0,
|
||||
status: _parseStatus(json['status']),
|
||||
mpath: json['mpath']?.toString() ??
|
||||
json['orgCode']?.toString() ??
|
||||
json['org_code']?.toString() ??
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'parentId': parentId,
|
||||
'children': children.map((e) => e.toJson()).toList(),
|
||||
'address': address,
|
||||
'contactPhone': contactPhone,
|
||||
'activationCode': activationCode,
|
||||
'province': province,
|
||||
'city': city,
|
||||
'themeId': themeId,
|
||||
'orderNo': orderNo,
|
||||
'status': status,
|
||||
'mpath': mpath,
|
||||
'expireTime': expireTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 不可变拷贝(主要用于渠道商管理页给节点挂载子节点树)。
|
||||
OrgNode copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
int? type,
|
||||
String? parentId,
|
||||
List<OrgNode>? children,
|
||||
String? address,
|
||||
String? contactPhone,
|
||||
String? activationCode,
|
||||
String? province,
|
||||
String? city,
|
||||
int? themeId,
|
||||
DateTime? createdAt,
|
||||
DateTime? expireTime,
|
||||
int? groupLimit,
|
||||
int? orderNo,
|
||||
String? status,
|
||||
String? mpath,
|
||||
}) {
|
||||
return OrgNode(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
parentId: parentId ?? this.parentId,
|
||||
children: children ?? this.children,
|
||||
address: address ?? this.address,
|
||||
contactPhone: contactPhone ?? this.contactPhone,
|
||||
activationCode: activationCode ?? this.activationCode,
|
||||
province: province ?? this.province,
|
||||
city: city ?? this.city,
|
||||
themeId: themeId ?? this.themeId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
expireTime: expireTime ?? this.expireTime,
|
||||
groupLimit: groupLimit ?? this.groupLimit,
|
||||
orderNo: orderNo ?? this.orderNo,
|
||||
status: status ?? this.status,
|
||||
mpath: mpath ?? this.mpath,
|
||||
);
|
||||
}
|
||||
|
||||
/// 类型展示文案(渠道商管理页徽章用)。
|
||||
String get typeLabel {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return '渠道';
|
||||
case 2:
|
||||
return '代理';
|
||||
case 3:
|
||||
return '门店';
|
||||
case 4:
|
||||
return '设备分组';
|
||||
default:
|
||||
return '平台';
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否已禁用。
|
||||
bool get isDisabled => status.toUpperCase() == 'DISABLED';
|
||||
|
||||
/// 「组织编码」列展示值:优先 mpath,缺省时降级为节点 id。
|
||||
String get orgCodeLabel => mpath.isNotEmpty ? mpath : id;
|
||||
}
|
||||
75
lib/models/schedule.dart
Normal file
75
lib/models/schedule.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
/// 定时任务模型。
|
||||
///
|
||||
/// 对应后端 /sys/schedule/* 接口的定时任务实体。
|
||||
/// status 取值:0=正常 1=暂停。
|
||||
class Schedule {
|
||||
const Schedule({
|
||||
required this.jobId,
|
||||
required this.beanName,
|
||||
required this.methodName,
|
||||
required this.params,
|
||||
required this.cronExpression,
|
||||
required this.status,
|
||||
required this.remark,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 任务 ID。
|
||||
final String jobId;
|
||||
|
||||
/// Spring Bean 名称。
|
||||
final String beanName;
|
||||
|
||||
/// 方法名称。
|
||||
final String methodName;
|
||||
|
||||
/// 方法参数。
|
||||
final String params;
|
||||
|
||||
/// Cron 表达式。
|
||||
final String cronExpression;
|
||||
|
||||
/// 状态(0=正常 1=暂停)。
|
||||
final int status;
|
||||
|
||||
/// 备注。
|
||||
final String remark;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory Schedule.fromJson(Map<String, dynamic> json) {
|
||||
return Schedule(
|
||||
jobId: (json['jobId'] ?? json['id'])?.toString() ?? '',
|
||||
beanName: json['beanName']?.toString() ?? '',
|
||||
methodName: json['methodName']?.toString() ?? '',
|
||||
params: json['params']?.toString() ?? '',
|
||||
cronExpression: json['cronExpression']?.toString() ?? '',
|
||||
status: int.tryParse(json['status']?.toString() ?? '0') ?? 0,
|
||||
remark: json['remark']?.toString() ?? '',
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'jobId': jobId,
|
||||
'beanName': beanName,
|
||||
'methodName': methodName,
|
||||
'params': params,
|
||||
'cronExpression': cronExpression,
|
||||
'status': status,
|
||||
'remark': remark,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
59
lib/models/store_config.dart
Normal file
59
lib/models/store_config.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
/// 门店配置(到期时间 / 禁用状态等)。
|
||||
///
|
||||
/// 对应后端 `GET /device/storeConfig/info/{orgId}`,与 Web 后台「门店配置」弹窗
|
||||
/// 同源(参考 screen-front `src/api/device.ts` 的 `getStoreConfig`)。
|
||||
///
|
||||
/// 重要:组织树 `/sys/org/tree` 与通用门店列表接口(`myStores` / `listByLoginOrg`)
|
||||
/// 通常**不返回** `expireTime`;移动端门店的到期时间必须由此端点补全,
|
||||
/// 否则会恒为「无到期时间」。
|
||||
class StoreConfig {
|
||||
const StoreConfig({
|
||||
this.expireTime,
|
||||
this.mediaVolume,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
/// 到期时间(null 表示不限/未设置)。
|
||||
final DateTime? expireTime;
|
||||
|
||||
/// 媒体区域音量(0-100,null 表示未设置)。
|
||||
final int? mediaVolume;
|
||||
|
||||
/// 是否禁用(后端用 bool 或 '1'/0)。
|
||||
final bool disabled;
|
||||
|
||||
factory StoreConfig.fromJson(Map<String, dynamic> json) {
|
||||
final rawDisabled = json['disabled'];
|
||||
final disabled = rawDisabled == true ||
|
||||
(rawDisabled is num && rawDisabled != 0) ||
|
||||
(rawDisabled is String && rawDisabled == '1');
|
||||
return StoreConfig(
|
||||
expireTime: _parseDateTime(
|
||||
json['expireTime'] ?? json['expire_time'] ?? json['expiredTime'],
|
||||
),
|
||||
mediaVolume: _parseInt(json['mediaVolume'] ?? json['media_volume']),
|
||||
disabled: disabled,
|
||||
);
|
||||
}
|
||||
|
||||
/// 空配置(接口异常/无数据时的占位)。
|
||||
static StoreConfig empty() => const StoreConfig();
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
/// 兼容 int / num / String 的整数解析(null 安全)。
|
||||
static int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is int) return value;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return int.tryParse(text);
|
||||
}
|
||||
}
|
||||
219
lib/models/store_info.dart
Normal file
219
lib/models/store_info.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
import '../data/region_data.dart';
|
||||
|
||||
/// 门店信息模型。
|
||||
///
|
||||
/// 对应后端 /sys/store/* 接口的门店实体。
|
||||
///
|
||||
/// 扩展字段([themeId]/[splashUrl]/[logoUrl]/省市区/[addressDetail])均为可空,
|
||||
/// 向后兼容既有后端响应;这些字段仅在「前端占位」阶段由本地缓存承载,
|
||||
/// 后续由后端提供后自动填充。
|
||||
class StoreInfo {
|
||||
const StoreInfo({
|
||||
required this.orgId,
|
||||
required this.name,
|
||||
required this.address,
|
||||
required this.contactPhone,
|
||||
required this.activationCode,
|
||||
required this.status,
|
||||
required this.expireTime,
|
||||
required this.createTime,
|
||||
this.parentId,
|
||||
this.themeId,
|
||||
this.splashUrl,
|
||||
this.logoUrl,
|
||||
this.provinceCode,
|
||||
this.cityCode,
|
||||
this.districtCode,
|
||||
this.addressDetail,
|
||||
int? deviceLimit,
|
||||
}) : deviceLimit = deviceLimit;
|
||||
|
||||
/// 门店组织 ID。
|
||||
final String orgId;
|
||||
|
||||
/// 父级组织 ID(所属渠道/代理商)。
|
||||
final String? parentId;
|
||||
|
||||
/// 门店名称。
|
||||
final String name;
|
||||
|
||||
/// 地址(兼容旧文本地址;新结构优先用省市区拼接)。
|
||||
final String address;
|
||||
|
||||
/// 联系电话。
|
||||
final String contactPhone;
|
||||
|
||||
/// 激活码。
|
||||
final String activationCode;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 到期时间。
|
||||
final DateTime? expireTime;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
/// 设备总数上限(组织树 groupLimit / 激活码 maxActivateCount)。
|
||||
final int? deviceLimit;
|
||||
|
||||
/// 主题 id(如 'ocean');前端占位,对应 [StoreThemePresets.byId]。
|
||||
final String? themeId;
|
||||
|
||||
/// 启动图本地路径 / 远程 URL(占位)。
|
||||
final String? splashUrl;
|
||||
|
||||
/// Logo 本地路径 / 远程 URL(占位)。
|
||||
final String? logoUrl;
|
||||
|
||||
/// 省 code(6 位)。
|
||||
final String? provinceCode;
|
||||
|
||||
/// 市 code。
|
||||
final String? cityCode;
|
||||
|
||||
/// 区 code。
|
||||
final String? districtCode;
|
||||
|
||||
/// 街道/门牌等详情文本。
|
||||
final String? addressDetail;
|
||||
|
||||
factory StoreInfo.fromJson(Map<String, dynamic> json) {
|
||||
return StoreInfo(
|
||||
orgId: (json['orgId'] ?? json['id'])?.toString() ?? '',
|
||||
parentId: json['parentId']?.toString() ??
|
||||
json['parent_id']?.toString() ??
|
||||
(json['parent'] is Map ? json['parent']['id']?.toString() : null),
|
||||
name: json['name']?.toString() ?? '',
|
||||
address: json['address']?.toString() ?? '',
|
||||
contactPhone: json['contactPhone']?.toString() ?? '',
|
||||
activationCode: json['activationCode']?.toString() ?? '',
|
||||
status: int.tryParse(json['status']?.toString() ?? '0') ?? 0,
|
||||
expireTime: _parseDateTime(
|
||||
json['expireTime'] ?? json['expire_time'] ?? json['expiredTime'],
|
||||
),
|
||||
createTime: _parseDateTime(
|
||||
json['createTime'] ??
|
||||
json['create_time'] ??
|
||||
json['createdTime'] ??
|
||||
json['createdAt'] ??
|
||||
json['created_at'],
|
||||
),
|
||||
deviceLimit: _parseInt(
|
||||
json['groupLimit'] ?? json['deviceLimit'] ?? json['device_limit'],
|
||||
),
|
||||
themeId: json['themeId']?.toString(),
|
||||
splashUrl: json['splashUrl']?.toString(),
|
||||
logoUrl: json['logoUrl']?.toString(),
|
||||
provinceCode: json['provinceCode']?.toString(),
|
||||
cityCode: json['cityCode']?.toString(),
|
||||
districtCode: json['districtCode']?.toString(),
|
||||
addressDetail: json['addressDetail']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
/// 解析可空整型(设备上限等)。
|
||||
static int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
return int.tryParse(value.toString());
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'orgId': orgId,
|
||||
'parentId': parentId,
|
||||
'name': name,
|
||||
'address': address,
|
||||
'contactPhone': contactPhone,
|
||||
'activationCode': activationCode,
|
||||
'status': status,
|
||||
'expireTime': expireTime?.toIso8601String(),
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
'deviceLimit': deviceLimit,
|
||||
'themeId': themeId,
|
||||
'splashUrl': splashUrl,
|
||||
'logoUrl': logoUrl,
|
||||
'provinceCode': provinceCode,
|
||||
'cityCode': cityCode,
|
||||
'districtCode': districtCode,
|
||||
'addressDetail': addressDetail,
|
||||
};
|
||||
}
|
||||
|
||||
/// 不可变更新副本(仅覆盖传入的非空参数)。
|
||||
///
|
||||
/// 用于门店列表加载后,用 [OrgService.getStoreListByLoginOrg] 回填的
|
||||
/// 到期时间等非组织树字段,避免重建整个对象。
|
||||
StoreInfo copyWith({
|
||||
String? orgId,
|
||||
String? parentId,
|
||||
String? name,
|
||||
String? address,
|
||||
String? contactPhone,
|
||||
String? activationCode,
|
||||
int? status,
|
||||
DateTime? expireTime,
|
||||
DateTime? createTime,
|
||||
int? deviceLimit,
|
||||
String? themeId,
|
||||
String? splashUrl,
|
||||
String? logoUrl,
|
||||
String? provinceCode,
|
||||
String? cityCode,
|
||||
String? districtCode,
|
||||
String? addressDetail,
|
||||
}) {
|
||||
return StoreInfo(
|
||||
orgId: orgId ?? this.orgId,
|
||||
parentId: parentId ?? this.parentId,
|
||||
name: name ?? this.name,
|
||||
address: address ?? this.address,
|
||||
contactPhone: contactPhone ?? this.contactPhone,
|
||||
activationCode: activationCode ?? this.activationCode,
|
||||
status: status ?? this.status,
|
||||
expireTime: expireTime ?? this.expireTime,
|
||||
createTime: createTime ?? this.createTime,
|
||||
deviceLimit: deviceLimit ?? this.deviceLimit,
|
||||
themeId: themeId ?? this.themeId,
|
||||
splashUrl: splashUrl ?? this.splashUrl,
|
||||
logoUrl: logoUrl ?? this.logoUrl,
|
||||
provinceCode: provinceCode ?? this.provinceCode,
|
||||
cityCode: cityCode ?? this.cityCode,
|
||||
districtCode: districtCode ?? this.districtCode,
|
||||
addressDetail: addressDetail ?? this.addressDetail,
|
||||
);
|
||||
}
|
||||
|
||||
/// 拼接展示地址:省名 + 市名 + 区名 + 详情文本。
|
||||
///
|
||||
/// 若已设置省市区则优先用编码反查名称拼接;否则回退到旧文本 [address]。
|
||||
String get regionText {
|
||||
final names = <String>[];
|
||||
if (provinceCode != null && provinceCode!.isNotEmpty) {
|
||||
final p = RegionData.instance.nameOf(provinceCode!);
|
||||
if (p.isNotEmpty) names.add(p);
|
||||
}
|
||||
if (cityCode != null && cityCode!.isNotEmpty) {
|
||||
final c = RegionData.instance.nameOf(cityCode!);
|
||||
if (c.isNotEmpty) names.add(c);
|
||||
}
|
||||
if (districtCode != null && districtCode!.isNotEmpty) {
|
||||
final d = RegionData.instance.nameOf(districtCode!);
|
||||
if (d.isNotEmpty) names.add(d);
|
||||
}
|
||||
final detail = (addressDetail ?? '').trim();
|
||||
if (detail.isNotEmpty) names.add(detail);
|
||||
if (names.isEmpty) return address;
|
||||
return names.join('');
|
||||
}
|
||||
}
|
||||
74
lib/models/store_theme.dart
Normal file
74
lib/models/store_theme.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 门店主题/皮肤枚举。
|
||||
///
|
||||
/// 仅定义「id + 名称 + 主色」,UI 统一引用 [id](`themeId`),不硬编码颜色。
|
||||
/// 当前为前端占位实现(纯色块 + 名称),后续可扩展为内置占位图。
|
||||
enum StoreTheme {
|
||||
/// 经典(默认)。
|
||||
classic,
|
||||
|
||||
/// 海洋蓝。
|
||||
ocean,
|
||||
|
||||
/// 森林绿。
|
||||
forest,
|
||||
|
||||
/// 日落橙。
|
||||
sunset,
|
||||
|
||||
/// 极简灰。
|
||||
mono;
|
||||
|
||||
/// 主题 id(小写字符串,用于网络传输与本地缓存)。
|
||||
String get id => name;
|
||||
|
||||
/// 主题中文名称。
|
||||
String get label {
|
||||
switch (this) {
|
||||
case StoreTheme.classic:
|
||||
return '经典';
|
||||
case StoreTheme.ocean:
|
||||
return '海洋蓝';
|
||||
case StoreTheme.forest:
|
||||
return '森林绿';
|
||||
case StoreTheme.sunset:
|
||||
return '日落橙';
|
||||
case StoreTheme.mono:
|
||||
return '极简灰';
|
||||
}
|
||||
}
|
||||
|
||||
/// 主题主色(用于色块预览与模拟主页头部)。
|
||||
Color get primary {
|
||||
switch (this) {
|
||||
case StoreTheme.classic:
|
||||
return const Color(0xFF1565C0);
|
||||
case StoreTheme.ocean:
|
||||
return const Color(0xFF00838F);
|
||||
case StoreTheme.forest:
|
||||
return const Color(0xFF2E7D32);
|
||||
case StoreTheme.sunset:
|
||||
return const Color(0xFFE65100);
|
||||
case StoreTheme.mono:
|
||||
return const Color(0xFF455A64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 内置主题清单与查询工具。
|
||||
class StoreThemePresets {
|
||||
StoreThemePresets._();
|
||||
|
||||
/// 全部内置主题(顺序即展示顺序)。
|
||||
static const List<StoreTheme> all = StoreTheme.values;
|
||||
|
||||
/// 按 id 查询主题;找不到返回 null。
|
||||
static StoreTheme? byId(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
for (final theme in all) {
|
||||
if (theme.id == id) return theme;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
41
lib/models/sys_config.dart
Normal file
41
lib/models/sys_config.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
/// 系统参数配置模型。
|
||||
///
|
||||
/// 对应后端 /sys/config/* 接口的配置实体。
|
||||
class SysConfig {
|
||||
const SysConfig({
|
||||
required this.id,
|
||||
required this.paramKey,
|
||||
required this.paramValue,
|
||||
required this.remark,
|
||||
});
|
||||
|
||||
/// 主键 ID。
|
||||
final String id;
|
||||
|
||||
/// 参数键名。
|
||||
final String paramKey;
|
||||
|
||||
/// 参数值。
|
||||
final String paramValue;
|
||||
|
||||
/// 备注。
|
||||
final String remark;
|
||||
|
||||
factory SysConfig.fromJson(Map<String, dynamic> json) {
|
||||
return SysConfig(
|
||||
id: json['id']?.toString() ?? '',
|
||||
paramKey: json['paramKey']?.toString() ?? '',
|
||||
paramValue: json['paramValue']?.toString() ?? '',
|
||||
remark: json['remark']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'paramKey': paramKey,
|
||||
'paramValue': paramValue,
|
||||
'remark': remark,
|
||||
};
|
||||
}
|
||||
}
|
||||
74
lib/models/sys_log.dart
Normal file
74
lib/models/sys_log.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
/// 系统操作日志模型。
|
||||
///
|
||||
/// 对应后端 /sys/log/* 接口的日志实体。
|
||||
class SysLog {
|
||||
const SysLog({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.operation,
|
||||
required this.method,
|
||||
required this.params,
|
||||
required this.time,
|
||||
required this.ip,
|
||||
required this.createDate,
|
||||
});
|
||||
|
||||
/// 日志 ID。
|
||||
final String id;
|
||||
|
||||
/// 操作用户名。
|
||||
final String username;
|
||||
|
||||
/// 操作描述。
|
||||
final String operation;
|
||||
|
||||
/// 请求方法(类名#方法名)。
|
||||
final String method;
|
||||
|
||||
/// 请求参数。
|
||||
final String params;
|
||||
|
||||
/// 执行时长(毫秒)。
|
||||
final int time;
|
||||
|
||||
/// 操作 IP。
|
||||
final String ip;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createDate;
|
||||
|
||||
factory SysLog.fromJson(Map<String, dynamic> json) {
|
||||
return SysLog(
|
||||
id: json['id']?.toString() ?? '',
|
||||
username: json['username']?.toString() ?? '',
|
||||
operation: json['operation']?.toString() ?? '',
|
||||
method: json['method']?.toString() ?? '',
|
||||
params: json['params']?.toString() ?? '',
|
||||
time: int.tryParse(json['time']?.toString() ?? '0') ?? 0,
|
||||
ip: json['ip']?.toString() ?? '',
|
||||
createDate: _parseDateTime(json['createDate']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'username': username,
|
||||
'operation': operation,
|
||||
'method': method,
|
||||
'params': params,
|
||||
'time': time,
|
||||
'ip': ip,
|
||||
'createDate': createDate?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
72
lib/models/sys_menu.dart
Normal file
72
lib/models/sys_menu.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
/// 系统菜单模型。
|
||||
///
|
||||
/// 对应后端 /sys/menu/* 接口的菜单实体。
|
||||
/// type 取值:0=目录 1=菜单 2=按钮。
|
||||
class SysMenu {
|
||||
const SysMenu({
|
||||
required this.menuId,
|
||||
required this.name,
|
||||
required this.url,
|
||||
required this.type,
|
||||
required this.icon,
|
||||
required this.parentId,
|
||||
required this.orderNum,
|
||||
required this.perms,
|
||||
required this.show,
|
||||
});
|
||||
|
||||
/// 菜单 ID。
|
||||
final String menuId;
|
||||
|
||||
/// 菜单名称。
|
||||
final String name;
|
||||
|
||||
/// 路由地址。
|
||||
final String url;
|
||||
|
||||
/// 类型(0=目录 1=菜单 2=按钮)。
|
||||
final int type;
|
||||
|
||||
/// 图标。
|
||||
final String icon;
|
||||
|
||||
/// 父级 ID。
|
||||
final String parentId;
|
||||
|
||||
/// 排序号。
|
||||
final int orderNum;
|
||||
|
||||
/// 权限标识。
|
||||
final String perms;
|
||||
|
||||
/// 是否显示(0=显示 1=隐藏)。
|
||||
final int show;
|
||||
|
||||
factory SysMenu.fromJson(Map<String, dynamic> json) {
|
||||
return SysMenu(
|
||||
menuId: (json['menuId'] ?? json['id'])?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
url: json['url']?.toString() ?? '',
|
||||
type: int.tryParse(json['type']?.toString() ?? '0') ?? 0,
|
||||
icon: json['icon']?.toString() ?? '',
|
||||
parentId: json['parentId']?.toString() ?? '',
|
||||
orderNum: int.tryParse(json['orderNum']?.toString() ?? '0') ?? 0,
|
||||
perms: json['perms']?.toString() ?? '',
|
||||
show: int.tryParse(json['show']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'menuId': menuId,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'type': type,
|
||||
'icon': icon,
|
||||
'parentId': parentId,
|
||||
'orderNum': orderNum,
|
||||
'perms': perms,
|
||||
'show': show,
|
||||
};
|
||||
}
|
||||
}
|
||||
84
lib/models/sys_role.dart
Normal file
84
lib/models/sys_role.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
/// 系统角色模型。
|
||||
///
|
||||
/// 对应后端 /sys/role/* 接口的角色实体。
|
||||
class SysRole {
|
||||
const SysRole({
|
||||
required this.roleId,
|
||||
required this.roleName,
|
||||
required this.remark,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 角色 ID。
|
||||
final String roleId;
|
||||
|
||||
/// 角色名称。
|
||||
final String roleName;
|
||||
|
||||
/// 备注。
|
||||
final String remark;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory SysRole.fromJson(Map<String, dynamic> json) {
|
||||
return SysRole(
|
||||
roleId: (json['roleId'] ?? json['id'])?.toString() ?? '',
|
||||
roleName: json['roleName']?.toString() ?? '',
|
||||
remark: json['remark']?.toString() ?? '',
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'roleId': roleId,
|
||||
'roleName': roleName,
|
||||
'remark': remark,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色-菜单关联模型(用于角色权限分配)。
|
||||
class RoleMenu {
|
||||
const RoleMenu({
|
||||
required this.id,
|
||||
required this.roleId,
|
||||
required this.menuId,
|
||||
});
|
||||
|
||||
/// 主键 ID。
|
||||
final String id;
|
||||
|
||||
/// 角色 ID。
|
||||
final String roleId;
|
||||
|
||||
/// 菜单 ID。
|
||||
final String menuId;
|
||||
|
||||
factory RoleMenu.fromJson(Map<String, dynamic> json) {
|
||||
return RoleMenu(
|
||||
id: json['id']?.toString() ?? '',
|
||||
roleId: json['roleId']?.toString() ?? '',
|
||||
menuId: json['menuId']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'roleId': roleId,
|
||||
'menuId': menuId,
|
||||
};
|
||||
}
|
||||
}
|
||||
218
lib/models/sys_user.dart
Normal file
218
lib/models/sys_user.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
/// 系统用户模型。
|
||||
///
|
||||
/// 对应后端 /sys/user/* 接口的用户实体。
|
||||
class SysUser {
|
||||
const SysUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.realName,
|
||||
required this.email,
|
||||
required this.mobile,
|
||||
required this.status,
|
||||
required this.roleId,
|
||||
required this.roleName,
|
||||
required this.roleNames,
|
||||
required this.orgId,
|
||||
required this.orgName,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 用户 ID。
|
||||
final String id;
|
||||
|
||||
/// 登录账号。
|
||||
final String username;
|
||||
|
||||
/// 真实姓名。
|
||||
final String realName;
|
||||
|
||||
/// 邮箱。
|
||||
final String email;
|
||||
|
||||
/// 手机号。
|
||||
final String mobile;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 角色 ID。
|
||||
final String roleId;
|
||||
|
||||
/// 角色名称。
|
||||
final String roleName;
|
||||
|
||||
/// 角色名称列表(后端部分接口返回 roleNames 数组)。
|
||||
final List<String> roleNames;
|
||||
|
||||
/// 所属组织 ID。
|
||||
final String orgId;
|
||||
|
||||
/// 所属组织名称(门店 / 渠道商 / 代理商名称)。
|
||||
final String orgName;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory SysUser.fromJson(Map<String, dynamic> json) {
|
||||
final roleList = _parseStringList(json['roleIdList'] ?? json['roleIds']);
|
||||
final roleNameFromObj = _extractNestedName(
|
||||
json['roles'] ?? json['role'] ?? json['sysRoles'] ?? json['sysRole'],
|
||||
const ['roleName', 'name', 'role'],
|
||||
);
|
||||
final roleIdFromObj = _extractNestedName(
|
||||
json['roles'] ?? json['role'] ?? json['sysRoles'] ?? json['sysRole'],
|
||||
const ['roleId', 'id'],
|
||||
);
|
||||
final roleNames = _parseStringList(
|
||||
json['roleNames'] ?? json['roleName'] ?? roleNameFromObj,
|
||||
);
|
||||
final roleId = json['roleId']?.toString() ??
|
||||
roleIdFromObj ??
|
||||
(roleList.isNotEmpty ? roleList.first : '');
|
||||
|
||||
final orgNameFromObj = _extractNestedName(
|
||||
json['org'] ??
|
||||
json['dept'] ??
|
||||
json['sysOrg'] ??
|
||||
json['sysDept'] ??
|
||||
json['organization'],
|
||||
const ['orgName', 'name', 'deptName', 'org'],
|
||||
);
|
||||
final orgIdFromObj = _extractNestedName(
|
||||
json['org'] ??
|
||||
json['dept'] ??
|
||||
json['sysOrg'] ??
|
||||
json['sysDept'] ??
|
||||
json['organization'],
|
||||
const ['orgId', 'id', 'deptId'],
|
||||
);
|
||||
|
||||
return SysUser(
|
||||
id: json['id']?.toString() ?? json['userId']?.toString() ?? '',
|
||||
username: json['username']?.toString() ?? '',
|
||||
realName: json['realName']?.toString() ?? '',
|
||||
email: json['email']?.toString() ?? '',
|
||||
mobile: json['mobile']?.toString() ?? '',
|
||||
status: _parseStatus(json['status']),
|
||||
roleId: roleId,
|
||||
roleName: json['roleName']?.toString() ??
|
||||
(roleNames.isNotEmpty ? roleNames.first : ''),
|
||||
roleNames: roleNames.isNotEmpty ? roleNames : roleList,
|
||||
orgId: json['orgId']?.toString() ?? orgIdFromObj ?? '',
|
||||
orgName: json['orgName']?.toString() ??
|
||||
json['storeName']?.toString() ??
|
||||
json['deptName']?.toString() ??
|
||||
orgNameFromObj ??
|
||||
'',
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 从嵌套对象或对象列表中提取名称/ID 字段。
|
||||
/// 用于兼容后端把角色/组织以对象/对象数组返回的情况。
|
||||
static String? _extractNestedName(dynamic value, List<String> keys) {
|
||||
if (value is Map) {
|
||||
final map = value as Map<String, dynamic>;
|
||||
for (final key in keys) {
|
||||
final v = map[key]?.toString();
|
||||
if (v != null && v.isNotEmpty) return v;
|
||||
}
|
||||
}
|
||||
if (value is List && value.isNotEmpty) {
|
||||
return _extractNestedName(value.first, keys);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 兼容 int / String / bool 三种状态表示:
|
||||
/// - int: 1=正常, 0=禁用(新后台 /sys/user/list 返回 status=1 为启用)
|
||||
/// - String: '0'/'正常'/'启用'/'true'=正常, 其他=禁用
|
||||
/// - bool: true=正常, false=禁用
|
||||
static int _parseStatus(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is bool) return value ? 0 : 1;
|
||||
if (value is int) {
|
||||
// 新后台:1=启用/正常,0=禁用/停用。
|
||||
return value == 1 ? 0 : 1;
|
||||
}
|
||||
final text = value.toString().trim().toLowerCase();
|
||||
if (text.isEmpty ||
|
||||
text == '0' ||
|
||||
text == '正常' ||
|
||||
text == '启用' ||
|
||||
text == 'true' ||
|
||||
text == 'ok' ||
|
||||
text == 'enabled' ||
|
||||
text == '1') {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static List<String> _parseStringList(dynamic value) {
|
||||
if (value is List) {
|
||||
return value.map((e) => e?.toString() ?? '').where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return [value];
|
||||
}
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'username': username,
|
||||
'realName': realName,
|
||||
'email': email,
|
||||
'mobile': mobile,
|
||||
'status': status,
|
||||
'roleId': roleId,
|
||||
'roleName': roleName,
|
||||
'roleNames': roleNames,
|
||||
'orgId': orgId,
|
||||
'orgName': orgName,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 复制并覆盖指定字段,用于列表层批量补全角色信息(/_enrichRoles)。
|
||||
SysUser copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
String? realName,
|
||||
String? email,
|
||||
String? mobile,
|
||||
int? status,
|
||||
String? roleId,
|
||||
String? roleName,
|
||||
List<String>? roleNames,
|
||||
String? orgId,
|
||||
String? orgName,
|
||||
DateTime? createTime,
|
||||
}) {
|
||||
return SysUser(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
realName: realName ?? this.realName,
|
||||
email: email ?? this.email,
|
||||
mobile: mobile ?? this.mobile,
|
||||
status: status ?? this.status,
|
||||
roleId: roleId ?? this.roleId,
|
||||
roleName: roleName ?? this.roleName,
|
||||
roleNames: roleNames ?? this.roleNames,
|
||||
orgId: orgId ?? this.orgId,
|
||||
orgName: orgName ?? this.orgName,
|
||||
createTime: createTime ?? this.createTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/models/theme_group.dart
Normal file
94
lib/models/theme_group.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
/// 门店分组下的主题页面(对应 web 后台 /theme/page/org/{orgId}/photos)。
|
||||
class ThemePage {
|
||||
const ThemePage({
|
||||
required this.themeId,
|
||||
required this.name,
|
||||
this.themePhoto,
|
||||
this.active = false,
|
||||
this.orderNo,
|
||||
});
|
||||
|
||||
/// 主题 ID(tv_theme.id)。
|
||||
final int themeId;
|
||||
|
||||
/// 主题名称。
|
||||
final String name;
|
||||
|
||||
/// 主题预览图 URL。
|
||||
final String? themePhoto;
|
||||
|
||||
/// 是否为当前分组激活主题。
|
||||
final bool active;
|
||||
|
||||
/// 排序号。
|
||||
final int? orderNo;
|
||||
|
||||
factory ThemePage.fromJson(Map<String, dynamic> json) {
|
||||
final rawActive = json['active'];
|
||||
return ThemePage(
|
||||
themeId: (json['themeId'] ?? json['theme_id'] ?? json['id'] ?? 0) as int,
|
||||
name: (json['name'] ?? json['themeName'] ?? '')?.toString() ?? '',
|
||||
themePhoto: (json['themePhoto'] ?? json['theme_photo'])?.toString(),
|
||||
active: rawActive == true ||
|
||||
(rawActive is num && rawActive.toInt() == 1) ||
|
||||
(rawActive is String && rawActive.toLowerCase() == 'true'),
|
||||
orderNo: (json['orderNo'] ?? json['order_no']) is num
|
||||
? ((json['orderNo'] ?? json['order_no']) as num).toInt()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 门店设备分组及其主题列表(界面配置)。
|
||||
class ThemeGroup {
|
||||
const ThemeGroup({
|
||||
required this.groupId,
|
||||
required this.groupName,
|
||||
required this.themes,
|
||||
this.themeLimit = 6,
|
||||
});
|
||||
|
||||
/// 设备分组 ID。
|
||||
final int groupId;
|
||||
|
||||
/// 设备分组名称。
|
||||
final String groupName;
|
||||
|
||||
/// 分组下主题数量上限。
|
||||
final int themeLimit;
|
||||
|
||||
/// 分组下已添加的主题列表。
|
||||
final List<ThemePage> themes;
|
||||
|
||||
factory ThemeGroup.fromJson(Map<String, dynamic> json) {
|
||||
final themesRaw = json['themes'];
|
||||
final themes = themesRaw is List
|
||||
? themesRaw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ThemePage.fromJson)
|
||||
.where((t) => t.themeId > 0)
|
||||
.toList()
|
||||
: <ThemePage>[];
|
||||
themes.sort((a, b) => (a.orderNo ?? 0).compareTo(b.orderNo ?? 0));
|
||||
|
||||
final rawLimit = json['themeLimit'] ?? json['theme_limit'];
|
||||
final themeLimit = rawLimit is num && rawLimit.toInt() > 0
|
||||
? rawLimit.toInt()
|
||||
: 6;
|
||||
|
||||
return ThemeGroup(
|
||||
groupId: (json['groupId'] ?? json['group_id'] ?? 0) as int,
|
||||
groupName: (json['groupName'] ?? json['group_name'] ?? '')?.toString() ??
|
||||
'未命名分组',
|
||||
themeLimit: themeLimit,
|
||||
themes: themes,
|
||||
);
|
||||
}
|
||||
|
||||
/// 当前激活的主题名称(空表示未激活任何主题)。
|
||||
String? get activeThemeName {
|
||||
final active = themes.where((t) => t.active).toList();
|
||||
if (active.isEmpty) return null;
|
||||
return active.first.name;
|
||||
}
|
||||
}
|
||||
55
lib/models/theme_template.dart
Normal file
55
lib/models/theme_template.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
/// 主题模板(对应 web 后台 /theme/themetemplate/* 接口)。
|
||||
///
|
||||
/// 用于门店表单中「主题皮肤」选择;门店的 themeId 通常即为模板 id。
|
||||
class ThemeTemplate {
|
||||
const ThemeTemplate({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.coverUrl,
|
||||
this.status = 'ENABLED',
|
||||
});
|
||||
|
||||
/// 模板 ID(对应门店 themeId)。
|
||||
final String id;
|
||||
|
||||
/// 模板名称。
|
||||
final String name;
|
||||
|
||||
/// 模板描述。
|
||||
final String? description;
|
||||
|
||||
/// 封面预览图 URL。
|
||||
final String? coverUrl;
|
||||
|
||||
/// 状态:ENABLED / DISABLED。
|
||||
final String status;
|
||||
|
||||
factory ThemeTemplate.fromJson(Map<String, dynamic> json) {
|
||||
return ThemeTemplate(
|
||||
id: json['id']?.toString() ??
|
||||
json['templateId']?.toString() ??
|
||||
json['themeTemplateId']?.toString() ??
|
||||
'',
|
||||
name: json['name']?.toString() ??
|
||||
json['templateName']?.toString() ??
|
||||
'',
|
||||
description: json['description']?.toString(),
|
||||
coverUrl: json['coverUrl']?.toString() ??
|
||||
json['cover_url']?.toString(),
|
||||
status: json['status']?.toString() ?? 'ENABLED',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (coverUrl != null) 'coverUrl': coverUrl,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
bool get isEnabled => status.toUpperCase() == 'ENABLED';
|
||||
}
|
||||
80
lib/models/tv_app.dart
Normal file
80
lib/models/tv_app.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
/// TV 应用模型。
|
||||
///
|
||||
/// 对应后端 /theme/tvapp/* 接口的 TV 应用实体。
|
||||
class TvApp {
|
||||
const TvApp({
|
||||
required this.id,
|
||||
required this.appName,
|
||||
required this.packageName,
|
||||
required this.appCategory,
|
||||
required this.iconUrl,
|
||||
required this.orgId,
|
||||
required this.groupId,
|
||||
required this.status,
|
||||
required this.createTime,
|
||||
});
|
||||
|
||||
/// 应用 ID。
|
||||
final String id;
|
||||
|
||||
/// 应用名称。
|
||||
final String appName;
|
||||
|
||||
/// 包名。
|
||||
final String packageName;
|
||||
|
||||
/// 应用分类。
|
||||
final String appCategory;
|
||||
|
||||
/// 图标地址。
|
||||
final String iconUrl;
|
||||
|
||||
/// 组织 ID。
|
||||
final String orgId;
|
||||
|
||||
/// 设备分组 ID。
|
||||
final String groupId;
|
||||
|
||||
/// 状态(0=正常 1=禁用)。
|
||||
final int status;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime? createTime;
|
||||
|
||||
factory TvApp.fromJson(Map<String, dynamic> json) {
|
||||
return TvApp(
|
||||
id: json['id']?.toString() ?? '',
|
||||
appName: json['appName']?.toString() ?? '',
|
||||
packageName: json['packageName']?.toString() ?? '',
|
||||
appCategory: json['appCategory']?.toString() ?? '',
|
||||
iconUrl: json['iconUrl']?.toString() ?? '',
|
||||
orgId: json['orgId']?.toString() ?? '',
|
||||
groupId: json['groupId']?.toString() ?? '',
|
||||
status: int.tryParse(json['status']?.toString() ?? '0') ?? 0,
|
||||
createTime: _parseDateTime(json['createTime']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601 两种时间格式。
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return DateTime.tryParse(text) ??
|
||||
DateTime.tryParse(text.replaceFirst(' ', 'T'));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'appName': appName,
|
||||
'packageName': packageName,
|
||||
'appCategory': appCategory,
|
||||
'iconUrl': iconUrl,
|
||||
'orgId': orgId,
|
||||
'groupId': groupId,
|
||||
'status': status,
|
||||
'createTime': createTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
76
lib/models/user.dart
Normal file
76
lib/models/user.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
/// 用户模型。
|
||||
///
|
||||
/// 字段由 [AuthService] 从真实登录响应(/sys/login)与
|
||||
/// 当前用户接口(/sys/user/info)映射而来。
|
||||
class User {
|
||||
const User({
|
||||
required this.id,
|
||||
required this.account,
|
||||
required this.displayName,
|
||||
required this.role,
|
||||
required this.token,
|
||||
this.orgId = '',
|
||||
this.roleId = '',
|
||||
this.orgType = 0,
|
||||
this.superAdmin = false,
|
||||
});
|
||||
|
||||
/// 用户唯一标识(真实模式下取 orgId,缺省回退为账号)。
|
||||
final String id;
|
||||
|
||||
/// 登录账号。
|
||||
final String account;
|
||||
|
||||
/// 显示名称。
|
||||
final String displayName;
|
||||
|
||||
/// 角色(如:运维管理员;多个角色以 `/` 连接)。
|
||||
final String role;
|
||||
|
||||
/// 登录令牌。
|
||||
final String token;
|
||||
|
||||
/// 当前用户所属组织 ID(用于表单默认选中)。
|
||||
final String orgId;
|
||||
|
||||
/// 当前用户角色 ID(用于表单默认选中)。
|
||||
final String roleId;
|
||||
|
||||
/// 当前用户所属组织类型(来自登录响应 orgType):
|
||||
/// 0=平台(管理员) 1=渠道(渠道商) 2=代理商。
|
||||
/// 用于按账号类别显隐功能模块(用户管理 / 渠道商管理)。
|
||||
final int orgType;
|
||||
|
||||
/// 是否超级管理员(来自登录响应 superAdmin);为 true 视为管理员。
|
||||
final bool superAdmin;
|
||||
|
||||
/// 序列化为 JSON(用于本地持久化)。
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'account': account,
|
||||
'displayName': displayName,
|
||||
'role': role,
|
||||
'token': token,
|
||||
'orgId': orgId,
|
||||
'roleId': roleId,
|
||||
'orgType': orgType,
|
||||
'superAdmin': superAdmin,
|
||||
};
|
||||
|
||||
/// 从 JSON 反序列化。
|
||||
factory User.fromJson(Map<String, dynamic> json) => User(
|
||||
id: json['id']?.toString() ?? '',
|
||||
account: json['account']?.toString() ?? '',
|
||||
displayName: json['displayName']?.toString() ?? '',
|
||||
role: json['role']?.toString() ?? '',
|
||||
token: json['token']?.toString() ?? '',
|
||||
orgId: json['orgId']?.toString() ?? '',
|
||||
roleId: json['roleId']?.toString() ?? '',
|
||||
orgType: json['orgType'] is int
|
||||
? json['orgType'] as int
|
||||
: int.tryParse(json['orgType']?.toString() ?? '') ?? 0,
|
||||
superAdmin: json['superAdmin'] is bool
|
||||
? json['superAdmin'] as bool
|
||||
: false,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user