This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
class Group {
final String id;
final String name;
final String? avatar;
final String? description;
final String? owner;
final List<GroupMember> members;
final List<GroupPanel> panels;
final DateTime? createdAt;
final DateTime? updatedAt;
Group({
required this.id,
required this.name,
this.avatar,
this.description,
this.owner,
this.members = const [],
this.panels = const [],
this.createdAt,
this.updatedAt,
});
factory Group.fromJson(Map<String, dynamic> json) {
return Group(
id: (json['_id'] ?? json['id'] ?? '').toString(),
name: json['name'] ?? '',
avatar: json['avatar'],
description: json['description'],
owner: json['owner']?.toString(),
members: (json['members'] as List?)
?.map((e) => GroupMember.fromJson(e))
.toList() ??
[],
panels: (json['panels'] as List?)
?.map((e) => GroupPanel.fromJson(e))
.toList() ??
[],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'].toString())
: null,
);
}
Map<String, dynamic> toJson() {
return {
'_id': id,
'name': name,
'avatar': avatar,
'description': description,
'owner': owner,
'members': members.map((e) => e.toJson()).toList(),
'panels': panels.map((e) => e.toJson()).toList(),
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
/// 群组成员数量
int get memberCount => members.length;
/// 获取第一个文本面板(频道)用于聊天
GroupPanel? get firstTextPanel {
try {
return panels.firstWhere((p) => p.type == 0);
} catch (_) {
return panels.isNotEmpty ? panels.first : null;
}
}
}
class GroupMember {
final String? userId;
final List<String>? roles;
final DateTime? muteUntil;
GroupMember({
this.userId,
this.roles,
this.muteUntil,
});
factory GroupMember.fromJson(Map<String, dynamic> json) {
return GroupMember(
userId: json['userId']?.toString(),
roles: (json['roles'] as List?)?.map((e) => e.toString()).toList(),
muteUntil: json['muteUntil'] != null
? DateTime.parse(json['muteUntil'].toString())
: null,
);
}
Map<String, dynamic> toJson() {
return {
'userId': userId,
'roles': roles,
'muteUntil': muteUntil?.toIso8601String(),
};
}
}
/// 群组面板类型:
/// 0 = 文本频道
/// 1 = 面板组(文件夹)
/// 2 = 插件面板
class GroupPanel {
final String id;
final String name;
final String? parentId;
final int type;
final String? provider;
final String? pluginPanelName;
final Map<String, dynamic>? meta;
GroupPanel({
required this.id,
required this.name,
this.parentId,
this.type = 0,
this.provider,
this.pluginPanelName,
this.meta,
});
factory GroupPanel.fromJson(Map<String, dynamic> json) {
return GroupPanel(
id: (json['id'] ?? '').toString(),
name: json['name'] ?? '',
parentId: json['parentId']?.toString(),
type: json['type'] ?? 0,
provider: json['provider'],
pluginPanelName: json['pluginPanelName'],
meta: json['meta'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'parentId': parentId,
'type': type,
'provider': provider,
'pluginPanelName': pluginPanelName,
'meta': meta,
};
}
}
/// getGroupBasicInfo 返回的基本群组信息
class GroupBasicInfo {
final String groupId;
final String name;
final String? avatar;
final String? description;
final int memberCount;
final String? backgroundImage;
GroupBasicInfo({
required this.groupId,
required this.name,
this.avatar,
this.description,
this.memberCount = 0,
this.backgroundImage,
});
factory GroupBasicInfo.fromJson(Map<String, dynamic> json) {
return GroupBasicInfo(
groupId: (json['groupId'] ?? '').toString(),
name: json['name'] ?? '',
avatar: json['avatar'],
description: json['description'],
memberCount: json['memberCount'] ?? 0,
backgroundImage: json['backgroundImage'],
);
}
}

View File

@@ -0,0 +1,166 @@
class Invite {
final String id;
final String code;
final String salesId;
final String groupId;
final String? link;
final String? qrCodeUrl;
final DateTime? createdAt;
final DateTime? expiresAt;
final int clickCount;
final int scanCount;
final int joinCount;
final String status;
Invite({
required this.id,
required this.code,
required this.salesId,
required this.groupId,
this.link,
this.qrCodeUrl,
this.createdAt,
this.expiresAt,
this.clickCount = 0,
this.scanCount = 0,
this.joinCount = 0,
this.status = 'active',
});
factory Invite.fromJson(Map<String, dynamic> json) {
return Invite(
id: (json['_id'] ?? json['id'] ?? '').toString(),
code: json['code'] ?? '',
salesId: (json['salesId'] ?? '').toString(),
groupId: (json['groupId'] ?? '').toString(),
link: json['link'],
qrCodeUrl: json['qrCodeUrl'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'].toString())
: null,
clickCount: json['clickCount'] ?? 0,
scanCount: json['scanCount'] ?? 0,
joinCount: json['joinCount'] ?? 0,
status: json['status'] ?? 'active',
);
}
Map<String, dynamic> toJson() {
return {
'_id': id,
'code': code,
'salesId': salesId,
'groupId': groupId,
'link': link,
'qrCodeUrl': qrCodeUrl,
'createdAt': createdAt?.toIso8601String(),
'expiresAt': expiresAt?.toIso8601String(),
'clickCount': clickCount,
'scanCount': scanCount,
'joinCount': joinCount,
'status': status,
};
}
bool get isExpired =>
expiresAt != null && DateTime.now().isAfter(expiresAt!);
bool get isActive => status == 'active' && !isExpired;
bool get isValid => isActive;
}
/// 单个邀请的统计数据,由 invite.getStats 返回
class InviteStats {
final String code;
final String? link;
final DateTime? createdAt;
final DateTime? expiresAt;
final InviteStatsDetail stats;
final List<AccessLog> recentAccess;
InviteStats({
required this.code,
this.link,
this.createdAt,
this.expiresAt,
required this.stats,
this.recentAccess = const [],
});
factory InviteStats.fromJson(Map<String, dynamic> json) {
return InviteStats(
code: json['code'] ?? '',
link: json['link'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'].toString())
: null,
stats: InviteStatsDetail.fromJson(json['stats'] ?? {}),
recentAccess: (json['recentAccess'] as List?)
?.map((e) => AccessLog.fromJson(e))
.toList() ??
[],
);
}
}
class InviteStatsDetail {
final int clicks;
final int scans;
final int joins;
final String conversionRate;
InviteStatsDetail({
this.clicks = 0,
this.scans = 0,
this.joins = 0,
this.conversionRate = '0',
});
factory InviteStatsDetail.fromJson(Map<String, dynamic> json) {
return InviteStatsDetail(
clicks: json['clicks'] ?? 0,
scans: json['scans'] ?? 0,
joins: json['joins'] ?? 0,
conversionRate: json['conversionRate']?.toString() ?? '0',
);
}
double get conversionRateDouble => double.tryParse(conversionRate) ?? 0.0;
}
/// 来自后端的访问日志条目
class AccessLog {
final String? id;
final String inviteCode;
final String? visitorId;
final String accessType;
final DateTime? timestamp;
final String? ipAddress;
AccessLog({
this.id,
required this.inviteCode,
this.visitorId,
required this.accessType,
this.timestamp,
this.ipAddress,
});
factory AccessLog.fromJson(Map<String, dynamic> json) {
return AccessLog(
id: (json['_id'] ?? json['id'])?.toString(),
inviteCode: json['inviteCode'] ?? '',
visitorId: json['visitorId']?.toString(),
accessType: json['accessType'] ?? '',
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'].toString())
: null,
ipAddress: json['ipAddress'],
);
}
}

View File

@@ -0,0 +1,81 @@
class Message {
final String id;
final String content;
final String? author;
final String? groupId;
final String converseId;
final bool hasRecall;
final Map<String, dynamic>? meta;
final DateTime? createdAt;
final DateTime? updatedAt;
Message({
required this.id,
required this.content,
this.author,
this.groupId,
required this.converseId,
this.hasRecall = false,
this.meta,
this.createdAt,
this.updatedAt,
});
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
id: (json['_id'] ?? json['id'] ?? '').toString(),
content: json['content'] ?? '',
author: json['author']?.toString(),
groupId: json['groupId']?.toString(),
converseId: (json['converseId'] ?? '').toString(),
hasRecall: json['hasRecall'] ?? false,
meta: json['meta'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'].toString())
: null,
);
}
Map<String, dynamic> toJson() {
return {
'_id': id,
'content': content,
'author': author,
'groupId': groupId,
'converseId': converseId,
'hasRecall': hasRecall,
'meta': meta,
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
/// Determine message type from meta
MessageType get type {
if (hasRecall) return MessageType.recalled;
final metaType = meta?['type'];
if (metaType == 'image' || metaType == 'file') {
return MessageType.file;
}
if (metaType == 'system') {
return MessageType.system;
}
return MessageType.text;
}
/// Convenience alias for author (sender ID)
String get senderId => author ?? '';
/// For display purposes - get sender name from meta if available
String get senderName => meta?['nickname']?.toString() ?? '';
}
enum MessageType {
text,
file,
system,
recalled,
}

View File

@@ -0,0 +1,5 @@
export 'user.dart';
export 'message.dart';
export 'group.dart';
export 'invite.dart';
export 'stats.dart';

View File

@@ -0,0 +1,154 @@
/// My stats returned by stats.getMyStats
class MyStats {
final TotalStats total;
final TodayStats today;
final List<InviteSummary> recentInvites;
MyStats({
required this.total,
required this.today,
this.recentInvites = const [],
});
factory MyStats.fromJson(Map<String, dynamic> json) {
return MyStats(
total: TotalStats.fromJson(json['total'] ?? {}),
today: TodayStats.fromJson(json['today'] ?? {}),
recentInvites: (json['recentInvites'] as List?)
?.map((e) => InviteSummary.fromJson(e))
.toList() ??
[],
);
}
}
class TotalStats {
final int totalInvites;
final int totalJoins;
final int totalConversions;
final double totalRevenue;
TotalStats({
this.totalInvites = 0,
this.totalJoins = 0,
this.totalConversions = 0,
this.totalRevenue = 0,
});
factory TotalStats.fromJson(Map<String, dynamic> json) {
return TotalStats(
totalInvites: json['totalInvites'] ?? 0,
totalJoins: json['totalJoins'] ?? 0,
totalConversions: json['totalConversions'] ?? 0,
totalRevenue: (json['totalRevenue'] ?? 0).toDouble(),
);
}
}
class TodayStats {
final int invitesCreated;
final int joins;
final int conversions;
final double revenue;
TodayStats({
this.invitesCreated = 0,
this.joins = 0,
this.conversions = 0,
this.revenue = 0,
});
factory TodayStats.fromJson(Map<String, dynamic> json) {
return TodayStats(
invitesCreated: json['invitesCreated'] ?? 0,
joins: json['joins'] ?? 0,
conversions: json['conversions'] ?? 0,
revenue: (json['revenue'] ?? 0).toDouble(),
);
}
}
class InviteSummary {
final String? id;
final String? code;
final String? groupId;
final String? link;
final int? clickCount;
final int? joinCount;
final DateTime? createdAt;
InviteSummary({
this.id,
this.code,
this.groupId,
this.link,
this.clickCount,
this.joinCount,
this.createdAt,
});
factory InviteSummary.fromJson(Map<String, dynamic> json) {
return InviteSummary(
id: (json['_id'] ?? json['id'])?.toString(),
code: json['code'],
groupId: json['groupId']?.toString(),
link: json['link'],
clickCount: json['clickCount'],
joinCount: json['joinCount'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
);
}
}
/// Ranking entry returned by stats.getRanking
class RankingEntry {
final int rank;
final String salesId;
final int totalJoins;
final int totalConversions;
final double totalRevenue;
final Map<String, dynamic>? user;
RankingEntry({
this.rank = 0,
required this.salesId,
this.totalJoins = 0,
this.totalConversions = 0,
this.totalRevenue = 0,
this.user,
});
factory RankingEntry.fromJson(Map<String, dynamic> json) {
return RankingEntry(
rank: json['rank'] ?? 0,
salesId: (json['_id'] ?? json['salesId'] ?? '').toString(),
totalJoins: json['totalJoins'] ?? 0,
totalConversions: json['totalConversions'] ?? 0,
totalRevenue: (json['totalRevenue'] ?? 0).toDouble(),
user: json['user'],
);
}
String get displayName =>
user?['nickname']?.toString() ?? user?['email']?.toString() ?? salesId;
}
/// Platform stats returned by stats.getPlatformStats
class PlatformStats {
final Map<String, dynamic> platform;
final Map<String, dynamic> today;
PlatformStats({
this.platform = const {},
this.today = const {},
});
factory PlatformStats.fromJson(Map<String, dynamic> json) {
return PlatformStats(
platform: json['platform'] ?? {},
today: json['today'] ?? {},
);
}
}

View File

@@ -0,0 +1,118 @@
class User {
final String id;
final String? email;
final String? username;
final String? nickname;
final String? avatar;
final String? discriminator;
final bool? temporary;
final String? type;
final bool? emailVerified;
final bool? banned;
final Map<String, dynamic>? extra;
final DateTime? createdAt;
User({
required this.id,
this.email,
this.username,
this.nickname,
this.avatar,
this.discriminator,
this.temporary,
this.type,
this.emailVerified,
this.banned,
this.extra,
this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: (json['_id'] ?? json['id'] ?? '').toString(),
email: json['email'],
username: json['username'],
nickname: json['nickname'],
avatar: json['avatar'],
discriminator: json['discriminator']?.toString(),
temporary: json['temporary'],
type: json['type'],
emailVerified: json['emailVerified'],
banned: json['banned'],
extra: json['extra'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
);
}
Map<String, dynamic> toJson() {
return {
'_id': id,
'email': email,
'username': username,
'nickname': nickname,
'avatar': avatar,
'discriminator': discriminator,
'temporary': temporary,
'type': type,
'emailVerified': emailVerified,
'banned': banned,
'extra': extra,
'createdAt': createdAt?.toIso8601String(),
};
}
/// Display name: nickname first, then username, then email prefix
String get displayName {
if (nickname != null && nickname!.isNotEmpty) return nickname!;
if (username != null && username!.isNotEmpty) return username!;
if (email != null && email!.isNotEmpty) return email!.split('@').first;
return 'User';
}
/// Check if user is a guest/temporary user
bool get isGuest => temporary == true;
/// Role checks - these may come from extra fields or custom logic
/// In Tailchat, roles are not built-in; they are managed by the plugin
bool get isAdmin {
final role = extra?['role'];
return role == 'admin' || role == 'super_admin';
}
bool get isSuperAdmin {
final role = extra?['role'];
return role == 'super_admin';
}
bool get isSales {
final role = extra?['role'];
return role == 'sales' || role == null;
}
}
/// Tailchat login/register response wraps the user info with a token.
/// The backend returns the user object directly with a `token` field appended.
/// The gateway wraps all responses as `{ code, data }`.
class AuthResponse {
final User user;
final String token;
AuthResponse({required this.user, required this.token});
/// Parse from the gateway response.
/// Gateway returns: { "code": 200, "data": { ...userFields..., "token": "jwt..." } }
factory AuthResponse.fromJson(Map<String, dynamic> json) {
// The gateway wraps response in { code, data }
final data = json['data'] ?? json;
final token = data['token'] ?? '';
final user = User.fromJson(data);
return AuthResponse(
user: user,
token: token,
);
}
}