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