ops-console v1.0.2 (versionCode 8) 源码入库

This commit is contained in:
txh
2026-09-03 18:01:05 +08:00
commit 685fff27c8
170 changed files with 53049 additions and 0 deletions

95
lib/models/alert.dart Normal file
View 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,
};
}
}

View 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
View 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,
};
}
}

View 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,
};
}
}

View 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;
}

View 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
View 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
View 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
View 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(),
};
}
}

View 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
View 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('');
}
}

View 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;
}
}

View 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
View 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
View 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
View 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
View 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,
);
}
}

View 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;
}
}

View 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
View 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
View 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,
);
}