🔍 MR: feat: 升级 nextjs 版本

代码变更影响范围深度分析与回归测试指南 · 由 AI 极速推演生成

项目仓库:PwaSlotCash
分支流向h5-70055-dev-upnextqidh5-70055-test
分析触发人:罗哲华

🕒 本 MR 分支陆续提交迭代历史 (点击可切换回溯分析)

共 2 个版本

🧠 回归测试专项拓扑脑图

graph TD Root[🔍 回归测试拓扑脑图] --> Func[🟢 功能验证] Root --> API[🔵 接口兼容] Root --> Edge[🟣 异常极值] Func --> F1[路由与Hydration] Func --> F2[构建产物与分包] Func --> F3[状态与滚动恢复] API --> A1[依赖版本对齐] API --> A2[Sentry上报链路] API --> A3[第三方SDK兼容] Edge --> E1[Console过滤验证] Edge --> E2[Chunk拆分极值] Edge --> E3[内存与并发压力] style Root fill:#ff9900,stroke:#333,stroke-width:2px,color:#fff style Func fill:#28a745,stroke:#333,stroke-width:1px,color:#fff style API fill:#007bff,stroke:#333,stroke-width:1px,color:#fff style Edge fill:#6f42c1,stroke:#333,stroke-width:1px,color:#fff
🔍 点击全屏放大查看

🔗 代码变更传导依赖链路图

graph LR file_0 ["authHelpers.tsx"]--> file_1 ["page.tsx"] file_0 ["authHelpers.tsx"]--> file_2 ["page.tsx"] file_3 ["GuidePage.tsx"]--> file_4 ["index.ts"] file_4 ["index.ts"]--> file_2 ["page.tsx"] file_0 ["authHelpers.tsx"]--> file_5 ["page.tsx"] file_6 ["Banner.tsx"]--> file_7 ["index.ts"] file_7 ["index.ts"]--> file_8 ["page.tsx"] file_9 ["footer.tsx"]--> file_7 ["index.ts"] file_10 ["useRewardHandlers.ts"]--> file_11 ["page.tsx"] style file_0 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_1 fill: #ccffcc, stroke:#28a745, stroke-width: 2px style file_2 fill: #ccffcc, stroke:#28a745, stroke-width: 2px style file_3 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_4 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_5 fill: #ccffcc, stroke:#28a745, stroke-width: 2px style file_6 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_7 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_8 fill: #ccffcc, stroke:#28a745, stroke-width: 2px style file_9 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_10 fill: #e7f3ff, stroke:#007bff, stroke-width: 1px style file_11 fill: #ccffcc, stroke:#28a745, stroke-width: 2px
🔍 点击全屏放大查看

📋 核心变更内容与测试边界提醒

💡 关键变动逻辑:
🚨 【高危必测区】
🛡️ 【安全豁免区】
📈 【极值边界点】

📊 风险评估级别

🔴
high Risk
当前合并请求的整体发布风险评级

⚠️ 潜在隐患与风险点

  • 1
    [状态水合失败] React 19 严格模式与 SSR 渲染差异可能导致客户端水合失败,引发白屏或状态重置。
  • 2
    [构建策略坍塌] 移除自定义 splitChunks 后,默认分包逻辑可能改变,导致动态路由加载延迟或缓存失效。
  • 3
    [日志静默风险] Terser 默认 drop_console 开启且 Sentry 移除 disableLogger,可能掩盖生产环境调试线索或引发上报风暴。

🗺️ 业务波及页面与反向依赖调用链

受改动波及路由入口 反向依赖链路 trace 路径
src/app/(auth)/forgot/page.tsx src/app/(auth)/common/authHelpers.tsx ➔ src/app/(auth)/forgot/page.tsx
src/app/(auth)/login/page.tsx src/app/(auth)/common/authHelpers.tsx ➔ src/app/(auth)/login/page.tsxsrc/app/(auth)/login/page.tsxsrc/app/_components/GuidePage/GuidePage.tsx ➔ src/app/_components/index.ts ➔ src/app/(auth)/login/page.tsx
src/app/(auth)/signup/page.tsx src/app/(auth)/common/authHelpers.tsx ➔ src/app/(auth)/signup/page.tsxsrc/app/(auth)/signup/page.tsx
src/app/(game)/game/page.tsx src/app/(game)/game/page.tsx
src/app/(game2)/game2/page.tsx src/app/(game2)/game2/page.tsx
src/app/(pages)/account/member/page.tsx src/app/(pages)/account/member/page.tsx
src/app/(pages)/account/page.tsx src/app/(pages)/account/page.tsx
src/app/(pages)/account/personal-info/page.tsx src/app/(pages)/account/personal-info/page.tsx
src/app/(pages)/home/page.tsx src/app/(pages)/components/Banner/Banner.tsx ➔ src/app/(pages)/components/index.ts ➔ src/app/(pages)/home/page.tsxsrc/app/(pages)/components/footer.tsx ➔ src/app/(pages)/components/index.ts ➔ src/app/(pages)/home/page.tsx
src/app/(pages)/rewards/page.tsx src/app/(pages)/rewards/hooks/useRewardHandlers.ts ➔ src/app/(pages)/rewards/page.tsx

💡 灰盒测试数据 Mock 与状态断言指南

测试手段 关键对象/键 数据构造 / 预期断言标准
API Mock JSON /api/user/profile Mock 返回 {"id":1,"role":"vip"},断言 React 19 服务端组件直接渲染无 hydration mismatch,且 Redux 状态同步延迟 <50ms。
LocalStorage 状态 sessionStorage/scroll-restoration 模拟 sessionStorage.setItem('scroll-pos', '1500'),断言路由切换后 window.scrollTo 准确恢复,且无 React 19 警告。
Webpack Chunk 加载 static/chunks/app-*.js 拦截 Network 请求,断言单 Chunk 体积 <300KB,并发请求数 ≤8,失败时自动 fallback 重试。

📋 回归测试专项用例

🎯 测试维度:功能验证

受影响模块 用例标题 前置条件 测试步骤 预期结果
核心路由与Hydration React 19 SSR/CSR 水合一致性验证 启用 Next.js 16 App Router,服务端渲染首屏 1. 访问 /login 与 /game 路由
2. 禁用 JS 查看 SSR 骨架
3. 启用 JS 观察控制台 Hydration 警告
4. 快速切换路由触发状态更新
无 hydration mismatch 警告,DOM 结构一致,状态无缝衔接。

🎯 测试维度:接口兼容

受影响模块 用例标题 前置条件 测试步骤 预期结果
构建与依赖链 Next16/React19 依赖对齐与分包策略验证 执行 pnpm build --webpack 生成生产包 1. 检查 package.json 版本锁定
2. 分析 .next/static/chunks 目录结构
3. 验证 core-js 与 ie 兼容策略移除影响
4. 检查 Sentry 配置树摇结果
构建成功无报错,Chunk 按默认策略合理拆分,无遗留 polyfill 冗余。

🎯 测试维度:异常与极值

受影响模块 用例标题 前置条件 测试步骤 预期结果
生产环境日志与压缩 Terser 压缩与 Console 过滤极值测试 生产环境构建完成,Terser 默认配置生效 1. 注入 console.log('debug') 至业务组件
2. 执行生产构建并解压产物
3. 搜索产物中是否残留 log 语句
4. 触发 Sentry 错误上报
产物中 console 语句被彻底剥离,Sentry 仅上报 Error 级别,无调试日志污染。

📂 本次 MR 代码变更审阅 (GitHub 级体验)

可通过左侧文件列表快速定位跳转,右侧包含文件卡片化高亮 Diff 细节

