优化
This commit is contained in:
164
client/flutter/lib/providers/auth_provider.dart
Normal file
164
client/flutter/lib/providers/auth_provider.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sales_chat/models/user.dart';
|
||||
import 'package:sales_chat/services/api_service.dart';
|
||||
|
||||
class AuthProvider with ChangeNotifier {
|
||||
final ApiService _apiService;
|
||||
|
||||
User? _user;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
AuthProvider(this._apiService);
|
||||
|
||||
User? get user => _user;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get isLoggedIn => _user != null;
|
||||
Future<bool> get isAuthenticated => _apiService.isAuthenticated;
|
||||
|
||||
/// 使用邮箱/用户名和密码登录。
|
||||
/// 至少提供其中一个 [email] 或 [username]。
|
||||
Future<bool> login({
|
||||
String? email,
|
||||
String? username,
|
||||
required String password,
|
||||
}) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await _apiService.login(
|
||||
email: email,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
_user = response.user;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = _parseError(e);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 以访客身份登录(临时用户)。
|
||||
Future<bool> loginAsGuest({required String nickname}) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await _apiService.createTemporaryUser(nickname: nickname);
|
||||
_user = response.user;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = _parseError(e);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册新用户。
|
||||
Future<bool> register({
|
||||
String? username,
|
||||
String? email,
|
||||
String? nickname,
|
||||
required String password,
|
||||
}) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await _apiService.register(
|
||||
username: username,
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
password: password,
|
||||
);
|
||||
_user = response.user;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = _parseRegisterError(e);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _apiService.logout();
|
||||
} finally {
|
||||
_user = null;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadUser() async {
|
||||
if (!await _apiService.isAuthenticated) return;
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_user = await _apiService.getCurrentUser();
|
||||
} catch (e) {
|
||||
_user = null;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _parseError(dynamic e) {
|
||||
final errStr = e.toString();
|
||||
if (errStr.contains('401')) {
|
||||
return 'Email/username or password incorrect';
|
||||
} else if (errStr.contains('422') || errStr.contains('password')) {
|
||||
return 'Email/username or password incorrect';
|
||||
} else if (errStr.contains('timeout')) {
|
||||
return 'Network timeout, please retry';
|
||||
} else if (errStr.contains('SocketException')) {
|
||||
return 'Network connection failed';
|
||||
} else if (errStr.contains('banned')) {
|
||||
return 'Account has been banned';
|
||||
}
|
||||
return 'Login failed, please retry';
|
||||
}
|
||||
|
||||
String _parseRegisterError(dynamic e) {
|
||||
final errorStr = e.toString();
|
||||
if (errorStr.contains('exist')) {
|
||||
return 'Username or email already exists';
|
||||
} else if (errorStr.contains('empty')) {
|
||||
return 'Username or email is required';
|
||||
} else if (errorStr.contains('timeout')) {
|
||||
return 'Network timeout, please retry';
|
||||
} else if (errorStr.contains('SocketException')) {
|
||||
return 'Network connection failed';
|
||||
} else if (errorStr.contains('not allowed')) {
|
||||
return 'Server does not allow new user registration';
|
||||
}
|
||||
return 'Registration failed, please retry';
|
||||
}
|
||||
}
|
||||
197
client/flutter/lib/providers/chat_provider.dart
Normal file
197
client/flutter/lib/providers/chat_provider.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sales_chat/models/group.dart';
|
||||
import 'package:sales_chat/models/message.dart';
|
||||
import 'package:sales_chat/services/api_service.dart';
|
||||
|
||||
class ChatProvider with ChangeNotifier {
|
||||
final ApiService _apiService;
|
||||
|
||||
List<Group> _groups = [];
|
||||
Map<String, List<Message>> _messagesByConverse = {};
|
||||
Group? _currentGroup;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingMessages = false;
|
||||
String? _error;
|
||||
|
||||
ChatProvider(this._apiService);
|
||||
|
||||
List<Group> get groups => _groups;
|
||||
Group? get currentGroup => _currentGroup;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMessages => _isLoadingMessages;
|
||||
String? get error => _error;
|
||||
|
||||
List<Message> get currentMessages {
|
||||
if (_currentGroup == null) return [];
|
||||
// 使用第一个文本面板的 ID 作为 converseId
|
||||
final converseId = _currentGroup!.firstTextPanel?.id ?? _currentGroup!.id;
|
||||
return _messagesByConverse[converseId] ?? [];
|
||||
}
|
||||
|
||||
/// 获取群组的 converseId(第一个文本面板,或以群组 ID 作为备选)
|
||||
String _getConverseIdForGroup(Group group) {
|
||||
return group.firstTextPanel?.id ?? group.id;
|
||||
}
|
||||
|
||||
Future<void> loadGroups() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_groups = await _apiService.getGroups();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to load groups';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void selectGroup(Group group) {
|
||||
_currentGroup = group;
|
||||
notifyListeners();
|
||||
|
||||
final converseId = _getConverseIdForGroup(group);
|
||||
if (_messagesByConverse[converseId] == null) {
|
||||
loadMessages(converseId);
|
||||
}
|
||||
}
|
||||
|
||||
void deselectGroup() {
|
||||
_currentGroup = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadMessages(String converseId, {bool refresh = false}) async {
|
||||
if (_isLoadingMessages) return;
|
||||
|
||||
_isLoadingMessages = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final messages = await _apiService.getMessages(
|
||||
converseId,
|
||||
limit: 50,
|
||||
before: refresh ? null : (_messagesByConverse[converseId]?.first.id),
|
||||
);
|
||||
|
||||
if (refresh) {
|
||||
_messagesByConverse[converseId] = messages;
|
||||
} else {
|
||||
_messagesByConverse[converseId] = [
|
||||
...messages,
|
||||
...(_messagesByConverse[converseId] ?? [])
|
||||
];
|
||||
}
|
||||
|
||||
_isLoadingMessages = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to load messages';
|
||||
_isLoadingMessages = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String content) async {
|
||||
if (_currentGroup == null || content.isEmpty) return;
|
||||
|
||||
final converseId = _getConverseIdForGroup(_currentGroup!);
|
||||
final groupId = _currentGroup!.id;
|
||||
|
||||
try {
|
||||
final message = await _apiService.sendMessage(
|
||||
converseId,
|
||||
content,
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
_messagesByConverse[converseId] = [
|
||||
...(_messagesByConverse[converseId] ?? []),
|
||||
message,
|
||||
];
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to send message';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 撤回消息
|
||||
Future<bool> recallMessage(String messageId) async {
|
||||
try {
|
||||
await _apiService.recallMessage(messageId);
|
||||
|
||||
// 更新本地消息列表
|
||||
if (_currentGroup != null) {
|
||||
final converseId = _getConverseIdForGroup(_currentGroup!);
|
||||
final messages = _messagesByConverse[converseId];
|
||||
if (messages != null) {
|
||||
final idx = messages.indexWhere((m) => m.id == messageId);
|
||||
if (idx != -1) {
|
||||
// 标记为已撤回而非删除
|
||||
final old = messages[idx];
|
||||
messages[idx] = Message(
|
||||
id: old.id,
|
||||
content: 'Message recalled',
|
||||
author: old.author,
|
||||
groupId: old.groupId,
|
||||
converseId: old.converseId,
|
||||
hasRecall: true,
|
||||
meta: old.meta,
|
||||
createdAt: old.createdAt,
|
||||
updatedAt: old.updatedAt,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = 'Failed to recall message: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除消息(管理员)
|
||||
Future<bool> deleteMessage(String messageId) async {
|
||||
try {
|
||||
await _apiService.deleteMessage(messageId);
|
||||
|
||||
// 从本地消息列表中移除
|
||||
if (_currentGroup != null) {
|
||||
final converseId = _getConverseIdForGroup(_currentGroup!);
|
||||
final messages = _messagesByConverse[converseId];
|
||||
if (messages != null) {
|
||||
messages.removeWhere((m) => m.id == messageId);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete message: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加通过 socket.io 接收的消息
|
||||
void addMessage(Message message) {
|
||||
final converseId = message.converseId;
|
||||
_messagesByConverse[converseId] = [
|
||||
...(_messagesByConverse[converseId] ?? []),
|
||||
message,
|
||||
];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
135
client/flutter/lib/providers/invite_provider.dart
Normal file
135
client/flutter/lib/providers/invite_provider.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sales_chat/models/group.dart';
|
||||
import 'package:sales_chat/models/invite.dart';
|
||||
import 'package:sales_chat/services/api_service.dart';
|
||||
|
||||
class InviteProvider with ChangeNotifier {
|
||||
final ApiService _apiService;
|
||||
|
||||
List<Invite> _invites = [];
|
||||
Invite? _selectedInvite;
|
||||
InviteStats? _selectedInviteStats;
|
||||
List<Group> _availableGroups = [];
|
||||
bool _isLoading = false;
|
||||
bool _isCreating = false;
|
||||
String? _error;
|
||||
|
||||
InviteProvider(this._apiService);
|
||||
|
||||
List<Invite> get invites => _invites;
|
||||
Invite? get selectedInvite => _selectedInvite;
|
||||
InviteStats? get selectedInviteStats => _selectedInviteStats;
|
||||
List<Group> get availableGroups => _availableGroups;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isCreating => _isCreating;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> loadInvites() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_invites = await _apiService.getMyInvites();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to load invite list';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadAvailableGroups(List<Group> groups) async {
|
||||
_availableGroups = groups;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<Invite?> createInvite({
|
||||
required String groupId,
|
||||
int? expiresInDays,
|
||||
}) async {
|
||||
_isCreating = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final invite = await _apiService.createInvite(
|
||||
groupId: groupId,
|
||||
expiresInDays: expiresInDays,
|
||||
);
|
||||
|
||||
_invites.insert(0, invite);
|
||||
_isCreating = false;
|
||||
notifyListeners();
|
||||
return invite;
|
||||
} catch (e) {
|
||||
_error = 'Failed to create invite';
|
||||
_isCreating = false;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectInvite(Invite invite) async {
|
||||
_selectedInvite = invite;
|
||||
notifyListeners();
|
||||
|
||||
await loadInviteStats(invite.code);
|
||||
}
|
||||
|
||||
Future<void> loadInviteStats(String code) async {
|
||||
try {
|
||||
_selectedInviteStats = await _apiService.getInviteStats(code);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deactivateInvite(String code) async {
|
||||
try {
|
||||
await _apiService.deactivateInvite(code);
|
||||
|
||||
final index = _invites.indexWhere((i) => i.code == code);
|
||||
if (index != -1) {
|
||||
final invite = _invites[index];
|
||||
_invites[index] = Invite(
|
||||
id: invite.id,
|
||||
code: invite.code,
|
||||
salesId: invite.salesId,
|
||||
groupId: invite.groupId,
|
||||
link: invite.link,
|
||||
qrCodeUrl: invite.qrCodeUrl,
|
||||
createdAt: invite.createdAt,
|
||||
expiresAt: invite.expiresAt,
|
||||
clickCount: invite.clickCount,
|
||||
scanCount: invite.scanCount,
|
||||
joinCount: invite.joinCount,
|
||||
status: 'inactive',
|
||||
);
|
||||
}
|
||||
|
||||
if (_selectedInvite?.code == code) {
|
||||
_selectedInvite = null;
|
||||
_selectedInviteStats = null;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to deactivate invite';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSelection() {
|
||||
_selectedInvite = null;
|
||||
_selectedInviteStats = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
4
client/flutter/lib/providers/providers.dart
Normal file
4
client/flutter/lib/providers/providers.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
export 'auth_provider.dart';
|
||||
export 'chat_provider.dart';
|
||||
export 'invite_provider.dart';
|
||||
export 'stats_provider.dart';
|
||||
56
client/flutter/lib/providers/stats_provider.dart
Normal file
56
client/flutter/lib/providers/stats_provider.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sales_chat/models/stats.dart';
|
||||
import 'package:sales_chat/services/api_service.dart';
|
||||
|
||||
class StatsProvider with ChangeNotifier {
|
||||
final ApiService _apiService;
|
||||
|
||||
MyStats? _myStats;
|
||||
List<RankingEntry> _ranking = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
StatsProvider(this._apiService);
|
||||
|
||||
MyStats? get myStats => _myStats;
|
||||
List<RankingEntry> get ranking => _ranking;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
|
||||
/// Load the current user's stats
|
||||
Future<void> loadMyStats() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_myStats = await _apiService.getMyStats();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to load stats';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the ranking list
|
||||
Future<void> loadRanking({String period = 'monthly'}) async {
|
||||
try {
|
||||
_ranking = await _apiService.getRanking(period: period);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
// Handle silently
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await loadMyStats();
|
||||
await loadRanking();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user