Files
CloudHome-Ops/lib/models/org_node.dart

274 lines
8.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 组织节点模型。
///
/// 对应后端 /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;
}