eslint.config.js
+24 -2
@@ -15,7 +15,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
export default [
// 基础配置
{
- ignores: ['**/node_modules', '**/dist', '**/workbox-sw.js', '*.config.js'],
+ ignores: [
+ '**/node_modules',
+ '**/.next/**',
+ '**/.open-next/**',
+ '**/.wrangler/**',
+ '**/.agent/**',
+ '**/coverage/**',
+ '**/dist',
+ '**/out/**',
+ '**/workbox-sw.js',
+ '*.config.js',
+ ],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
@@ -46,6 +57,16 @@ export default [
},
},
+ // Node/工具脚本不参与 type-aware TS lint,避免要求它们必须被 tsconfig include
+ {
+ files: ['**/*.{js,cjs,mjs}'],
+ languageOptions: {
+ parserOptions: {
+ project: false,
+ },
+ },
+ },
+
// Import 排序规则
{
plugins: {
@@ -158,7 +179,7 @@ export default [
// 配置文件特殊规则
{
- files: ['*.config.js'],
+ files: ['*.config.{js,cjs}'],
languageOptions: {
globals: {
node: true, // 使用 Node.js 环境
@@ -166,6 +187,7 @@ export default [
},
rules: {
'@typescript-eslint/no-var-requires': 'off', // 允许 require 语法
+ '@typescript-eslint/no-require-imports': 'off', // CommonJS 配置文件允许 require
},
},
next.config.ts
+7 -68
@@ -50,11 +50,6 @@ const nextConfig: NextConfig = {
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }) => {
- // 添加 core-js polyfill
- config.resolve.alias = {
- ...config.resolve.alias,
- 'core-js': path.resolve(__dirname, 'node_modules', 'core-js')
- };
if (!isServer) {
config.stats = 'verbose'; // 启用详细的 Webpack 输出日志
// config.devtool = 'source-map'; // 例如 source-map
@@ -81,22 +76,6 @@ const nextConfig: NextConfig = {
// );
// }
//================================ end oss 图片资源 上传插件 ================================
-
- // 保留 console.log 用于生产环境调试
- // 修改默认的 Terser 配置而不是替换整个 minimizer
- if (config.optimization.minimizer) {
- config.optimization.minimizer.forEach((minimizer: any) => {
- if (minimizer.constructor.name === 'TerserPlugin') {
- minimizer.options.terserOptions = {
- ...minimizer.options.terserOptions,
- compress: {
- ...minimizer.options.terserOptions?.compress,
- drop_console: false // 保留 console.log
- }
- };
- }
- });
- }
}
// 复制并转换FCM配置文件
config.plugins.push(
@@ -191,47 +170,6 @@ const nextConfig: NextConfig = {
);
}
- // ======== Next.js 代码分割优化 ========
- // 仅在生产环境中启用代码分割优化,以提高性能并减少资源加载时间
- if (!dev && !isServer) {
- config.optimization.splitChunks = {
- chunks: 'all',
- minSize: 20000, // 20KB以上的chunk才会被拆分
- maxSize: 300000, // 300KB以上的chunk尝试拆分
- minChunks: 1, // 被1个以上chunk使用的模块才拆分
- maxAsyncRequests: 10, // 异步加载时的最大并行请求数
- maxInitialRequests: 8, // 初始加载时的最大并行请求数
- automaticNameDelimiter: '.', // 自动生成的文件名分隔符
- cacheGroups: {
- vendor: {
- name: 'vendor',
- test: /[\\/]node_modules[\\/]/,
- priority: 100,
- minSize: 200000, // 200KB以上的库单独打包
- reuseExistingChunk: true, // 复用已存在的 chunk
- minChunks: 2,
- enforce: true // 强制执行拆分
- },
- core: {
- name: 'core', // 分割的chunk名称
- test: /[\\/]src[\\/](Core|Fwk|Master)[\\/]/, // 修正路径大小写
- priority: 20,
- minSize: 50000,
- minChunks: 2,
- reuseExistingChunk: true,
- enforce: true
- },
- components: {
- name: 'components',
- test: /[\\/]src[\\/](app[\\/]_components|components)[\\/]/,
- priority: 15,
- minChunks: 2, // 被2个以上模块引用才分包
- reuseExistingChunk: true
- }
- }
- };
- }
-
return config;
},
@@ -239,11 +177,8 @@ const nextConfig: NextConfig = {
images: {
unoptimized: true // 禁用 Next.js 内置的图像优化,适用于静态导出
},
- trailingSlash: true, // 生成带斜杠的静态路径,例如 `/about/`
experimental: {
- scrollRestoration: true, // 启用浏览器的滚动恢复
- // optimizeCss: true, // CSS 优化 - 暂时禁用,需要critters依赖
- webpackBuildWorker: true // 使用 worker 进程进行 webpack 构建
+ scrollRestoration: true // 启用浏览器的滚动恢复
}
};
@@ -254,6 +189,10 @@ export default withSentryConfig(nextConfig, {
silent: true,
authToken: process.env.SENTRY_AUTH_TOKEN,
widenClientFileUpload: true,
- disableLogger: true,
- automaticVercelMonitors: true
+ webpack: {
+ treeshake: {
+ removeDebugLogging: true
+ },
+ automaticVercelMonitors: true
+ }
});
package.json
+19 -22
@@ -5,19 +5,18 @@
"version_core": 36,
"private": true,
"scripts": {
- "dev": "next dev --turbo",
- "dev2": "next dev",
- "build": "BUILD_ENV=production next build && node scripts/copySW.js",
- "build:dev": "export BUILD_ENV=dev_test && cp .env.development .env.local && next build && node scripts/copySW.js && rm .env.local",
+ "dev": "next dev",
+ "build": "BUILD_ENV=production next build --webpack && node scripts/copySW.js",
+ "build:dev": "export BUILD_ENV=dev_test && cp .env.development .env.local && next build --webpack && node scripts/copySW.js && rm .env.local",
"build:time": "node scripts/build.js",
- "build:analyze": "ANALYZE=true next build && node scripts/copySW.js",
- "build:cloudflare": "BUILD_ENV=production OUTPUT_MODE=cloudflare next build && npx @opennextjs/cloudflare build --skipBuild && node scripts/copySW-cloudflare.js",
- "build:native": "BUILD_ENV=production OUTPUT_MODE=standalone next build",
+ "build:analyze": "ANALYZE=true next build --webpack && node scripts/copySW.js",
+ "build:cloudflare": "BUILD_ENV=production OUTPUT_MODE=cloudflare next build --webpack && npx @opennextjs/cloudflare build --skipBuild && node scripts/copySW-cloudflare.js",
+ "build:native": "BUILD_ENV=production OUTPUT_MODE=standalone next build --webpack",
"start": "pnpm run build && npx serve@latest out",
"start:cloudflare": "npx wrangler dev",
"start:native": "node .next/standalone/server.js",
"upload:images": "oss-upload",
- "lint": "next lint",
+ "lint": "eslint . --ext .ts,.tsx,.js --max-warnings 0",
"lint2": "eslint . --ext .ts,.tsx,.js --max-warnings 0",
"format": "prettier --write .",
"perf-simple": "node scripts/simple-perf.js",
@@ -28,18 +27,17 @@
"format:changed": "git diff --name-only --cached -z | xargs -0 -r pnpm exec prettier --write --ignore-unknown"
},
"dependencies": {
- "@iap/kit-oss-image": "latest",
"@babel/parser": "^7.27.5",
"@babel/traverse": "^7.27.4",
"@eslint/js": "^9.27.0",
"@hookform/resolvers": "^5.2.2",
+ "@iap/kit-oss-image": "latest",
"@lottiefiles/dotlottie-react": "^0.14.2",
- "@next/third-parties": "^15.0.3",
+ "@next/third-parties": "^16.2.6",
"@react-oauth/google": "^0.12.2",
"@reduxjs/toolkit": "^2.8.2",
- "@sentry/nextjs": "9.24.0",
+ "@sentry/nextjs": "10.53.1",
"ahooks": "^3.9.0",
- "core-js": "^3.36.1",
"crypto-js": "^4.2.0",
"firebase": "^11.9.1",
"framer-motion": "^12.15.0",
@@ -49,11 +47,11 @@
"jsencrypt": "^3.3.2",
"lottie-react": "^2.4.1",
"motion": "^12.4.3",
- "next": "15.0.3",
+ "next": "16.2.6",
"pako": "^2.1.0",
"qrcode": "^1.5.4",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
"react-hook-form": "7.62.0",
"react-redux": "^9.2.0",
"react-slick": "^0.30.3",
@@ -67,8 +65,7 @@
"production": [
">0.2%",
"not dead",
- "not op_mini all",
- "ie >= 11"
+ "not op_mini all"
],
"development": [
"last 1 chrome version",
@@ -79,16 +76,16 @@
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@eslint/js": "^9.28.0",
- "@next/eslint-plugin-next": "15.0.3",
- "@opennextjs/cloudflare": "^1.14.7",
- "@sentry/types": "^9.24.0",
+ "@next/eslint-plugin-next": "16.2.6",
+ "@opennextjs/cloudflare": "^1.19.4",
+ "@sentry/types": "^10.53.1",
"@types/canvas-confetti": "^1.9.0",
"@types/crypto-js": "^4.2.2",
"@types/node": "^20",
"@types/pako": "^2.0.3",
"@types/qrcode": "^1.5.5",
- "@types/react": "^18",
- "@types/react-dom": "^18",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
"@types/react-slick": "^0.23.13",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^8.32.1",
postcss.config.cjs
+15 -7
@@ -1,3 +1,7 @@
+// 使用动态字符串拼装,彻底绕过 Turbopack 编译器的静态 AST 依赖分析(防止其强行解析并虚拟化为有毒的 /ROOT 绝对路径)
+// 在 Node 运行时(Webpack 阶段)此拼装能够被正常解析并加载成功
+const pluginPath = './scripts' + '/ossImage/postcss-kit-oss-image.cjs';
+
module.exports = {
plugins: [
// next 默认基础配置
@@ -12,12 +16,16 @@ module.exports = {
],
'autoprefixer',
- // CSS 中的 /img/... 在构建期按版本映射替换为 OSS / CDN 地址
- [
- './scripts/ossImage/postcss-kit-oss-image.cjs',
- {
- versionMapPath: './oss-image-version.json'
- }
- ]
+ // 仅在生产构建打包时启用 OSS 图片替换插件
+ ...(process.env.NODE_ENV === 'production'
+ ? [
+ [
+ pluginPath,
+ {
+ versionMapPath: './oss-image-version.json',
+ },
+ ],
+ ]
+ : []),
],
};
scripts/build.js
+1 -6
@@ -57,12 +57,7 @@ async function runBuild() {
try {
console.log('🛠️ 正在执行Next.js构建...');
- const buildOutput = await runCommand('next', ['build']);
-
- // 检查是否完成Exporting (3/3)
- if (!buildOutput.includes('Exporting (3/3)')) {
- throw new Error('构建未完成Exporting阶段');
- }
+ await runCommand('next', ['build', '--webpack']);
console.log('\n🔧 正在复制Service Worker文件...');
await runCommand('node', ['scripts/copySW.js']);
src/Core/RouterMgr.ts
+77 -52
@@ -17,10 +17,59 @@ export default class RouterMgr {
static tag = '[路由Mgr]';
static router: AppRouterInstance;
static dialog: DialogMomentMgr;
- // static lastRouterChangeTime: number = 0;
+
+ /**
+ * 将 qid 注入到 href 中(如果尚未包含)
+ * 用于 Proxy 拦截所有 router.push,自动携带当前渠道 qid
+ */
+ private static injectQid(href: string): string {
+ const qid = ChannelMgr.channelQid;
+ if (!qid) return href;
+ try {
+ // URL 构造函数要求绝对路径,传入占位符 base 让相对路径也能解析
+ // 最终只取 pathname + search,base 部分会被丢弃,不影响结果
+ const url = new URL(href, 'http://x');
+ if (!url.searchParams.get('qid')) {
+ url.searchParams.set('qid', qid);
+ }
+ return url.pathname + url.search;
+ } catch {
+ return href;
+ }
+ }
+
+ /**
+ * 创建 router.push 的 Proxy,让所有 SPA 导航自动携带 qid
+ * replace 不注入 qid,避免影响 UtilUrl.replaceState 的精确控制
+ */
+ private static createQidProxy(router: AppRouterInstance): AppRouterInstance {
+ return new Proxy(router, {
+ get(target, prop) {
+ if (prop === 'push') {
+ return (href: string, options?: NavigateOptions) => {
+ target.push(RouterMgr.injectQid(href), options);
+ };
+ }
+ return (target as any)[prop];
+ }
+ });
+ }
+
+ /** login/signup 共用的 query 构建逻辑 */
+ private static buildAuthUrl(
+ path: string,
+ from?: string,
+ extraParams?: [string, string][]
+ ): string {
+ const params = new URLSearchParams();
+ if (from) params.set('from', from);
+ extraParams?.forEach(([k, v]) => params.set(k, v));
+ const query = params.toString();
+ return query ? `${path}?${query}` : path;
+ }
static Init(router: AppRouterInstance, pathname: string) {
- this.router = router;
+ this.router = this.createQidProxy(router);
if (Util.isLog) {
LogUtil.info(this.tag, 'Init router:', router);
LogUtil.info(this.tag, 'Init pathname:', pathname);
@@ -30,77 +79,53 @@ export default class RouterMgr {
static push(href: string, options?: NavigateOptions) {
this.router.push(href, options);
}
-
static back() {
this.router.back();
}
-
- static home(params = '') {
- this.router.push('/' + ChannelMgr.getQid + params);
+ static refresh() {
+ this.router.refresh();
}
-
- static shop() {
- this.router.push('/store' + ChannelMgr.getQid);
+ static replace(value: string) {
+ this.router.replace(value);
}
- static login(params?: [string, string][]) {
- const newPar = ChannelMgr.getNewParam();
- if (params) {
- for (let i = 0; i < params.length; i++) {
- const arr = params[i];
- newPar.set(arr[0], arr[1]);
- }
- }
- this.router.push(`/login?${newPar.toString()}`);
+ /** 跳转首页,Proxy 自动注入 qid */
+ static home(from?: string) {
+ this.router.push(from ? `/?from=${from}` : '/');
}
- static signup(params?: [string, string][]) {
- const newPar = ChannelMgr.getNewParam();
- if (params) {
- for (let i = 0; i < params.length; i++) {
- const arr = params[i];
- newPar.set(arr[0], arr[1]);
- }
- }
- this.router.push(`/signup?${newPar.toString()}`);
+ /** 跳转商店,Proxy 自动注入 qid */
+ static shop() {
+ this.router.push('/store');
}
- /**
- * 社交登录绑定成功后返回个人信息页,携带渠道参数
- */
- static socialLoginReturn() {
- this.router.push('/account/personal-info' + ChannelMgr.getQid);
+ /** 跳转登录页,Proxy 自动注入 qid */
+ static login(from?: string, extraParams?: [string, string][]) {
+ this.router.push(this.buildAuthUrl('/login', from, extraParams));
}
- static refresh() {
- this.router.refresh();
+ /** 跳转注册页,Proxy 自动注入 qid */
+ static signup(from?: string, extraParams?: [string, string][]) {
+ this.router.push(this.buildAuthUrl('/signup', from, extraParams));
}
- static replace(value: string) {
- this.router.replace(value);
+ /** 社交登录绑定成功后返回个人信息页,Proxy 自动注入 qid */
+ static socialLoginReturn() {
+ this.router.push('/account/personal-info');
}
static goActivity(url: string) {
- if (Util.isLog) {
- LogUtil.info(this.tag, 'goActivity:', url);
- }
- switch (url) {
- case ActivityTypes.CashBack:
- this.router.replace('/activity/coinscashback');
- break;
- }
+ if (Util.isLog) LogUtil.info(this.tag, 'goActivity:', url);
+ const activityMap: Record<string, string> = {
+ [ActivityTypes.CashBack]: '/activity/coinscashback'
+ };
+ const path = activityMap[url];
+ if (path) this.router.replace(path);
}
static onPathnameChange(pathName: string, userInfo: UserInfoState, dispatch: any, win: Window) {
- if (Util.isLog) {
- LogUtil.info(this.tag, '路由发生了变化:', pathName);
- }
+ if (Util.isLog) LogUtil.info(this.tag, '路由发生了变化:', pathName);
hideForceLoading();
- // const currentRouterChangeTime = Date.now();
- // if (this.lastRouterChangeTime !== 0) {
- // console.log('间隔:', currentRouterChangeTime - this.lastRouterChangeTime + 'ms');
- // }
- // this.lastRouterChangeTime = Date.now();
const dialogMgr = new DialogMomentMgr(pathName, userInfo, dispatch);
dialogMgr.init(win);
this.dialog = dialogMgr;
src/Core/UtilUrl.tsx
+17 -7
@@ -1,5 +1,4 @@
'use client'; // 声明为客户端组件
-import RouterMgr from '@/Core/RouterMgr';
import DataMgr from '@/Fwk/DataMgr';
import LogUtil from '@/Fwk/LogUtil';
import Util from '@/Master/Util';
@@ -47,30 +46,41 @@ export default class UtilUrl {
return paramsObject;
}
- // 设置或更新参数
+ /**
+ * 静默同步内存 params 到浏览器地址栏
+ * 使用 replaceState 不触发 Next.js 路由,useSearchParams 会自动同步(官方认可的浅路由方式)
+ */
+ private static syncToHistory(): void {
+ if (typeof window === 'undefined') return;
+ const search = this.params.toString() ? `?${this.params.toString()}` : '';
+ window.history.replaceState(window.history.state, '', search || window.location.pathname);
+ }
+
+ // 设置或更新参数(不触发路由)
static setParam(key: string, value: string) {
if (!this.params) return;
this.params.set(key, value);
- RouterMgr.replace(`?${this.params.toString()}`); // 只更新 query
+ this.syncToHistory();
}
/**
- * 删除某个参数
+ * 删除某个参数(不触发路由)
* @param key 参数名
*/
static deleteParam(key: string): void {
if (!this.params) return;
this.params.delete(key);
- RouterMgr.replace(`?${this.params.toString()}`); // 只更新 query
+ this.syncToHistory();
}
/**
- * 清除所有参数
+ * 清除所有参数(不触发路由)
*/
static clearParams(): void {
- RouterMgr.replace('?');
this.params = new URLSearchParams('');
+ this.syncToHistory();
}
+
static get isShowPreinstall() {
return this.getParam('peinstall') === '1';
}
src/Fwk/Channel/ChannelMgr.ts
+1 -15
@@ -35,26 +35,11 @@ export default class ChannelMgr {
return '';
}
- static get getQid() {
- if (this.channelQid) return `?qid=${this.channelQid}`;
- return '';
- }
-
static get channelAttributionData() {
if (this._attributionData) return this._attributionData;
return '';
}
- static getNewParam() {
- const curPar = UtilUrl.params?.toString() || '';
- const newPar = new URLSearchParams(curPar);
-
- if (ChannelMgr.channelQid) {
- newPar.set('qid', ChannelMgr.channelQid);
- }
- return newPar;
- }
-
static Init(): void {
if (!Util.isWindow) return;
if (this.isInitOk) return;
@@ -166,6 +151,7 @@ export default class ChannelMgr {
static setChannelQID(qid: string): void {
this._channelQid = qid;
+ DataMgr.setStorage(fwkConst.CHANNEL_QID, qid); // 强制覆盖 localStorage,修复小号 _ht qid 不更新的 bug
UpdateMainState(this.tag, {
channelQid: qid
});
src/app/(auth)/common/authHelpers.tsx
+20 -18
@@ -1,7 +1,7 @@
import React from 'react';
import { FontIcon } from '@/app/_components/';
import ErrorIcon from '@/app/(auth)/ErrorIcon';
-import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
+
import { checkPhoneNumber, validateEmail, validatePassword } from '@/app/utils/validateForm';
import AuthTrack, { PasswordInputScene, LoginMethod } from '@/Core/AuthTrack';
import { useLoginMethodStore } from '@/app/stores/loginMethodStore';
@@ -11,19 +11,17 @@ import formStyles from '@/app/_components/Form/form.module.css';
export const getFieldClassName = (focusedField: string, fieldName: string) =>
`${formStyles.custom_item_wrapper} ${focusedField === fieldName ? formStyles.focused : ''}`;
-
-
-
// 辅助函数:判断验证码按钮是否禁用
-export const isSmsCodeDisabled = (loginMethod: 'phone' | 'email', mobile: string, areaCode: string, email: string) =>
- loginMethod === 'phone'
- ? !checkPhoneNumber(mobile, areaCode)
- : !validateEmail(email);
+export const isSmsCodeDisabled = (
+ loginMethod: 'phone' | 'email',
+ mobile: string,
+ areaCode: string,
+ email: string,
+) => (loginMethod === 'phone' ? !checkPhoneNumber(mobile, areaCode) : !validateEmail(email));
-// 辅助函数:构建URL(带qid参数)
+// 辅助函数:构建URL
export const buildUrl = (basePath: string, initParams?: Record<string, string>) => {
const params = new URLSearchParams(initParams);
- if (ChannelMgr.channelQid) params.set('qid', ChannelMgr.channelQid);
const query = params.toString();
return query ? `${basePath}?${query}` : basePath;
};
@@ -32,14 +30,17 @@ export const buildUrl = (basePath: string, initParams?: Record<string, string>)
export const handlePasswordChange = (
onChange: (e: any) => void,
e: React.ChangeEvent<HTMLInputElement>,
- scene: PasswordInputScene
+ scene: PasswordInputScene,
) => {
const { loginMethod } = useLoginMethodStore();
onChange(e);
AuthTrack.inputPassword(scene, LoginMethod[loginMethod]);
const isValid = validatePassword(e.target.value);
- AuthTrack[isValid ? 'inputPasswordSuccess' : 'inputPasswordFail'](scene, LoginMethod[loginMethod]);
+ AuthTrack[isValid ? 'inputPasswordSuccess' : 'inputPasswordFail'](
+ scene,
+ LoginMethod[loginMethod],
+ );
};
// 辅助函数:确认密码输入埋点处理
@@ -48,7 +49,7 @@ export const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>,
currentValue: string,
password: string,
- scene: PasswordInputScene
+ scene: PasswordInputScene,
) => {
const { loginMethod } = useLoginMethodStore();
@@ -56,14 +57,16 @@ export const handleConfirmPasswordChange = (
if (currentValue) {
AuthTrack.inputPasswordAgain(scene, LoginMethod[loginMethod]);
const isValid = validatePassword(e.target.value) && password === e.target.value;
- AuthTrack[isValid ? 'inputPasswordAgainSuccess' : 'inputPasswordAgainFail'](scene, LoginMethod[loginMethod]);
+ AuthTrack[isValid ? 'inputPasswordAgainSuccess' : 'inputPasswordAgainFail'](
+ scene,
+ LoginMethod[loginMethod],
+ );
}
};
-
// 辅助函数:批量关闭Dialog
export const closeMultipleDialogs = (SDialogManagerFC: any, dialogs: any[]) => {
- dialogs.forEach(dialog => {
+ dialogs.forEach((dialog) => {
const key = SDialogManagerFC.getFCKey(dialog);
if (key) SDialogManagerFC.closeDialog(key);
});
@@ -75,7 +78,7 @@ export const checkSmsCodeBeforeSubmit = async (
data: { areaCode: string; captcha: string; mobile: string },
scene: any,
$http: any,
- AuthTrack: any
+ AuthTrack: any,
) => {
if (process.env.NODE_ENV === 'production' && loginMethod === 'phone') {
const checkResult = await $http.Auth.checkSmsCode({
@@ -98,4 +101,3 @@ export const resetFormFields = (setValue: any, fields: string[]) => {
setValue(field, '', { shouldValidate: false, shouldTouch: false });
});
};
-
src/app/(auth)/login/page.tsx
+2 -2
@@ -347,7 +347,7 @@ export default function LoginPage() {
id='link-forgot-password'
className={`${styles.forgot_password_link} login-forgot-link`}
href={'/forgot'}
- prefetch={true}
+ prefetch={false}
onClick={handleForgotPassword}
>
Forgot password
@@ -375,7 +375,7 @@ export default function LoginPage() {
<Link
style={{ color: '#998EA4' }}
href={'/signup'}
- prefetch={true}
+ prefetch={false}
onClick={handleCreateAccount}
>
Don't have an account yet?{' '}
src/app/(auth)/signup/modalPage.tsx
+4 -5
@@ -48,6 +48,7 @@ import AuthTrack, {
EmailInputScene,
SignUpScene,
} from '@/Core/AuthTrack';
+import RouterMgr from '@/Core/RouterMgr';
import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
import LogUtil from '@/Fwk/LogUtil';
import { SDialogManagerFC } from '@/Master/SDialogFC/SDialogManagerFC';
@@ -453,7 +454,7 @@ export default function SignupPage({ btnId }: { btnId: string }) {
return;
}
await dispatch(visitorLoginAsync({}));
- router.push('/');
+ RouterMgr.home();
};
const handleClickExistingAccount = () => {
@@ -487,12 +488,10 @@ export default function SignupPage({ btnId }: { btnId: string }) {
const buildUrl = (basePath: string, initParams?: Record<string, string>) => {
const params = new URLSearchParams(initParams || {});
- if (ChannelMgr.channelQid && ChannelMgr.channelQid.length > 0) {
- params.set('qid', ChannelMgr.channelQid);
- }
const query = params.toString();
return query ? `${basePath}?${query}` : basePath;
};
+
const openLadingPageLogin = () => {
handleClickExistingAccount();
TrackFacebook.lp_button_click('btn_09');
@@ -844,7 +843,7 @@ export default function SignupPage({ btnId }: { btnId: string }) {
<Link
className='common-link'
href={buildUrl('/')}
- prefetch={true}
+ prefetch={false}
onClick={handleVisitorJump}
>
Play as guest
src/app/(auth)/signup/page.tsx
+3 -3
@@ -294,7 +294,7 @@ export default function SignupPage() {
return;
}
await dispatch(visitorLoginAsync({}));
- router.push('/');
+ RouterMgr.home();
};
const handleClickExistingAccount = () => {
@@ -623,7 +623,7 @@ export default function SignupPage() {
<Link
className='common-link'
href={buildUrl('/')}
- prefetch={true}
+ prefetch={false}
onClick={handleVisitorJump}
>
Play as guest
@@ -633,7 +633,7 @@ export default function SignupPage() {
<Link
style={{ color: '#998EA4' }}
href={'/login'}
- prefetch={true}
+ prefetch={false}
onClick={handleClickExistingAccount}
>
Already have an account?{' '}
src/app/(game)/GameMgr.ts
+1 -1
@@ -53,7 +53,7 @@ export default class GameMgr {
// RouterMgr.goActivity('cashback');
// return
// }
- // RouterMgr.home('&from=game');
+ // RouterMgr.home('game');
// });
const from = getUrlParams('from') ?? undefined;
UnityResourceMgr.quitAndGoByFrom(from);
src/app/(game)/components/GameHome/GameHome.tsx
+1 -1
@@ -19,7 +19,7 @@ const GameHome = () => {
// RouterMgr.goActivity('cashback');
// return
// }
- // RouterMgr.home('&from=game');
+ // RouterMgr.home('game');
// });
UnityResourceMgr.quitAndGoByFrom(from);
}}
src/app/(game)/game/UnityResourceMgr.ts
+1 -1
@@ -53,7 +53,7 @@ export default class UnityResourceMgr {
RouterMgr.goActivity('cashback');
break;
default:
- RouterMgr.home('&from=game');
+ RouterMgr.home('game');
}
}
}
src/app/(game)/game/page.tsx
+1 -1
@@ -189,7 +189,7 @@ const GamePage = () => {
//通知子页面游戏执行token过期处理
GameMsgUtil.sendMsgLower({ type: GameMsgKey.game_page_running });
// 这里不需要处理,直接返回首页
- RouterMgr.home('&from=game');
+ RouterMgr.home('game');
// checkShowGuideDialog(DialogShowType.OnSlotToHome)
if (Util.isLog) {
LogUtil.info(`广播通讯-Game`, '消息变动-end', tabChannelMsg.type);
src/app/(game2)/game2/page.tsx
+2 -2
@@ -130,7 +130,7 @@ export default function Game2() {
switch (tabChannelMsg.type) {
case GameMsgKey.game_page_running:
// 这里不需要处理,直接返回首页
- RouterMgr.home('&from=game');
+ RouterMgr.home('game');
// checkShowGuideDialog(DialogShowType.OnSlotToHome)
break;
}
@@ -171,7 +171,7 @@ export default function Game2() {
);
const handleBackClick = useCallback(() => {
- RouterMgr.home('&from=game');
+ RouterMgr.home('game');
}, []);
// 主要流程:接收游戏错误 -> 打印错误信息 -> 显示游戏不可用弹窗
src/app/(pages)/account/member/page.tsx
+2 -3
@@ -1,6 +1,6 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import { selectAccountInfo } from '@/app/(pages)/account/accountSlice';
import { FontIcon, Carousel, Button } from '@/app/_components';
@@ -23,7 +23,6 @@ function MemberIcon({ level }: { level: number }) {
}
export default function MemberPage() {
- const router = useRouter();
const account = useAppSelector(selectAccountInfo);
const [showTips, setShowTips] = useState(false);
const [isShowLimits, setShowLimits] = useState(false);
@@ -328,7 +327,7 @@ export default function MemberPage() {
}}
color='#0090FF'
onClick={() => {
- router.push('/store/');
+ RouterMgr.push('/store');
}}
>
Upgrade Now
src/app/(pages)/account/page.tsx
+4 -4
@@ -148,7 +148,7 @@ export default function AccountPage() {
useEffect(() => {
if (userInfo.isLoggedOut === true && !isGameGod() && !isLandingPath()) {
- router.push(`/login?from=signOut`);
+ RouterMgr.push('/login?from=signOut');
}
setNickname(userInfo.nickname);
}, [userInfo]);
@@ -164,7 +164,7 @@ export default function AccountPage() {
const handleJump = (path: string) => {
TrackCommon.click_vip_page();
- router.push(path);
+ RouterMgr.push(path);
};
const handleSignOut = async () => {
@@ -177,7 +177,7 @@ export default function AccountPage() {
}
TrackCommon.click_sign_out(2);
await handleGetRewardConfig(); // 获取奖励配置
- router.push(`/signup${ChannelMgr.getQid}`);
+ RouterMgr.push('/signup');
setOpenSignUpType('page');
return;
}
@@ -309,7 +309,7 @@ export default function AccountPage() {
<AnimatedButton
icon='s-form-f'
onClick={() => {
- router.push(`/account/personal-info${ChannelMgr.getQid}`);
+ RouterMgr.push('/account/personal-info');
TrackCommon.click_personal_info();
}}
>
src/app/(pages)/account/personal-info/page.tsx
+4 -8
@@ -5,8 +5,8 @@ import ScrollableContainer from '@/app/_components/ScrollableContainer/Scrollabl
import SDialogHeader from '@/app/_components/SDialogHeader/SDialogHeader';
import FontIcon from '@/app/_components/FontIcon/fontIcon';
import TrackCommon from '@/Core/Track/TrackCommon';
-import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
import RouterMgr from '@/Core/RouterMgr';
+
import PersonalInfoUI from './PersonalInfoUI';
import styles from './page.module.css';
@@ -16,18 +16,15 @@ export default function PersonalInfoPage() {
TrackCommon.show_account_page();
}, []);
- // 主要流程:返回账户页(带上 QID)
+ // 主要流程:返回账户页(Proxy 自动注入 qid)
const handleBack = () => {
- RouterMgr.push('/account' + ChannelMgr.getQid);
+ RouterMgr.push('/account');
};
return (
<div className={styles.page}>
<ComBG>
- <SDialogHeader
- title='PERSONAL INFO'
- onClickBack={handleBack}
- />
+ <SDialogHeader title='PERSONAL INFO' onClickBack={handleBack} />
<ScrollableContainer>
<PersonalInfoUI />
</ScrollableContainer>
@@ -35,4 +32,3 @@ export default function PersonalInfoPage() {
</div>
);
}
-
src/app/(pages)/components/Banner/Banner.tsx
+1 -1
@@ -312,7 +312,7 @@ export const Banner = () => {
!Number.isNaN(affiliateOpenVipLevel) &&
userInfo.vipLevel >= affiliateOpenVipLevel
) {
- window.location.href = '/affiliate/';
+ window.location.href = '/affiliate';
}
return;
}
src/app/(pages)/components/CoinsCashBackPopup/index.tsx
+3 -4
@@ -10,7 +10,7 @@ import { Timeout } from 'ahooks/es/useRequest/src/types';
import Util from '@/Master/Util';
import LogUtil from '@/Fwk/LogUtil';
import { useRouter } from 'next/navigation';
-import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
+import RouterMgr from '@/Core/RouterMgr';
import { SDialogManagerFC } from '@/Master/SDialogFC/SDialogManagerFC';
import { RewardDialogFC } from '@/app/_dialogs/RewardDialog/RewardDialog';
import $http from '@/app/_api';
@@ -53,10 +53,9 @@ const CoinsCashBackPopupContent: React.FC<CoinsCashBackPopupProps> = () => {
});
const handleOpenDialog = useCallback(async () => {
- // await openCoinsCashBackModal();
TrackCoinsCashBack.home_icon_click();
- router.push(`/activity/coinscashback${ChannelMgr.getQid}`);
- }, [ChannelMgr.getQid]);
+ RouterMgr.push('/activity/coinscashback');
+ }, []);
useEffect(() => {
TrackCoinsCashBack.home_icon_show();
src/app/(pages)/components/Header/AppHeader.tsx
+7 -7
@@ -1,6 +1,5 @@
'use client';
import React, { ChangeEvent, memo, useEffect, useState } from 'react';
-import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
@@ -12,16 +11,17 @@ import { selectUserInfo, SET_REWARD_TYPE } from '@/app/userSlice';
import { setLocalRewardType } from '@/app/utils/auth';
import { switchTheme } from '@/app/utils/themeSwitch';
import { RewardType } from '@/Core/Reward/RewardInfo';
-import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
+import RouterMgr from '@/Core/RouterMgr';
import styles from './AppHeader.module.css';
+
import { getImagePath } from '@/app/utils/ossImagePath';
import KitPwa from '@/Master/PWA/KitPwa';
import Util from '@/Master/Util';
import useScrollShadow from '@/app/hooks/useScrollShadow';
const AppHeader = () => {
- const router = useRouter();
const pathname = usePathname();
+
const userInfo = useAppSelector(selectUserInfo);
const account = useAppSelector(selectAccountInfo);
const mainState = useAppSelector(selectMainState);
@@ -87,13 +87,13 @@ const AppHeader = () => {
className={`${styles.header} ${fade ? styles.fade : ''} ${shrink ? styles.shrink : ''} ${mainState.isHeaderShow ? styles.show_header : ''} ${scrolled ? styles.scrolled : ''}`}
>
<div className={`${styles.header_wrapper}`}>
- <Link href={`/${ChannelMgr.getQid}`} className={styles.header_logoa}>
+ <div className={styles.header_logoa} onClick={() => RouterMgr.home()}>
<img
className={styles.header_logo}
src={getImagePath('/img/logo.webp')}
alt=''
/>
- </Link>
+ </div>
{/* 奖励切换按钮 */}
<div
id='guideStep1_2'
@@ -115,7 +115,7 @@ const AppHeader = () => {
type={userInfo.rewardType}
isShowAdd={true}
click={() => {
- router.push('/store');
+ RouterMgr.push('/store');
}}
/>
</div>
@@ -128,7 +128,7 @@ const AppHeader = () => {
(item: any) => item.levelName === account.info.userVipName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
}}
>
<div
src/app/(pages)/components/Header/WebHeader.tsx
+3 -3
@@ -1,6 +1,7 @@
'use client';
import React, { ChangeEvent, memo, useEffect, useState } from 'react';
-import { usePathname, useRouter } from 'next/navigation';
+import { usePathname } from 'next/navigation';
+
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
import CoinLabel from '@/app/_components/Coin/CoinLabel';
import { FOOTER_PAGES, HashMapPathName } from '@/app/config';
@@ -23,7 +24,6 @@ import styles from './WebHeader.module.css';
import { Toast } from '@/app/_components';
const WebHeader = () => {
- const router = useRouter();
const pathname = usePathname();
const userInfo = useAppSelector(selectUserInfo);
const account = useAppSelector(selectAccountInfo);
@@ -165,7 +165,7 @@ const WebHeader = () => {
(item: any) => item.levelName === account.info.userVipName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
TrackCommon.click_vip_page();
}}
>
src/app/(pages)/components/RechargeRebate/UI/RechargeRebateDialogUI.tsx
+2 -2
@@ -293,10 +293,10 @@ export default function RechargeRebateDialogUI(props: RechargeRebateDialogUIProp
</div>
<div className={styles.step_container}>
<div className={styles.step_content}>
- {StepFC({ step: rechargedDays, stepTotal: reqRechargeDays })}
+ <StepFC step={rechargedDays} stepTotal={reqRechargeDays} />
</div>
<div className={styles.step_bg_container}>
- {StepTotalFC({ stepTotal: reqRechargeDays })}
+ <StepTotalFC stepTotal={reqRechargeDays} />
</div>
</div>
<div className={styles.time}>
src/app/(pages)/components/VipCarnivalDayPopup/TaskItem/TaskItem.tsx
+49 -35
@@ -2,7 +2,10 @@
import React, { useMemo } from 'react';
import { Button } from '@/app/_components';
import TrackVipCarnivalDay from '@/Core/Track/TrackVipCarnivalDay';
-import { hideForceLoading, showForceLoading } from '@/app/_dialogs/LoadingForceDialog/LoadingForceDialogFC';
+import {
+ hideForceLoading,
+ showForceLoading,
+} from '@/app/_dialogs/LoadingForceDialog/LoadingForceDialogFC';
import {
CLICK_TYPES,
TASK_STATUS,
@@ -23,12 +26,22 @@ interface TaskItemProps extends TaskItemType {
vipCarnivalDayInfoData: VipCarnivalDayProps;
}
-const TaskItem: React.FC<TaskItemProps> = ({ userVipLevel, type, target, status, reward, taskId, claimVipCarnivalDayReward, vipCarnivalDayInfoData, before }) => {
+const TaskItem: React.FC<TaskItemProps> = ({
+ userVipLevel,
+ type,
+ target,
+ status,
+ reward,
+ taskId,
+ claimVipCarnivalDayReward,
+ vipCarnivalDayInfoData,
+ before,
+}) => {
const { betAmount } = vipCarnivalDayInfoData;
const title = useMemo(() => {
if (TASK_TYPES.LOGIN === type) {
return 'Login';
- } else if (TASK_TYPES.VIP_LEVEL ===type) {
+ } else if (TASK_TYPES.VIP_LEVEL === type) {
return 'VIP extra benefits';
} else if (TASK_TYPES.BET === type) {
return 'Cumulative bet of';
@@ -48,14 +61,19 @@ const TaskItem: React.FC<TaskItemProps> = ({ userVipLevel, type, target, status,
await before?.();
if (TASK_STATUS.UNFINISHED === status) {
if (TASK_TYPES.BET === type) {
- RouterMgr.home('&from=vipCarnivalDay');
+ RouterMgr.home('vipCarnivalDay');
} else {
// 跳转到商店
RouterMgr.shop();
}
- TrackVipCarnivalDay.carnival_activity_click(vipCarnivalDayInfoData, CLICK_TYPES.GO_PLAY).then();
+ TrackVipCarnivalDay.carnival_activity_click(
+ vipCarnivalDayInfoData,
+ CLICK_TYPES.GO_PLAY,
+ ).then();
} else if (TASK_STATUS.AVAILABLE === status) {
- TrackVipCarnivalDay.carnival_activity_click(vipCarnivalDayInfoData, CLICK_TYPES.CLAIM, { currentTaskId: taskId }).then();
+ TrackVipCarnivalDay.carnival_activity_click(vipCarnivalDayInfoData, CLICK_TYPES.CLAIM, {
+ currentTaskId: taskId,
+ }).then();
// 可领取状态 等级等于0则不满足条件提示升级领取
if (userVipLevel < 1) {
await openUnlockedDialog();
@@ -72,38 +90,34 @@ const TaskItem: React.FC<TaskItemProps> = ({ userVipLevel, type, target, status,
<>
<div className={styles.activityMissionsDetail}>
<span className={styles.activityMissionsDetailTitle}>{title}&nbsp;</span>
- {
- subTaskTitle && (
- <b className={styles.activityMissionsBet}>{subTaskTitle}</b>
- )
- }
- {
- TASK_TYPES.BET === type && (
- <>
- (<strong className={styles.activityMissionsBetCurrent}>{Math.min(betAmount, target)}</strong>/{target})
- </>
- )
- }
+ {subTaskTitle && <b className={styles.activityMissionsBet}>{subTaskTitle}</b>}
+ {TASK_TYPES.BET === type && (
+ <>
+ (
+ <strong className={styles.activityMissionsBetCurrent}>
+ {Math.min(betAmount, target)}
+ </strong>
+ /{target})
+ </>
+ )}
</div>
<div className={styles.activityMissionsButton}>
{reward} Free spins
- {
- [TASK_STATUS.UNFINISHED, TASK_STATUS.AVAILABLE].includes(status) && (
- <Button
- color={TASK_STATUS.UNFINISHED === status ? '#0090FF' : '#17C147'}
- style={{
- color: '#fff',
- width: '2.8rem',
- height: '1.1rem',
- fontSize: '.52rem',
- fontWeight: 900
- }}
- onClick={() => doTask()}
- >
- { TASK_STATUS.UNFINISHED === status ? 'Go' : 'Claim' }
- </Button>
- )
- }
+ {[TASK_STATUS.UNFINISHED, TASK_STATUS.AVAILABLE].includes(status) && (
+ <Button
+ color={TASK_STATUS.UNFINISHED === status ? '#0090FF' : '#17C147'}
+ style={{
+ color: '#fff',
+ width: '2.8rem',
+ height: '1.1rem',
+ fontSize: '.52rem',
+ fontWeight: 900,
+ }}
+ onClick={() => doTask()}
+ >
+ {TASK_STATUS.UNFINISHED === status ? 'Go' : 'Claim'}
+ </Button>
+ )}
</div>
</>
);
src/app/(pages)/components/VipCarnivalDayPopup/VipCarnivalDayPopup.tsx
+2 -3
@@ -1,7 +1,7 @@
// 图标入口文件
'use client';
import React, { useEffect, useMemo } from 'react';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import Image from 'next/image';
import { useAppSelector } from '@/app/storeHooks';
import { selectUserInfo } from '@/app/userSlice';
@@ -18,7 +18,6 @@ const VipCarnivalDayPopupContent: React.FC = () => {
const { vipCarnival } = useRedDotViewModel(selectRedDotInfo);
const { vipCarnivalDayInfoData } = useVipCarnivalDay();
- const router = useRouter();
const {
hasVipCarnivalRedPoint = false, // 红点状态
} = userInfo || {};
@@ -35,7 +34,7 @@ const VipCarnivalDayPopupContent: React.FC = () => {
const handleOpenDialog = async () => {
VipCarnivalDayMgr.info(`entrance clicked, status: ${status}`);
TrackVipCarnivalDay.vip_carnival_icon_click(vipCarnivalDayInfoData).then();
- router.push(`/activity/vip-carnival-day`);
+ RouterMgr.push('/activity/vip-carnival-day');
};
// 入口曝光
src/app/(pages)/components/VipClubPopup/VipClubPopup.tsx
+2 -3
@@ -2,19 +2,18 @@ import React, { useEffect, useCallback, useMemo, memo } from 'react';
import Image from 'next/image';
import { useAppDispatch, useAppSelector } from '@/app/storeHooks';
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import styles from './VipClubPopup.module.css';
import { getImagePath } from '@/app/utils/ossImagePath';
const VipClubPopup: React.FC = () => {
const dispatch = useAppDispatch();
const account = useAppSelector(selectAccountInfo);
- const router = useRouter();
const handleOpenDialog = useCallback(async () => {
const index = account.info.vipConfigList.vipLevels.findIndex(
(item: any) => item.levelName === account.info.userVipName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
}, [account.info.vipConfigList.vipLevels, account.info.userVipName]);
return (
<div className={styles.vipClubPopupMain} onClick={handleOpenDialog}>
src/app/(pages)/components/affiliate/AffiliateDialog.tsx
+1 -1
@@ -60,7 +60,7 @@ async function jumpToAffiliate(
}
setTrackBasicInfo(affiliateTrackBasicInfo);
TrackAffiliate.affiliate_enter_click(affiliateTrackBasicInfo, userInfo.vipLevel, entrySource);
- window.location.href = '/affiliate/';
+ window.location.href = '/affiliate';
return true;
}
src/app/(pages)/components/footer.tsx
+7 -63
@@ -12,21 +12,16 @@ import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
import { motion } from 'motion/react';
import useResize from '@/app/hooks/useResize';
import TrackRedeem from '@/Core/Track/TrackRedeem';
-import Util from '@/Master/Util';
-import { useDebounceEffect } from 'ahooks';
-import { useRouter } from 'next/navigation';
import { selectRedDotInfo, useRedDotViewModel } from '@/app/(viewmodels)/useRedDotViewModel';
import UnityGameDataManager from '@/app/(game)/game/UnityGameDataManager';
import { useDailyMissionRedDotViewModel } from './DailyMissions/ViewModels/DailyMissionRedDotViewModel';
import GeoComply from '@/Fwk/GeoComply/GeoComply';
import { GeoSceneId } from '@/Fwk/GeoComply/GeoSceneId';
-
+import RouterMgr from '@/Core/RouterMgr';
import styles from './footer.module.css';
export const FooterBase = () => {
const pathname = usePathname() || '';
- const router = useRouter();
-
const userInfo = useAppSelector(selectUserInfo);
const redDotInfo = useRedDotViewModel(selectRedDotInfo);
@@ -57,51 +52,6 @@ export const FooterBase = () => {
}, []),
);
- // 主要流程:预加载所有页面,提升页面切换性能
- const prefetchedRef = useRef<Set<string>>(new Set());
-
- // 预加载所有核心页面
- useDebounceEffect(
- () => {
- // 主要流程:预加载Footer页面和其他重要页面,提升页面切换性能
- // 只在生产环境下预加载页面
- if (Util.isDev) {
- return;
- }
- const prefetchPage = (path: string) => {
- // 只预加载基础路由,不包含qid参数
- // Next.js会预加载页面组件文件,查询参数不影响预加载效果
- const baseUrl = path;
-
- if (!prefetchedRef.current.has(baseUrl)) {
- router.prefetch(baseUrl);
- prefetchedRef.current.add(baseUrl);
- if (Util.isLog) {
- console.log(`预加载页面: ${baseUrl}`);
- }
- }
- };
-
- // Footer页面 - 只预加载基础路由
- FOOTER_PAGES.forEach((page) => {
- const baseUrl = `/${page.url === '/' ? '' : page.url}`;
- prefetchPage(baseUrl);
- });
-
- // 其他重要页面
- const additionalPages = [
- '/game',
- '/signup',
- '/forgot',
- '/activity/coinscashback',
- '/login',
- ];
- additionalPages.forEach(prefetchPage);
- },
- [], // 不需要监听qid变化,因为预加载的是基础路由
- { wait: 100 },
- );
-
const updateTabActive = () => {
const tabs = footerRef.current?.querySelectorAll(`.${styles.tab}`);
if (!tabs) return;
@@ -137,24 +87,20 @@ export const FooterBase = () => {
const getUrl = (row: FooterPage) => {
let url = `/${row.url === '/' ? '' : row.url}`;
- if (ChannelMgr.channelQid && ChannelMgr.channelQid.length > 0) {
- url += `?qid=${ChannelMgr.channelQid}`;
- }
+ // Link 不经过 RouterMgr Proxy,需要手动注入 qid
+ const qid = ChannelMgr.channelQid;
+ if (qid) url += `?qid=${qid}`;
return url;
};
const onClickBtn = async (row: FooterPage, event: React.MouseEvent<HTMLAnchorElement>) => {
- // 检查是否超过最大失败次数
+ // 检查是否超过最大失败次数,阻止默认跳转
if (GeoComply.isExceedMaxFailCount()) {
- // 阻止默认跳转行为
event.preventDefault();
- // 显示超限弹窗
await GeoComply.showExceedLimitDialog(GeoSceneId.enterHome);
return;
}
- window.routeStartTime = performance.now(); // ✅ 记录点击跳转时间
-
switch (row.name) {
case 'STORE':
TrackCommon.clickStoreButton();
@@ -221,10 +167,8 @@ export const FooterBase = () => {
data-active={isActiveTab(row.url, pathname) ? '1' : ''}
href={getUrl(row)}
key={row.name}
- prefetch={true} // 强制启用预加载
- onClick={(e) => {
- onClickBtn(row, e);
- }}
+ prefetch={false}
+ onClick={(e) => onClickBtn(row, e)}
>
<div className={`${styles.tab_box}`}>
<div className={styles.tab_icon_wrapper}>
src/app/(pages)/paymentFlow.ts
+1 -1
@@ -270,7 +270,7 @@ async function onRewardAni(item: ShopItem, data: any) {
const userInfo = store.getState().userInfo;
if (userInfo.loginType == LoginType.visitor) {
await handleGetRewardConfig();
- RouterMgr.signup([['from', 'payOk']]);
+ RouterMgr.signup('payOk');
}
},
gcCount: item.gc,
src/app/(pages)/rewards/hooks/useRewardHandlers.ts
+1 -1
@@ -185,7 +185,7 @@ export const useRewardHandlers = () => {
);
const affiliateOpenVipLevel = getSafeAffiliateOpenVipLevel();
if (!Number.isNaN(affiliateOpenVipLevel) && userInfo.vipLevel >= affiliateOpenVipLevel) {
- window.location.href = '/affiliate/';
+ window.location.href = '/affiliate';
return;
}
Toast.show('Coming soon');
src/app/(pay)/UI/ForcedSignUpDialog/ForcedSignUpDialog.tsx
+3 -4
@@ -1,6 +1,7 @@
'use client';
import React, { useEffect } from 'react';
-import { useRouter, usePathname } from 'next/navigation';
+import { usePathname } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import { imgZcts } from '@/app/(pay)/UI/ForcedSignUpDialog/imgBase64';
import { PaySelectionDialogFC } from '@/app/(pay)/UI/PaySelectionDialog/PaySelectionDialogFC';
@@ -19,7 +20,6 @@ export type ForcedSignUpDialogProps = SDialogProps;
export const ForcedSignUpDialogFC: React.FC<ForcedSignUpDialogProps> = (props) => {
const { fcKey, onClose, isCloseBtn } = props;
const { closeDialog } = useDialogMethods(fcKey);
- const router = useRouter();
const pathname = usePathname();
const onCloseCall = () => {
@@ -63,8 +63,7 @@ export const ForcedSignUpDialogFC: React.FC<ForcedSignUpDialogProps> = (props) =
onClick: async () => {
await handleGetRewardConfig();
onCloseCall();
- router.push(`/signup/?from=payOk`);
- // RouterMgr.signup();
+ RouterMgr.signup('payOk');
},
style: 'greenBtn',
},
src/app/(pay)/UI/PaySelectionDialog/PaySelectionDialogFC.tsx
+1 -1
@@ -168,7 +168,7 @@ export const PaySelectionDialogFC: React.FC<PaySelectionDialogProps> = (props) =
//================ end =================
if (userInfo.loginType == LoginType.visitor) {
await handleGetRewardConfig();
- RouterMgr.signup([['from', 'payOk']]);
+ RouterMgr.signup('payOk');
}
},
gcCount: item.gc,
src/app/(pay)/UI/PayWaitingDialog/PayWaitingDialog.tsx
+1 -1
@@ -185,7 +185,7 @@ export const PayWaitingDialogFC: React.FC<PayWaitingDialogProps> = (props) => {
}
if (userInfo.loginType == LoginType.visitor) {
await handleGetRewardConfig();
- RouterMgr.signup([['from', 'payOk']]);
+ RouterMgr.signup('payOk');
}
if (
!userMobile &&
src/app/(redeem)/redeemCashOut/RedeemCashOutUI.tsx
+57 -41
@@ -51,7 +51,7 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
const { withdrawalThreshold } = useAppSelector(selectMainState);
- const broadcast = useRef<BroadcastChannel>();
+ const broadcast = useRef<BroadcastChannel | null>(null);
const [currentTier, setCurrentTier] = useState<number>(0);
const money = React.useMemo(() => {
const value = Number(moneyValue);
@@ -115,7 +115,7 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
}
return () => {
broadcast.current?.close();
- }
+ };
}, []);
const updateChannelCheckData = async () => {
@@ -174,7 +174,7 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
const onRedeemX = async (result: RedeemResult) => {
showForceLoading();
- setIsRedeeming(true)
+ setIsRedeeming(true);
const sendData = {} as RequestRedeemApplyData;
sendData.currency = 'USD';
sendData.money = money;
@@ -237,7 +237,7 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
});
}
SDialogManagerFC.closeDialogFC(RedeemCashOutDialogFC);
- setIsRedeeming(false)
+ setIsRedeeming(false);
break;
case RedeemResultCode.ServerError:
BigTips.showMsg(result.errorMessage);
@@ -276,15 +276,18 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
if (newValue3 > maxMoney) {
setMoneyValue(maxMoney + '');
}
- if(redeemCheckData.redeemInputType === 2) {
- const _money = newValue3 - newValue3 % redeemCheckData.redeemGranularity;
+ if (redeemCheckData.redeemInputType === 2) {
+ const _money = newValue3 - (newValue3 % redeemCheckData.redeemGranularity);
setMoneyValue(_money + '');
}
};
const amountErrTips = () => {
if (money == 0) return '';
- if (redeemCheckData.redeemInputType === 2 && money % redeemCheckData.redeemGranularity !== 0) {
+ if (
+ redeemCheckData.redeemInputType === 2 &&
+ money % redeemCheckData.redeemGranularity !== 0
+ ) {
return `Redemption amount must be divisible by ${DriveUtil.ToScString(redeemCheckData.redeemGranularity, true)} for successful processing due to payment limitations.`;
}
if (redeemCheckData.redeemSwitch === 1 && money > redeemCheckData.redeemLimit) {
@@ -304,7 +307,7 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
}
setCurrentTier(tier);
setMoneyValue(tier + '');
- }
+ };
return (
<div className={styles.redeem_cash_out_wrapper}>
@@ -348,44 +351,57 @@ export const RedeemCashOutUI: React.FC<RedeemCashOutUIProps> = (props) => {
<div className={`${styles.outBox} `}>
<div className={`center-left ${styles.eTitle}`}>
Withdrawal Amount:
- {redeemCheckData.redeemInputType === 3 && <img
- src={getImagePath(`/img/redeem/${getYuanImgName()}.webp`)}
- className={styles.amout_img}
- />}
+ {redeemCheckData.redeemInputType === 3 && (
+ <img
+ src={getImagePath(`/img/redeem/${getYuanImgName()}.webp`)}
+ className={styles.amout_img}
+ />
+ )}
</div>
<div className={`${styles.outAmount} center-left`}>
- {redeemCheckData.redeemInputType !==3 && <><div className={`${styles.outAmount_type} center`}>
- <img src={getImagePath(`/img/redeem/${getYuanImgName()}.webp`)} />
- </div>
- <div className={styles.outAmount_input}>
- <input
- type='number'
- placeholder='Enter Redemption Amount'
- value={moneyValue}
- onChange={(e) => onWithdrawalAmountChange(e.target.value)}
- onBlur={(e) => onWithdrawalAmountBlur(e.target.value)}
- />
- {amountErrTips() && (
- <div className={styles.outAmount_input_err}>
- <span>{amountErrTips()}</span>
+ {redeemCheckData.redeemInputType !== 3 && (
+ <>
+ <div className={`${styles.outAmount_type} center`}>
+ <img
+ src={getImagePath(
+ `/img/redeem/${getYuanImgName()}.webp`,
+ )}
+ />
</div>
- )}
- </div></>}
- {redeemCheckData.redeemInputType === 3 && redeemCheckData?.redeemTiers?.length && (
- <div className={styles.redeem_tiers}>
- {redeemCheckData.redeemTiers.map((item) => (
- <div
- className={`${styles.redeem_tier_item}
+ <div className={styles.outAmount_input}>
+ <input
+ type='number'
+ placeholder='Enter Redemption Amount'
+ value={moneyValue}
+ onChange={(e) =>
+ onWithdrawalAmountChange(e.target.value)
+ }
+ onBlur={(e) => onWithdrawalAmountBlur(e.target.value)}
+ />
+ {amountErrTips() && (
+ <div className={styles.outAmount_input_err}>
+ <span>{amountErrTips()}</span>
+ </div>
+ )}
+ </div>
+ </>
+ )}
+ {redeemCheckData.redeemInputType === 3 &&
+ redeemCheckData?.redeemTiers?.length && (
+ <div className={styles.redeem_tiers}>
+ {redeemCheckData.redeemTiers.map((item) => (
+ <div
+ className={`${styles.redeem_tier_item}
${minMoney > item || maxMoney < item || item > redeemCheckData.redeemLimit ? styles.redeem_tier_item_disabled : ''}
${currentTier === item ? styles.redeem_tier_item_selected : ''}`}
- key={item}
- onClick={() => selectTier(item)}
- >
- <span>{item}</span>
- </div>
- ))}
- </div>
- )}
+ key={item}
+ onClick={() => selectTier(item)}
+ >
+ <span>{item}</span>
+ </div>
+ ))}
+ </div>
+ )}
</div>
<div className={`${styles.outInfo}`}>
src/app/(shop)/ShopNewVipIcon.tsx
+3 -7
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
import styles from '@/app/(pages)/store/page.module.css';
@@ -18,8 +18,6 @@ export default function ShopVipIcon(props: ShopVipIconProps) {
const account = useAppSelector(selectAccountInfo);
const userInfo = useAppSelector(selectUserInfo);
- const router = useRouter();
-
const { shopItem } = props;
const vipConfigList = account.info.vipConfigList.vipLevels;
const userVipExp = userInfo.vipExp;
@@ -52,15 +50,13 @@ export default function ShopVipIcon(props: ShopVipIconProps) {
(item: any) => item.levelName === currentVipName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
};
return (
<div className={styles.icon_vip_new} onClick={handleJump}>
{isUpgrade && <img className={styles.icon_upgrade_new} src={imgUpgrade} alt='' />}
- <div className={styles.icon_grade}>
- {icon && <img src={icon} alt='' />}
- </div>
+ <div className={styles.icon_grade}>{icon && <img src={icon} alt='' />}</div>
</div>
);
}
src/app/(shop)/ShopVipIcon.tsx
+2 -4
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
import styles from '@/app/(pages)/store/page.module.css';
@@ -18,8 +18,6 @@ export default function ShopVipIcon(props: ShopVipIconProps) {
const account = useAppSelector(selectAccountInfo);
const userInfo = useAppSelector(selectUserInfo);
- const router = useRouter();
-
const { shopItem } = props;
const vipConfigList = account.info.vipConfigList.vipLevels;
const userVipExp = userInfo.vipExp;
@@ -53,7 +51,7 @@ export default function ShopVipIcon(props: ShopVipIconProps) {
(item: any) => item.levelName === currentVipName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
};
return (
src/app/Main.tsx
+7 -2
@@ -60,6 +60,13 @@ export default function Main({ children }: MainProps) {
const pathname = usePathname() || '';
const isGamePage = useMemo(() => pathname.includes('unity-game'), [pathname]);
+ // 【关键修复】将 RouterMgr 初始化放在 Render 阶段的最顶端!
+ // 因为 React 的 useEffect 执行顺序是先子后父,
+ // 如果在父组件的 useEffect 里初始化,子组件的 useEffect 会在之前执行并报错
+ useMemo(() => {
+ RouterMgr.Init(router, pathname || '');
+ }, [router, pathname]);
+
// 使用核心业务逻辑 Hook
const { handleNotificationReward } = useNotificationReward();
const { isShowGuideIOS, closeGuide } = useGuideIOS();
@@ -172,8 +179,6 @@ export default function Main({ children }: MainProps) {
if (Util.isLog) LogUtil.info('[Main] 初始化数据在这里');
// 2. 有轻微依赖的任务,建议顺序执行
- RouterMgr.Init(router, pathname || ''); // 路由初始化,后续如有依赖需保证顺序
-
MainScene.onInit(); // 主场景初始化,内部有多项子初始化
// 根据url参数自动登录并回到首页
await useAuthUrlParams();
src/app/RootClientComponent.tsx
+2 -8
@@ -1,11 +1,8 @@
'use client';
import dynamic from 'next/dynamic';
-import React, { ReactNode, Suspense, useMemo } from 'react';
+import React, { ReactNode, Suspense } from 'react';
-import RouteProfiler from '@/app/RouteProfiler';
-import Util from '@/Master/Util';
import Loading from '@/app/loading-back';
-import DailyMissionsFloatTipsHomeUI from '@/app/(pages)/components/DailyMissions/Views/DailyMissionsFloatTipsHomeUI';
// 动态导入 Main 组件,关闭服务端渲染
const Main = dynamic(() => import('@/app/Main'), {
@@ -14,13 +11,10 @@ const Main = dynamic(() => import('@/app/Main'), {
});
export default function RootClientComponent({ children }: { children: ReactNode }) {
- //避免每次路由切换都重新加载 Main
- const memoMain = useMemo(() => <Main>{children}</Main>, []);
return (
<>
<Suspense fallback={<Loading />}>
- {Util.isLog && <RouteProfiler />}
- {memoMain}
+ <Main>{children}</Main>
</Suspense>
</>
);
src/app/RouteProfiler.tsx
+0 -45
@@ -1,45 +0,0 @@
-'use client';
-
-import { usePathname } from 'next/navigation';
-import { useEffect, useRef } from 'react';
-import Util from '@/Master/Util';
-import LogUtil from '@/Fwk/LogUtil';
-
-declare global {
- interface Window {
- routeStartTime?: number;
- }
-}
-
-export default function RouteProfiler() {
- const pathname = usePathname();
- const prevPath = useRef('');
- const loadStartTime = useRef(0);
-
- // 初次加载,记录起点
- useEffect(() => {
- loadStartTime.current = performance.now();
- prevPath.current = pathname;
- }, []);
-
- useEffect(() => {
- const now = performance.now();
-
- if (prevPath.current !== pathname) {
- const routeStart = window.routeStartTime ?? loadStartTime.current;
- const duration = Math.round(now - routeStart);
- const from = prevPath.current;
- const to = pathname;
-
- if (Util.isLog) {
- LogUtil.info(`✅ 路由跳转完成: ${from} → ${to},耗时 ${duration}ms`);
- }
-
- prevPath.current = to;
- loadStartTime.current = now;
- window.routeStartTime = undefined; // 清除
- }
- }, [pathname]);
-
- return null;
-}
src/app/_components/GuidePage/GuidePage.tsx
+4 -1
@@ -79,7 +79,10 @@ const GuidePage: React.FC<GuidePageProps> = (props) => {
<div id='guide-page-overlay' className={styles.guide_page} onClick={onClickNext}>
{steps &&
steps.map((step, index) => {
- return React.cloneElement(step.content as React.ReactElement, {
+ const content = step.content as React.ReactElement<
+ Record<string, unknown>
+ >;
+ return React.cloneElement(content, {
key: index,
currentStep,
currIndex: index,
src/app/_components/HeaderAuth/headerAuth.tsx
+3 -3
@@ -20,9 +20,9 @@ interface UrlObjProps {
}
const urlObj: UrlObjProps = {
- '/login/': 'LOGIN',
- '/signup/': 'SIGN UP',
- '/forgot/': 'RETRIEVE PASSWORD',
+ '/login': 'LOGIN',
+ '/signup': 'SIGN UP',
+ '/forgot': 'RETRIEVE PASSWORD',
'/redeemRecord': 'REDEEM RECORD',
'/redeemAccount': 'PRIZE REDEMPTION',
src/app/_dialogs/AccountSelectionDialog/AccountSelectionDialog.tsx
+4 -12
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect } from 'react';
-import { useRouter } from 'next/navigation';
import { Button } from '../../_components';
+
import SDialogFC, { SDialogProps } from '@/Master/SDialogFC/SDialogFC';
import { useDialogMethods } from '@/Master/SDialogFC/useDialogMethods';
import GameTrack from '@/Core/GameTrack';
@@ -11,7 +11,9 @@ import { envelopeImg } from '@/app/_dialogs/AccountSelectionDialog/imgBase64';
import { SDialogManagerFC } from '@/Master/SDialogFC/SDialogManagerFC';
import ChannelMgr from '@/Fwk/Channel/ChannelMgr';
import { getLocalUserLoginMethod } from '@/app/utils/auth';
+import RouterMgr from '@/Core/RouterMgr';
import styles from './AccountSelectionDialog.module.css';
+
import { handleGetRewardConfig } from '@/app/hooks/userRewardConfig';
import DomainMigrationViewModel from '../DomainMigrationDialog/DomainMigrationViewModel';
@@ -24,7 +26,6 @@ type AccountSelectionDialogProps = SDialogProps;
export let AccountSelectionDialogFC: React.FC<AccountSelectionDialogProps> = (props) => {
const { fcKey, onClose } = props;
- const router = useRouter();
const { closeDialog } = useDialogMethods(fcKey);
const onCloseDialog = (isLogin: boolean) => {
@@ -45,16 +46,7 @@ export let AccountSelectionDialogFC: React.FC<AccountSelectionDialogProps> = (pr
AuthTrack.showLogin(LoginScene.Welcome, getLocalUserLoginMethod() || 3);
setOpenSignUpType('page');
- // ========== 【圣诞礼包】临时方案 - 待删除 ==========
- // 保留 qid 参数到登录页(确保渠道追踪不丢失)
- const currentUrl = new URL(window.location.href);
- const qid = currentUrl.searchParams.get('qid');
- const loginPath = qid ? `/login?qid=${qid}` : '/login';
- router.push(loginPath);
- // ================= end =================
-
- // 删除上面临时方案后恢复下面代码
- // router.push('/login');
+ RouterMgr.login();
} else {
onCloseDialog(false);
GameTrack.clickStart();
src/app/_dialogs/EmailRegisteredDialog/EmailRegisteredDialog.tsx
+1 -1
@@ -53,7 +53,7 @@ export const EmailRegisteredDialogFC: React.FC<GeoTipsDialogProps> = (props) =>
// 如果是落地页情况打开落地页登录弹窗,否则走默认登录页
if (!isShowLadingGuide) {
- RouterMgr.login([['type', 'email']]);
+ RouterMgr.login(undefined, [['type', 'email']]);
} else {
SDialogManagerFC.openDialog(LoginDialog, {
fcKey: SDialogManagerFC.getFCKey(LoginDialog),
src/app/_dialogs/GameUnavailableDialog/GameUnavailableDialog.tsx
+1 -2
@@ -22,7 +22,7 @@ export const GameUnavailableDialogFC: React.FC<GameUnavailableDialogProps> = (pr
const goToHome = () => {
onClose?.();
closeDialog();
- RouterMgr.home('&from=game');
+ RouterMgr.home('game');
};
return (
@@ -40,4 +40,3 @@ export const GameUnavailableDialogFC: React.FC<GameUnavailableDialogProps> = (pr
);
};
GameUnavailableDialogFC.displayName = 'GameUnavailableDialog';
-
src/app/_dialogs/ScoreGuideDialog/ScoreGuideDialog.tsx
+66 -42
@@ -10,12 +10,12 @@ import styles from './ScoreGuideDialog.module.css';
import LogUtil from '@/Fwk/LogUtil';
export interface ScoreGuideProps extends SDialogProps {
- googlePlayUrl: string // googlePlayUrl 谷歌应用链接 market://details?id=com.example.app;
- appStoreUrl: string, // appStoreUrl 苹果应用链接 https://apps.apple.com/app/id
- game: string, // 游戏名称
- version: string, // 游戏版本
- email: string, // 邮箱
- isRemember?: boolean // 是否记住已选(true时用户选星星后不再弹出)
+ googlePlayUrl: string; // googlePlayUrl 谷歌应用链接 market://details?id=com.example.app;
+ appStoreUrl: string; // appStoreUrl 苹果应用链接 https://apps.apple.com/app/id
+ game: string; // 游戏名称
+ version: string; // 游戏版本
+ email: string; // 邮箱
+ isRemember?: boolean; // 是否记住已选(true时用户选星星后不再弹出)
}
export const ScoreGuideDialogFC: React.FC<ScoreGuideProps> = (props) => {
@@ -23,11 +23,11 @@ export const ScoreGuideDialogFC: React.FC<ScoreGuideProps> = (props) => {
const { closeDialog } = useDialogMethods(fcKey);
const [score, setScore] = useState(0);
- const [message, setMessage] = useState<JSX.Element | null>(null);
+ const [message, setMessage] = useState<React.ReactElement | null>(null);
useEffect(() => {
return () => {
- setMessage(<div>Are you enjoy the game?</div>)
+ setMessage(<div>Are you enjoy the game?</div>);
};
}, []);
@@ -39,61 +39,74 @@ export const ScoreGuideDialogFC: React.FC<ScoreGuideProps> = (props) => {
window.open(appStoreUrl);
}
SDialogManagerFC.closeDialogFC(ScoreGuideDialogFC);
- }
+ };
const onClickEmail = () => {
- window.open(`mailto:${email}?subject=Feedback_${game} ${version}`)
+ window.open(`mailto:${email}?subject=Feedback_${game} ${version}`);
SDialogManagerFC.closeDialogFC(ScoreGuideDialogFC);
- }
+ };
const selectStar = (num: number) => {
// 只有一次评分
if (score !== 0) {
- return
+ return;
}
setScore(num);
if (num === 5) {
- setMessage(<div>We will be glad if you<br/>evaluate us</div>)
+ setMessage(
+ <div>
+ We will be glad if you
+ <br />
+ evaluate us
+ </div>,
+ );
} else {
- setMessage(<div>Please help us get better!</div>)
+ setMessage(<div>Please help us get better!</div>);
}
isRemember && localStorage.setItem('hasScore', '1');
- }
+ };
const starsFC = () => {
return (
<div className={styles.stars}>
{[1, 2, 3, 4, 5].map((index) => {
return (
- <span key={index} onClick={() => {selectStar(index)}} className={styles.starContainer}>
- {index <= score ? <img src="/img/score-guide/star_1@2x.png" alt="star" /> : ''}
+ <span
+ key={index}
+ onClick={() => {
+ selectStar(index);
+ }}
+ className={styles.starContainer}
+ >
+ {index <= score ? (
+ <img src='/img/score-guide/star_1@2x.png' alt='star' />
+ ) : (
+ ''
+ )}
</span>
);
})}
</div>
);
- }
+ };
const defaultBtn = () => {
return (
<div className={styles.defaultBtn}>
<span></span>
</div>
);
- }
+ };
return (
<SDialogFC
fcKey={fcKey}
mode='Center'
contentStyle={{ minHeight: 'auto' }}
- btnStyle={{width: '7rem'}}
+ btnStyle={{ width: '7rem' }}
isBodyPad={false}
isBodyTransparent={true}
- title={
- <>
- </>
- }
- {...(score===0 && {bottom: defaultBtn()})
- }
- {...(score ===5 && { buttons: [
+ title={<></>}
+ {...(score === 0 && { bottom: defaultBtn() })}
+ {...(score === 5 && {
+ buttons: [
{
label: 'Evaluateus',
onClick: () => {
@@ -101,22 +114,26 @@ export const ScoreGuideDialogFC: React.FC<ScoreGuideProps> = (props) => {
},
style: 'warnBtn',
},
- ]})}
- {...(score !==5 && score !==0 && { buttons: [
- {
- label: 'Feedback',
- onClick: () => {
- onClickEmail();
+ ],
+ })}
+ {...(score !== 5 &&
+ score !== 0 && {
+ buttons: [
+ {
+ label: 'Feedback',
+ onClick: () => {
+ onClickEmail();
+ },
+ style: 'blueBtn',
},
- style: 'blueBtn',
- },
- ]})}
+ ],
+ })}
>
<div>
{starsFC()}
<div className={styles.message}>{message}</div>
</div>
- <img src="/img/score-guide/img_sz@2x.png" alt="" className={styles.slideInOut}/>
+ <img src='/img/score-guide/img_sz@2x.png' alt='' className={styles.slideInOut} />
</SDialogFC>
);
};
@@ -126,11 +143,18 @@ export interface scoreProps {
isRefresh: boolean;
}
-export async function showScoreGuideDialog({googlePlayUrl, appStoreUrl, game, version, email, isRemember}: any) {
+export async function showScoreGuideDialog({
+ googlePlayUrl,
+ appStoreUrl,
+ game,
+ version,
+ email,
+ isRemember,
+}: any) {
const hasScore = localStorage.getItem('hasScore'); // 是否已经评分
- if ( isRemember && hasScore === '1') {
+ if (isRemember && hasScore === '1') {
LogUtil.warn('已经评分了');
- return
+ return;
}
return new Promise<scoreProps>((resolve, reject) => {
SDialogManagerFC.openDialog(ScoreGuideDialogFC, {
@@ -140,8 +164,8 @@ export async function showScoreGuideDialog({googlePlayUrl, appStoreUrl, game, ve
appStoreUrl: appStoreUrl, // 苹果应用链接 https://apps.apple.com/app/id
game: game, // 游戏名
version: version, // 游戏版本
- email: email,// email邮箱
- isRemember: isRemember
+ email: email, // email邮箱
+ isRemember: isRemember,
});
});
}
src/app/_dialogs/VipUnlockDialog/VipUnlockDialog.tsx
+4 -6
@@ -1,6 +1,6 @@
'use client';
import React from 'react';
-import { useRouter } from 'next/navigation';
+import RouterMgr from '@/Core/RouterMgr';
import { selectAccountInfo, SET_INITIAL_SLIDER_INDEX } from '@/app/(pages)/account/accountSlice';
import { useAppDispatch, useAppSelector } from '@/app/storeHooks';
@@ -24,8 +24,6 @@ export const VipUnlockDialogFC: React.FC<VipUnlockDialogProps> = (props) => {
const { closeDialog } = useDialogMethods(fcKey);
- const router = useRouter();
-
const account = useAppSelector(selectAccountInfo);
const dispatch = useAppDispatch();
@@ -47,7 +45,7 @@ export const VipUnlockDialogFC: React.FC<VipUnlockDialogProps> = (props) => {
onClick: () => {
TrackCommon.click_upgrade();
onCloseCall();
- router.push('/store/?from=upgrade');
+ RouterMgr.push('/store/?from=upgrade');
// dispatch(SET_OPEN_SHOP_TYPE('upgrade'));
},
style: 'greenBtn',
@@ -79,7 +77,7 @@ export const VipUnlockDialogFC: React.FC<VipUnlockDialogProps> = (props) => {
(item: any) => item.levelName === vipLevelName,
);
dispatch(SET_INITIAL_SLIDER_INDEX(index));
- router.push('/account/member');
+ RouterMgr.push('/account/member');
}}
>
<div className={styles.tips_icon}>
@@ -124,7 +122,7 @@ export const VipUnlockDialogFC: React.FC<VipUnlockDialogProps> = (props) => {
onClick={() => {
TrackCommon.click_upgrade();
onCloseCall();
- router.push('/store/?from=upgrade');
+ RouterMgr.push('/store/?from=upgrade');
}}
>
Upgrade to VIP
src/app/config/index.ts
+13 -13
@@ -2,7 +2,13 @@ import { AUTH_PAGES } from '@/app/utils/auth';
export const APP_AUTH_PAGES: readonly string[] = ['/login', '/install', '/game', '/forgot'];
-export const whiteList: readonly string[] = ['/login/', '/install/', '/signup/', '/forgot/', '/landing/'];
+export const whiteList: readonly string[] = [
+ '/login',
+ '/install',
+ '/signup',
+ '/forgot',
+ '/landing'
+];
export type FooterPage = {
name: string;
@@ -56,10 +62,10 @@ export const FOOTER_PAGES: readonly FooterPage[] = [
// }
];
export const NOT_AUTH_FOOTER_PAGES: readonly string[] = [
- '/game/',
- '/game2/',
- '/activity/vip-carnival-day/',
- '/activity/coinscashback/'
+ '/game',
+ '/game2',
+ '/activity/vip-carnival-day',
+ '/activity/coinscashback'
];
export const getAuthPages = () => {
@@ -68,17 +74,11 @@ export const getAuthPages = () => {
};
export const isActiveTab = (targetRoute: string, pathname: string) => {
- const currentPath = pathname;
-
if (targetRoute === '/') {
- return currentPath === targetRoute; // 主页精确匹配
- }
-
- if (targetRoute + '/' === currentPath || currentPath.split('/').includes(targetRoute)) {
- return true;
+ return pathname === '/'; // 主页精确匹配
}
- return currentPath === targetRoute; // 其他路径匹配
+ return pathname === targetRoute || pathname.split('/').includes(targetRoute);
};
/**
src/app/context/UserContext.tsx
+3 -3
@@ -104,7 +104,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
// 有token的情况下,白名页面重定向到首页
// 排除 /landing 路径,允许已登录和未登录用户都能访问落地页
if (whiteList.includes(path) && !isLandingPath(path)) {
- router.push('/');
+ RouterMgr.push('/');
// 处理Unity游戏数据并获取用户信息
UnityGameDataManager.HandleUnityGameData(() => {
dispatch(getUserInfoAsync());
@@ -152,7 +152,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
const isOAuthCallback = searchParams?.get('code');
const currentIsLandingPath = isLandingPath();
if (getLocalLogout() && !isShowLadingGuide && !isOAuthCallback && !currentIsLandingPath) {
- router.push('/login?from=signOut');
+ RouterMgr.login('signOut');
return;
}
@@ -355,7 +355,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
userInfo.firstRequestUserInfo
) {
if (userInfo.deleteAccountStatus === 2) {
- RouterMgr.login([['form', 'signOut']]);
+ RouterMgr.login('signOut');
return null;
}
return (
src/app/hooks/useInitRedDot.ts
+1 -1
@@ -83,7 +83,7 @@ export const useInitRedDot = (props?: FetchAndSyncRedDotProps) => {
}
// 监听pathname变化,如果不在白名单中,重新拉取红点
- const refreshPath = ['/', '/rewards', '/rewards/'];
+ const refreshPath = ['/', '/rewards'];
useEffect(() => {
if (!refreshPath.includes(pathName) || !watchVisibility) {
return;
src/app/hooks/useSocialLogin.ts
+79 -51
@@ -1,10 +1,12 @@
-
import { useSafeState } from 'ahooks';
import { useDispatch } from 'react-redux';
import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';
import { userLoginAsync } from '@/app/userSliceAPI';
-import { showForceLoading, hideForceLoading } from '@/app/_dialogs/LoadingForceDialog/LoadingForceDialogFC';
+import {
+ showForceLoading,
+ hideForceLoading
+} from '@/app/_dialogs/LoadingForceDialog/LoadingForceDialogFC';
import { Toast } from '@/app/_components';
import { getUserId, getLastLoginUserId } from '@/app/utils/auth';
import $http from '@/app/_api';
@@ -16,13 +18,13 @@ import AuthTrack, { SocialProviderType } from '@/Core/AuthTrack';
/**
* 统一的社交登录Hook - 支持Google、Facebook和Apple
- *
+ *
* ============================================
* 核心流程图
* ============================================
- *
+ *
* 【登录/绑定流程】
- *
+ *
* 用户点击按钮
* ↓
* handleSocialLogin (主入口)
@@ -61,13 +63,15 @@ import AuthTrack, { SocialProviderType } from '@/Core/AuthTrack';
* │ └─ dispatch(userLoginAsync)
* │
* └─ 清理: 状态 + URL 参数
- *
+ *
*/
// 日志工具
const logger = {
- info: (msg: string, ...args: any[]) => Util.isLog && LogUtil.info('[SocialLogin]', msg, ...args),
- error: (msg: string, ...args: any[]) => Util.isLog && LogUtil.error('[SocialLogin]', msg, ...args)
+ info: (msg: string, ...args: any[]) =>
+ Util.isLog && LogUtil.info('[SocialLogin]', msg, ...args),
+ error: (msg: string, ...args: any[]) =>
+ Util.isLog && LogUtil.error('[SocialLogin]', msg, ...args)
};
// Apple Sign In 类型声明
@@ -89,27 +93,31 @@ const getProviderType = (provider: SocialProvider) => SocialProviderType[provide
const STORAGE_KEYS = {
BINDING_MODE: 'social_binding_mode',
PROVIDER: 'social_provider',
- PAGE_SOURCE: 'social_page_source',
+ PAGE_SOURCE: 'social_page_source'
} as const;
const storage = {
set: (key: string, value: string) => localStorage.setItem(key, value),
get: (key: string) => localStorage.getItem(key),
- remove: (key: string) => localStorage.removeItem(key),
+ remove: (key: string) => localStorage.removeItem(key)
};
-const setBindingMode = (isBinding: boolean) =>
- isBinding ? storage.set(STORAGE_KEYS.BINDING_MODE, '1') : storage.remove(STORAGE_KEYS.BINDING_MODE);
+const setBindingMode = (isBinding: boolean) =>
+ isBinding
+ ? storage.set(STORAGE_KEYS.BINDING_MODE, '1')
+ : storage.remove(STORAGE_KEYS.BINDING_MODE);
const getBindingMode = () => storage.get(STORAGE_KEYS.BINDING_MODE) === '1';
-const setProviderStorage = (provider: SocialProvider | null) =>
+const setProviderStorage = (provider: SocialProvider | null) =>
provider ? storage.set(STORAGE_KEYS.PROVIDER, provider) : storage.remove(STORAGE_KEYS.PROVIDER);
const getProviderStorage = () => storage.get(STORAGE_KEYS.PROVIDER) as SocialProvider | null;
-const setPageSourceStorage = (source: 'login' | 'signup' | null) =>
- source ? storage.set(STORAGE_KEYS.PAGE_SOURCE, source) : storage.remove(STORAGE_KEYS.PAGE_SOURCE);
+const setPageSourceStorage = (source: 'login' | 'signup' | null) =>
+ source
+ ? storage.set(STORAGE_KEYS.PAGE_SOURCE, source)
+ : storage.remove(STORAGE_KEYS.PAGE_SOURCE);
// 防重复提交
const usedOauthCodes = new Set<string>();
@@ -170,21 +178,24 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
// 延迟检查时重新读取最新状态
const url = new URL(window.location.href);
const provider = getProviderStorage();
- const hasOAuthParams = url.searchParams.has('code') || url.searchParams.has('error');
-
+ const hasOAuthParams =
+ url.searchParams.has('code') || url.searchParams.has('error');
+
// 检查条件:有 provider、没有 OAuth 参数、不在处理中(Apple 是弹窗不会有后退返回)
if (provider && !hasOAuthParams && !isProcessingAuth) {
AuthTrack.thirdPartyAuth(getProviderType(provider));
clearLoginState(true);
}
}, 2000); // 延迟 2 秒,确保正常回调有足够时间处理
-
+
return timer;
};
- const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
+ const navEntry = performance.getEntriesByType(
+ 'navigation'
+ )[0] as PerformanceNavigationTiming;
const timers: NodeJS.Timeout[] = [];
-
+
const handlePageShow = (event: PageTransitionEvent) => {
// persisted 表示从缓存恢复(通常是后退)
if (event.persisted) {
@@ -193,7 +204,7 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
};
window.addEventListener('pageshow', handlePageShow);
-
+
// 初始加载时如果是后退导航,也检查
if (navEntry?.type === 'back_forward') {
timers.push(checkBackNavigation());
@@ -201,7 +212,7 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
return () => {
window.removeEventListener('pageshow', handlePageShow);
- timers.forEach(timer => clearTimeout(timer));
+ timers.forEach((timer) => clearTimeout(timer));
};
}, [pathname, isProcessingAuth]);
@@ -233,7 +244,9 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
const registerUserDTO: Record<string, any> = {
...(currentUserId && { userId: currentUserId }),
...(lastUserId && { lastUserId }),
- ...(ChannelMgr.openMorey !== undefined && { isFromBcUser: ChannelMgr.openMorey ? 1 : 2 }),
+ ...(ChannelMgr.openMorey !== undefined && {
+ isFromBcUser: ChannelMgr.openMorey ? 1 : 2
+ }),
...(Util.GetTimeZone && { timeZone: Util.GetTimeZone })
};
@@ -259,32 +272,43 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
// 获取 provider 显示名称
const getProviderName = (isAppleLogin: boolean) => {
if (isAppleLogin) return 'Apple';
- const providerNames: Record<string, string> = { google: 'Google', facebook: 'Facebook', apple: 'Apple' };
+ const providerNames: Record<string, string> = {
+ google: 'Google',
+ facebook: 'Facebook',
+ apple: 'Apple'
+ };
return providerNames[getProviderStorage() || ''] || 'Social media';
};
// 处理登录
- const handleLogin = async (code: string, state?: string, appleUserInfo?: any, isAppleLogin?: boolean) => {
+ const handleLogin = async (
+ code: string,
+ state?: string,
+ appleUserInfo?: any,
+ isAppleLogin?: boolean
+ ) => {
const loginParams = isAppleLogin
? { ...buildLoginParams(code, undefined, appleUserInfo), isAppleLogin: true }
: buildLoginParams(code, state, appleUserInfo);
-
+
const result = await dispatch(userLoginAsync(loginParams) as any);
- return result.meta.requestStatus === 'fulfilled' // "rejected" || "fulfilled"
+ return result.meta.requestStatus === 'fulfilled'; // "rejected" || "fulfilled"
};
// 处理绑定成功
const handleBindSuccess = (isAppleLogin: boolean, provider?: SocialProvider) => {
Toast.show(`${getProviderName(isAppleLogin)} account bound successfully`);
-
+
if (provider) {
AuthTrack.thirdPartyAuthSuc(getProviderType(provider));
}
-
+
clearLoginState();
-
+
if (!isAppleLogin) {
- window.location.href = '/account/personal-info' + ChannelMgr.getQid;
+ // window.location.href 是全刷新,不走 RouterMgr Proxy,需要手动带 qid
+ const qid = ChannelMgr.channelQid;
+ window.location.href = '/account/personal-info' + (qid ? `?qid=${qid}` : '');
} else {
setTimeout(() => (window as any).__socialLoginRefresh?.(), 500);
}
@@ -298,14 +322,14 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
appleUserInfo?: any;
isAppleLogin?: boolean;
}
-
+
const processAuth = async (params: ProcessAuthParams): Promise<void> => {
const { code, error, state, appleUserInfo, isAppleLogin } = params;
-
+
// 防重复
if (isProcessingAuth) return;
if (code && usedOauthCodes.has(code)) return;
-
+
// 优先处理错误(用户取消授权),避免被后续的 code 检查拦截
const currentProvider = getProviderStorage();
if (error) {
@@ -318,7 +342,7 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
// 没有 code 也没有 error,直接返回
if (!code) return;
-
+
if (code) usedOauthCodes.add(code);
setIsProcessingAuth(true);
@@ -326,12 +350,12 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
showForceLoading();
const isBinding = getBindingMode();
-
+
// 触发授权成功埋点(OAuth 跳转回来时)
if (currentProvider && !isAppleLogin) {
AuthTrack.thirdPartyAuth(getProviderType(currentProvider));
}
-
+
if (isBinding) {
await handleBindingFlow();
} else {
@@ -340,16 +364,16 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
clearLoginState();
hideForceLoading();
-
+
// 绑定流程
async function handleBindingFlow() {
if (!code) return;
-
+
const bindParams = buildBindParams(code, state, appleUserInfo);
- const bindApi = isAppleLogin
+ const bindApi = isAppleLogin
? $http.Auth.appleBind(bindParams)
: $http.Auth.thirdPartySocialMediaBind(bindParams);
-
+
const response = await bindApi;
if (response.code === 200 && response.data) {
@@ -358,7 +382,10 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
Toast.show(response.message || 'Binding failed');
clearLoginState();
if (!isAppleLogin) {
- window.location.href = '/account/personal-info' + ChannelMgr.getQid;
+ // window.location.href 是全刷新,不走 RouterMgr Proxy,需要手动带 qid
+ const qid = ChannelMgr.channelQid;
+ window.location.href =
+ '/account/personal-info' + (qid ? `?qid=${qid}` : '');
}
}
}
@@ -377,17 +404,17 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
provider: SocialProvider;
isBindingMode?: boolean;
}
-
+
const handleSocialLogin = async (params: HandleSocialLoginParams) => {
const { provider, isBindingMode } = params;
-
+
if (isBindingMode) {
AuthTrack.thirdPartyBind(getProviderType(provider));
}
try {
setupAuthState();
-
+
if (provider === 'apple') {
await handleAppleAuth();
} else {
@@ -403,17 +430,18 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
}
clearLoginState(true);
}
-
+
// 设置认证状态
function setupAuthState() {
- const pageSource = options?.isSignupContext || pathname?.includes('/signup') ? 'signup' : 'login';
-
+ const pageSource =
+ options?.isSignupContext || pathname?.includes('/signup') ? 'signup' : 'login';
+
setBindingMode(!!isBindingMode);
setProviderStorage(provider);
setPageSourceStorage(pageSource);
localStorage.setItem(`social_${provider}`, '1');
}
-
+
// 处理 Apple 授权
async function handleAppleAuth() {
await initAppleSDK(appleInitialized.current);
@@ -440,10 +468,10 @@ export const useSocialLogin = (options?: { isSignupContext?: boolean }) => {
await processAuth({
code: result.authorization.code,
appleUserInfo: result.user,
- isAppleLogin: true,
+ isAppleLogin: true
});
}
-
+
// 处理 OAuth 跳转(Google/Facebook)
async function handleOAuthRedirect() {
setIsLoading(false);
src/app/layout.tsx
+2 -1
@@ -80,7 +80,8 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
{/*仅用于首屏关键资源或不可延迟的脚本。*/}
{/*<link rel="preload" href="app.js" as="script"/>*/}
- <GoogleTagManager gtmId={confJson.GTM_ID} />
+ {/* 临时屏蔽 GTM,验证是否影响 Tab 跳转 */}
+ {/* <GoogleTagManager gtmId={confJson.GTM_ID} /> */}
{/*提前加载geo定位的基础库 - 延迟加载*/}
<script
src/app/sitemap.ts
+7 -7
@@ -14,31 +14,31 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 1.0
},
{
- url: '/login/',
+ url: '/login',
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.8
},
{
- url: '/signup/',
+ url: '/signup',
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.8
},
{
- url: '/store/',
+ url: '/store',
lastModified: currentDate,
changeFrequency: 'daily',
priority: 0.9
},
{
- url: '/rewards/',
+ url: '/rewards',
lastModified: currentDate,
changeFrequency: 'daily',
priority: 0.9
},
{
- url: '/redeem/',
+ url: '/redeem',
lastModified: currentDate,
changeFrequency: 'daily',
priority: 0.9
@@ -51,13 +51,13 @@ export default function sitemap(): MetadataRoute.Sitemap {
// priority: 0.8,
// },
{
- url: '/affiliate/',
+ url: '/affiliate',
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.7
},
{
- url: '/install/',
+ url: '/install',
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.6
src/app/userSliceAPI.ts
+2 -2
@@ -586,7 +586,7 @@ export const userSignOutAsync = createAsyncThunk('user/signOut', async (params:
} else {
// 跳转到登录页
hideForceLoading();
- RouterMgr.login([['form', 'signOut']]);
+ RouterMgr.login('signOut');
}
useRedDotViewModel.getState().reset();
ThunkApi.dispatch(SET_PAY_TIMES(0));
@@ -626,7 +626,7 @@ export const deleteAccountAsync = createAsyncThunk(
RouterMgr.home();
} else {
// 跳转到登录页
- RouterMgr.login([['form', 'signOut']]);
+ RouterMgr.login('signOut');
}
useRedDotViewModel.getState().reset();
ThunkApi.dispatch(SET_PAY_TIMES(0));
src/app/utils/native-props.ts
+5 -2
@@ -8,8 +8,11 @@ export type NativeProps<S extends string = never> = {
tabIndex?: number;
} & AriaAttributes;
-export function withNativeProps<P extends NativeProps>(props: P, element: ReactElement) {
- const p = {
+export function withNativeProps<P extends NativeProps>(
+ props: P,
+ element: ReactElement<Record<string, any>>
+) {
+ const p: Record<string, any> = {
...element.props
};
if (props.className) {
src/app/utils/with-stop-propagation.tsx
+4 -1
@@ -7,7 +7,10 @@ const eventToPropRecord: Record<PropagationEvent, string> = {
touchstart: 'onTouchStart',
};
-export function withStopPropagation(events: PropagationEvent[], element: ReactElement) {
+export function withStopPropagation(
+ events: PropagationEvent[],
+ element: ReactElement<Record<string, any>>,
+) {
const props: Record<string, any> = { ...element.props };
for (const key of events) {
const prop = eventToPropRecord[key];
tsconfig.json
+3 -3
@@ -11,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
+ "exclude": ["node_modules", "midscene-bridge"]
}