router 冲突解决

This commit is contained in:
chenshuainan
2026-09-09 16:24:44 +08:00
19 changed files with 1658 additions and 802 deletions
+48
View File
@@ -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();
+1 -1
View File
@@ -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,
});
+7 -2
View File
@@ -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';
}
+1 -1
View File
@@ -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._()
+46 -23
View File
@@ -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;
}
+68
View File
@@ -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 {