Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user