76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
4.7 KiB
Dart
151 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../app_config.dart';
|
|
import '../im/im_bridge.dart';
|
|
import '../im/tcp_client.dart';
|
|
|
|
class MenuNode {
|
|
MenuNode({required this.name, required this.code, this.path, this.icon, this.children = const []});
|
|
final String name;
|
|
final String code;
|
|
final String? path;
|
|
final String? icon;
|
|
final List<MenuNode> children;
|
|
|
|
factory MenuNode.fromJson(Map<String, dynamic> j) {
|
|
final kids = (j['children'] as List? ?? []).whereType<Map>().map((e) => MenuNode.fromJson(Map<String, dynamic>.from(e))).toList();
|
|
return MenuNode(
|
|
name: '${j['name'] ?? ''}',
|
|
code: '${j['code'] ?? ''}',
|
|
path: j['path']?.toString(),
|
|
icon: j['icon']?.toString(),
|
|
children: kids,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SessionStore extends ChangeNotifier {
|
|
String apiBase = AppConfig.defaultApiBase;
|
|
String imHost = AppConfig.defaultImHost;
|
|
int imPort = AppConfig.defaultImPort;
|
|
String accessToken = '';
|
|
String refreshToken = '';
|
|
Map<String, dynamic> user = {};
|
|
List<MenuNode> menus = [];
|
|
bool loaded = false;
|
|
final ImTcpClient im = ImTcpClient();
|
|
final ImBridge bridge = ImBridge();
|
|
|
|
String get userId => '${user['id'] ?? ''}';
|
|
String get displayName => '${user['displayName'] ?? user['username'] ?? ''}';
|
|
List<String> get roles => (user['roles'] as List? ?? []).map((e) => '$e').toList();
|
|
List<String> get permissions => (user['permissions'] as List? ?? []).map((e) => '$e').toList();
|
|
bool get signedIn => accessToken.isNotEmpty && userId.isNotEmpty;
|
|
|
|
bool canSee(String code) {
|
|
if (roles.contains('admin') || roles.contains('owner')) return true;
|
|
return permissions.contains(code) || permissions.any((p) => p.startsWith('$code:'));
|
|
}
|
|
|
|
Future<void> load() async {
|
|
final p = await SharedPreferences.getInstance();
|
|
apiBase = p.getString('apiBase') ?? AppConfig.defaultApiBase;
|
|
imHost = p.getString('imHost') ?? AppConfig.defaultImHost;
|
|
imPort = p.getInt('imPort') ?? AppConfig.defaultImPort;
|
|
accessToken = p.getString('accessToken') ?? '';
|
|
refreshToken = p.getString('refreshToken') ?? '';
|
|
final u = p.getString('user');
|
|
if (u != null && u.isNotEmpty) {
|
|
try {
|
|
user = Map<String, dynamic>.from(jsonDecode(u) as Map);
|
|
} catch (_) {
|
|
user = {};
|
|
}
|
|
}
|
|
loaded = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> saveEndpoints({required String api, required String host, required int port}) async {
|
|
apiBase = api.trim().isEmpty ? AppConfig.defaultApiBase : api.trim();
|
|
imHost = host.trim().isEmpty ? AppConfig.defaultImHost : host.trim();
|
|
imPort = port <= 0 ? AppConfig.defaultImPort : port;
|
|
final p = await SharedPreferences.getInstance();
|
|
await p.setString('apiBase', apiBase);
|
|
await p.setString('imHost', imHost);
|
|
await p.setInt('imPort', imPort);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> applyLogin(Map<String, dynamic> data) async {
|
|
accessToken = '${data['accessToken'] ?? ''}';
|
|
refreshToken = '${data['refreshToken'] ?? ''}';
|
|
user = Map<String, dynamic>.from(data['user'] as Map? ?? {});
|
|
final p = await SharedPreferences.getInstance();
|
|
await p.setString('accessToken', accessToken);
|
|
await p.setString('refreshToken', refreshToken);
|
|
await p.setString('user', jsonEncode(user));
|
|
notifyListeners();
|
|
await connectIm();
|
|
}
|
|
|
|
Future<void> setTokens(String access, String refresh) async {
|
|
final changed = access != accessToken;
|
|
accessToken = access;
|
|
refreshToken = refresh;
|
|
final p = await SharedPreferences.getInstance();
|
|
await p.setString('accessToken', access);
|
|
await p.setString('refreshToken', refresh);
|
|
notifyListeners();
|
|
if (changed && signedIn) await connectIm(force: true);
|
|
}
|
|
|
|
Future<void> patchUser(Map<String, dynamic> next) async {
|
|
user = next;
|
|
final p = await SharedPreferences.getInstance();
|
|
await p.setString('user', jsonEncode(user));
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setMenus(List<MenuNode> items) async {
|
|
menus = items;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> connectIm({bool force = false}) async {
|
|
if (!signedIn) return;
|
|
if (force) im.disconnect();
|
|
await bridge.init();
|
|
await im.connect(
|
|
host: imHost,
|
|
port: imPort,
|
|
userId: userId,
|
|
token: accessToken,
|
|
);
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
im.disconnect();
|
|
await bridge.release();
|
|
accessToken = '';
|
|
refreshToken = '';
|
|
user = {};
|
|
menus = [];
|
|
final p = await SharedPreferences.getInstance();
|
|
await p.remove('accessToken');
|
|
await p.remove('refreshToken');
|
|
await p.remove('user');
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
im.disconnect();
|
|
im.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|