router 冲突解决
This commit is contained in:
@@ -89,6 +89,54 @@ class UserApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// 注销账号 → POST /api/v1/User/CloseAccount
|
||||
///
|
||||
/// 对齐 Vue api/login: `closeAccount = () => request.post('/api/v1/User/CloseAccount')`
|
||||
/// 调用方负责弹确认框 + 注销后清理本地状态。
|
||||
static Future<void> closeAccount() async {
|
||||
await DioClient.instance.post('/api/v1/User/CloseAccount');
|
||||
}
|
||||
|
||||
// ==================== 注册 ====================
|
||||
|
||||
/// 注册 → POST /api/v1/User/Register
|
||||
///
|
||||
/// 对齐 Vue api/login `register`: { account, password },密码同样 MD5 加密
|
||||
/// Vue RegisterResponse = { code, message }(业务响应壳),Flutter 端 DioClient
|
||||
/// 已自动处理成功/失败分支,成功不关心 body,失败直接抛 ApiException。
|
||||
static Future<void> register({
|
||||
required String account,
|
||||
required String password,
|
||||
}) async {
|
||||
await DioClient.instance.post(
|
||||
'/api/v1/User/Register',
|
||||
data: {'account': account.trim(), 'password': _md5(password.trim())},
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 审核模式 ====================
|
||||
|
||||
/// 获取审核模式(决定是否展示"去注册"入口)→ GET /api/v1/Public/GetIsAuditMode
|
||||
///
|
||||
/// 对齐 Vue api/login: `getAuditMode()`,返回值本身就是 boolean
|
||||
/// (request 拦截器已解包),接口不可用时安全回退 false(不展示注册入口)。
|
||||
static Future<bool> getAuditMode() async {
|
||||
try {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/Public/GetIsAuditMode',
|
||||
);
|
||||
if (data is bool) return data;
|
||||
if (data is Map) {
|
||||
// 兼容 { isAuditMode: true } 包装
|
||||
final v = data['isAuditMode'];
|
||||
if (v is bool) return v;
|
||||
}
|
||||
return false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// MD5 加密(UniApp stringToMd5 的等价实现)
|
||||
static String _md5(String text) {
|
||||
return md5.convert(utf8.encode(text)).toString();
|
||||
|
||||
@@ -73,7 +73,7 @@ class CommonPagedList<T> extends StatefulWidget {
|
||||
this.separatorBuilder,
|
||||
this.padding,
|
||||
this.emptyWidget,
|
||||
this.noMoreText = '— 没有更多了 —',
|
||||
this.noMoreText = '- AI 爱学蝶变提醒你 没有更多了 -',
|
||||
this.invisibleItemsThreshold = 0,
|
||||
this.enableRefresh = true,
|
||||
});
|
||||
|
||||
@@ -22,8 +22,10 @@ class AppEnv {
|
||||
|
||||
/// 品牌标识:aixue / chucai / yule / ...(默认 aixue=爱学蝶变)
|
||||
/// 品牌表见 app_brand.dart;主题/接口/App 名都随它走(AppConfig 聚合)
|
||||
static const String brand =
|
||||
String.fromEnvironment('BRAND', defaultValue: 'aixue');
|
||||
static const String brand = String.fromEnvironment(
|
||||
'BRAND',
|
||||
defaultValue: 'aixue',
|
||||
);
|
||||
|
||||
/// 三套环境地址表(与 uni-app envList 完全对齐)
|
||||
static const Map<String, Map<String, String>> _envTable = {
|
||||
@@ -58,6 +60,9 @@ class AppEnv {
|
||||
/// 当前环境学习报告地址(预留,对标 envList.reportUrl)
|
||||
static String get reportUrl => value('reportUrl');
|
||||
|
||||
/// 静态资源 CDN 域名(对标 uni-app utils/image.js 的 IMG_URL)
|
||||
static const String imgBaseUrl = 'https://hx.cnzhyx.cn';
|
||||
|
||||
/// 是否正式环境(组合规则里判断用:prod 才走品牌专属地址)
|
||||
static bool get isProd => env == 'prod';
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
/// ============================================================
|
||||
class DioClient {
|
||||
/// 网络日志总开关:true = 控制台打印请求/响应数据,false = 静默
|
||||
static const bool enableNetworkLog = false;
|
||||
static const bool enableNetworkLog = true;
|
||||
|
||||
// ---------- 1. 单例:整个 App 只有一个 Dio ----------
|
||||
DioClient._()
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:new_learn/features/auth/legal_document_page.dart';
|
||||
import 'package:new_learn/features/auth/legal_documents.dart';
|
||||
import 'package:new_learn/features/auth/login_page.dart';
|
||||
import 'package:new_learn/features/auth/privacy_consent_page.dart';
|
||||
import 'package:new_learn/features/auth/register_page.dart';
|
||||
import 'package:new_learn/features/class/all_student_view.dart';
|
||||
import 'package:new_learn/features/class/class_page.dart';
|
||||
import 'package:new_learn/features/class/my_interact_page.dart';
|
||||
@@ -14,6 +15,10 @@ import 'package:new_learn/core/api/class/model.dart';
|
||||
import 'package:new_learn/features/my/my_page.dart';
|
||||
import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||||
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
||||
import 'package:new_learn/features/my/my_report/my_report_page.dart';
|
||||
import 'package:new_learn/features/my/my_report/report_detail_page.dart';
|
||||
import 'package:new_learn/features/my/settings/settings_page.dart';
|
||||
import 'package:new_learn/features/my/settings/about_app_page.dart';
|
||||
import 'package:new_learn/features/recite/recite_page.dart';
|
||||
import 'package:new_learn/features/task/task_page.dart';
|
||||
import 'package:new_learn/core/router/main_tab_page.dart';
|
||||
@@ -96,34 +101,47 @@ final GoRouter appRouter = GoRouter(
|
||||
GoRoute(path: '/recite', builder: (_, _) => const RecitePage()),
|
||||
],
|
||||
),
|
||||
// 班级(含班级模块子页:全部同学 / 我的互动 / 动态详情)
|
||||
// 班级(只有主 tab 页在 ShellBranch 内,子页放顶层避免 tabbar 遮挡)
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/class', builder: (_, _) => const ClassPage()),
|
||||
GoRoute(
|
||||
path: '/class/all-student',
|
||||
builder: (_, _) => const AllStudentPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/class/my-interact',
|
||||
builder: (_, _) => const MyInteractPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/class/my-update-detail',
|
||||
builder: (_, state) =>
|
||||
MyUpdateDetailPage(initialPost: state.extra as UserPostDto),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 我的(含我的积分页)
|
||||
// 我的
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/my', builder: (_, _) => const MyPage()),
|
||||
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
|
||||
],
|
||||
routes: [GoRoute(path: '/my', builder: (_, _) => const MyPage())],
|
||||
),
|
||||
],
|
||||
),
|
||||
// ---------- 二级业务页(不在 ShellBranch 内,push 时不显示 tabbar) ----------
|
||||
GoRoute(
|
||||
path: '/class/all-student',
|
||||
builder: (_, _) => const AllStudentPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/class/my-interact',
|
||||
builder: (_, _) => const MyInteractPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/class/my-update-detail',
|
||||
builder: (_, state) =>
|
||||
MyUpdateDetailPage(initialPost: state.extra as UserPostDto),
|
||||
),
|
||||
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
|
||||
GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()),
|
||||
GoRoute(
|
||||
path: '/report-detail',
|
||||
builder: (_, state) {
|
||||
// extra 格式:{'title': '...', 'url': '...'}
|
||||
final extra = state.extra as Map<String, dynamic>? ?? {};
|
||||
return ReportDetailPage(
|
||||
title: extra['title'] as String? ?? '我的报告',
|
||||
url: extra['url'] as String? ?? '',
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
|
||||
GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()),
|
||||
// ---------- 首次启动隐私协议页 ----------
|
||||
GoRoute(
|
||||
path: '/privacy-consent',
|
||||
@@ -143,7 +161,7 @@ final GoRouter appRouter = GoRouter(
|
||||
content: kPrivacyPolicyContent,
|
||||
),
|
||||
),
|
||||
// ---------- 登录页 ----------
|
||||
// ---------- 登录/注册页(未登录可访问) ----------
|
||||
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
|
||||
// ---------- 攻克包 - 截图名单 ----------
|
||||
GoRoute(
|
||||
@@ -151,6 +169,8 @@ final GoRouter appRouter = GoRouter(
|
||||
builder: (_, state) =>
|
||||
CaptureListPage(courseId: state.extra as String),
|
||||
),
|
||||
GoRoute(path: '/register', builder: (_, _) => const RegisterPage()),
|
||||
|
||||
// app_router.dart 的 routes 里加,不用管跳转逻辑
|
||||
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
|
||||
// ---------- 以后新业务页在这里加,一行一个 ----------
|
||||
@@ -158,7 +178,7 @@ final GoRouter appRouter = GoRouter(
|
||||
);
|
||||
|
||||
/// 登录守卫(对标 vue-router beforeEach):
|
||||
/// 没登录 → 强制去 /login;已登录还访问 /login → 踢回首页
|
||||
/// 没登录 → 强制去 /login;已登录还访问 /login 或 /register → 踢回首页
|
||||
String? _guardLogin(BuildContext context, GoRouterState state) {
|
||||
final acceptedPrivacy = privacyAcceptedNotifier.value;
|
||||
final loggedIn = DioClient.instance.accessToken != null;
|
||||
@@ -167,11 +187,14 @@ String? _guardLogin(BuildContext context, GoRouterState state) {
|
||||
state.matchedLocation == '/user-agreement' ||
|
||||
state.matchedLocation == '/privacy-policy';
|
||||
final atLogin = state.matchedLocation == '/login';
|
||||
final atRegister = state.matchedLocation == '/register';
|
||||
// 未登录可白名单:隐私协议页 / 协议文档 / 登录页 / 注册页
|
||||
final atAuthFree = atLogin || atRegister || atLegalDocument;
|
||||
if (!acceptedPrivacy && !atPrivacyConsent && !atLegalDocument) {
|
||||
return '/privacy-consent';
|
||||
}
|
||||
if (acceptedPrivacy && atPrivacyConsent) return loggedIn ? '/' : '/login';
|
||||
if (!loggedIn && !atLogin && !atLegalDocument) return '/login';
|
||||
if (loggedIn && atLogin) return '/';
|
||||
if (!loggedIn && !atAuthFree && !atPrivacyConsent) return '/login';
|
||||
if (loggedIn && (atLogin || atRegister)) return '/';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,44 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:new_learn/core/models/user_model.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【LoginHistoryItem】登录历史单条记录 —— 对齐 Vue loginHistory
|
||||
///
|
||||
/// account → 登录账号(唯一标识,重复登录时去重用)
|
||||
/// userName → 登录后拿到的昵称(展示用,可能为空)
|
||||
/// password → 记住的密码(明文,存在 secure_storage 里一条 JSON)
|
||||
/// 空串 "" 表示本次没勾选记住密码
|
||||
/// rememberPassword → 是否勾选了记住密码(决定密码是否回填)
|
||||
/// ============================================================
|
||||
class LoginHistoryItem {
|
||||
const LoginHistoryItem({
|
||||
required this.account,
|
||||
required this.userName,
|
||||
required this.password,
|
||||
required this.rememberPassword,
|
||||
});
|
||||
|
||||
final String account;
|
||||
final String userName;
|
||||
final String password;
|
||||
final bool rememberPassword;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'account': account,
|
||||
'userName': userName,
|
||||
'password': password,
|
||||
'rememberPassword': rememberPassword,
|
||||
};
|
||||
|
||||
factory LoginHistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||
LoginHistoryItem(
|
||||
account: json['account'] as String? ?? '',
|
||||
userName: json['userName'] as String? ?? '',
|
||||
password: json['password'] as String? ?? '',
|
||||
rememberPassword: json['rememberPassword'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// ============================================================
|
||||
/// 【UserStorage】用户本地存储(token 安全存储 + 用户信息缓存)
|
||||
///
|
||||
@@ -118,6 +156,36 @@ class UserStorage {
|
||||
Future<void> clearRememberedPassword() =>
|
||||
_secure.delete(key: _rememberedPasswordKey);
|
||||
|
||||
// ---------- 登录历史(多条,最多 3 条,对齐 Vue loginHistory) ----------
|
||||
|
||||
static const String _loginHistoryKey = 'login_history';
|
||||
static const int _maxHistoryCount = 3;
|
||||
|
||||
/// 读取登录历史(没存过返回空列表)
|
||||
Future<List<LoginHistoryItem>> readLoginHistory() async {
|
||||
final raw = await _secure.read(key: _loginHistoryKey);
|
||||
if (raw == null) return [];
|
||||
try {
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
return list
|
||||
.map((e) => LoginHistoryItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return []; // 缓存损坏时当没历史处理
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存登录历史(整体替换存,最多保留 3 条)
|
||||
Future<void> saveLoginHistory(List<LoginHistoryItem> items) async {
|
||||
final trimmed = items.length > _maxHistoryCount
|
||||
? items.sublist(0, _maxHistoryCount)
|
||||
: items;
|
||||
await _secure.write(
|
||||
key: _loginHistoryKey,
|
||||
value: jsonEncode(trimmed.map((e) => e.toJson()).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
/// 一键退出登录:清 Token + 用户信息
|
||||
/// (记住的账号密码保留,方便用户下次登录)
|
||||
Future<void> clearAll() async {
|
||||
|
||||
Reference in New Issue
Block a user