BaiLuoYan 1 månad sedan
förälder
incheckning
a5715e46bb

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@ node_modules
 dist
 dist-ssr
 *.local
+.pnpm-store
 
 # Editor directories and files
 !.vscode/extensions.json

+ 6 - 2
.stylelintrc.mjs

@@ -17,7 +17,7 @@ export default {
         'selector-pseudo-element-no-unknown': [
             true,
             {
-                ignorePseudoElements: ['v-deep'],
+                ignorePseudoElements: ['v-deep', 'v-slotted', 'v-global'],
             },
         ],
         'at-rule-no-unknown': null,
@@ -71,7 +71,7 @@ export default {
         ],
         'color-function-notation': null,
         'alpha-value-notation': null,
-        'scss/dollar-variable-colon-space-after': 'always-single-line',
+        'scss/dollar-variable-colon-space-after': null,
         'scss/dollar-variable-empty-line-before': null,
         'less/color-no-invalid-hex': null,
     },
@@ -90,5 +90,9 @@ export default {
                 'at-rule-no-unknown': null,
             },
         },
+        {
+            files: ['*.vue', '**/*.vue'],
+            customSyntax: 'postcss-html',
+        },
     ],
 };

+ 11 - 15
eslint.config.js

@@ -1,15 +1,15 @@
 import js from '@eslint/js';
-import globals from 'globals';
+import prettierConfig from 'eslint-config-prettier/flat';
+import importX from 'eslint-plugin-import-x';
+import jsxA11y from 'eslint-plugin-jsx-a11y';
+import react from 'eslint-plugin-react';
 import reactHooks from 'eslint-plugin-react-hooks';
 import reactRefresh from 'eslint-plugin-react-refresh';
+import globals from 'globals';
 import tseslint from 'typescript-eslint';
-import react from 'eslint-plugin-react';
-import importPlugin from 'eslint-plugin-import';
-import jsxA11y from 'eslint-plugin-jsx-a11y';
-import prettierConfig from 'eslint-config-prettier/flat';
 
 export default [
-    { ignores: ['dist/**/*', 'node_modules/**/*', 'public/**/*'] },
+    { ignores: ['dist/**/*', 'node_modules/**/*', 'public/**/*', '*.config.js', '*.config.mjs', '.*.mjs'] },
     js.configs.recommended,
     ...tseslint.configs.recommended,
     {
@@ -30,14 +30,14 @@ export default [
                 },
                 sourceType: 'module',
                 project: ['./tsconfig.json'],
-                tsconfigRootDir: '.',
+                tsconfigRootDir: import.meta.dirname,
             },
         },
         plugins: {
             'react-hooks': reactHooks,
             'react-refresh': reactRefresh,
             react: react,
-            import: importPlugin,
+            'import-x': importX,
             'jsx-a11y': jsxA11y,
             '@typescript-eslint': tseslint.plugin,
         },
@@ -49,14 +49,14 @@ export default [
             '@typescript-eslint/explicit-module-boundary-types': 'off',
             '@typescript-eslint/no-explicit-any': 'off',
             '@typescript-eslint/no-unused-vars': [
-                'warn',
+                'error',
                 {
                     argsIgnorePattern: '^_',
                     varsIgnorePattern: '^_',
                     ignoreRestSiblings: true,
                 },
             ],
-            'import/order': [
+            'import-x/order': [
                 'error',
                 {
                     groups: [
@@ -90,8 +90,6 @@ export default [
                     distinctGroup: true,
                 },
             ],
-            // 移除 indent 规则,让 Prettier 处理缩进
-            // indent 规则与 Prettier 冲突,已由 eslint-config-prettier 禁用
             'no-unused-vars': 'off',
             'no-redeclare': 'off',
             'prefer-const': 'error',
@@ -100,14 +98,12 @@ export default [
             react: {
                 version: 'detect',
             },
-            'import/resolver': {
+            'import-x/resolver': {
                 typescript: {
                     project: ['./tsconfig.json'],
                 },
             },
         },
     },
-    // 禁用所有与 Prettier 冲突的 ESLint 规则
-    // eslint-config-prettier 必须在配置数组的最后,以覆盖其他配置
     prettierConfig,
 ];

+ 13 - 12
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "visa-card-h5",
+  "name": "nomo-home-web",
   "private": true,
   "version": "0.0.1",
   "type": "module",
@@ -18,10 +18,10 @@
     "build:prod": "tsc && vite build --mode production",
     "build:test": "tsc && vite build --mode test",
     "preview": "vite preview --mode localdev",
-    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
-    "lint:fix": "eslint . --ext ts,tsx --fix",
-    "stylelint": "stylelint \"src/**/*.{css,less,scss}\"",
-    "stylelint:fix": "stylelint \"src/**/*.{css,less,scss}\" --fix",
+    "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
+    "lint:fix": "eslint . --fix",
+    "stylelint": "stylelint \"src/**/*.{css,less,scss,vue}\"",
+    "stylelint:fix": "stylelint \"src/**/*.{css,less,scss,vue}\" --fix",
     "prepare": "husky install"
   },
   "dependencies": {
@@ -71,8 +71,6 @@
     "@types/ramda": "^0.30.2",
     "@types/react": "^18.3.28",
     "@types/react-dom": "^18.3.7",
-    "@typescript-eslint/eslint-plugin": "^8.27.0",
-    "@typescript-eslint/parser": "^8.27.0",
     "@vitejs/plugin-legacy": "6.1.1",
     "@vitejs/plugin-react": "^4.3.4",
     "autoprefixer": "10.4.17",
@@ -80,8 +78,9 @@
     "consola": "^3.4.2",
     "cross-env": "^7.0.3",
     "eslint": "^9.22.0",
+    "eslint-config-prettier": "10.1.8",
     "eslint-import-resolver-typescript": "^4.2.2",
-    "eslint-plugin-import": "^2.31.0",
+    "eslint-plugin-import-x": "4.16.2",
     "eslint-plugin-jsx-a11y": "^6.10.2",
     "eslint-plugin-react": "^7.37.4",
     "eslint-plugin-react-hooks": "^5.2.0",
@@ -92,6 +91,8 @@
     "less": "^4.2.2",
     "lint-staged": "^15.5.0",
     "postcss": "8.4.35",
+    "postcss-html": "1.8.1",
+    "postcss-less": "6.0.0",
     "postcss-nesting": "^13.0.1",
     "postcss-scss": "^4.0.9",
     "prettier": "^3.5.3",
@@ -100,11 +101,10 @@
     "stylelint-config-standard": "^30.0.0",
     "stylelint-config-standard-scss": "^7.0.1",
     "stylelint-order": "^6.0.4",
-    "stylelint-prettier": "^2.0.0",
     "svgo": "^3.3.2",
     "tailwindcss": "3.4.1",
     "typescript": "^5.7.3",
-    "typescript-eslint": "^8.24.1",
+    "typescript-eslint": "^8.58.1",
     "vite": "^6.2.2",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-remove-console": "^2.2.0",
@@ -122,7 +122,7 @@
       "eslint --fix",
       "prettier --write"
     ],
-    "src/**/*.{css,scss,less}": [
+    "src/**/*.{css,scss,less,vue}": [
       "stylelint --fix",
       "prettier --write"
     ]
@@ -134,7 +134,8 @@
       "@swc/core",
       "core-js",
       "esbuild",
-      "protobufjs"
+      "protobufjs",
+      "unrs-resolver"
     ]
   }
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 461 - 110
pnpm-lock.yaml


+ 1 - 0
src/App.tsx

@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
 import { RouterProvider } from 'react-router-dom';
 
 import { AntdAppInstanceCapture } from '@/utils/antdAppInstance';
+
 import { DialogContainer } from './components/Dialog/DialogContainer';
 import router from './router';
 import models from './utils/model/autoImportModels';

+ 1 - 1
src/components/Dialog/index.tsx

@@ -2,10 +2,10 @@ import { Fragment, memo } from 'react';
 
 import { Icon, IconifyIcon } from '@iconify/react';
 
+import closeIcon from '@/assets/iconify/single-color/close.svg';
 import { useResponsive } from '@/hooks/useSize';
 import { dialogModel } from '@/models/dialogModel';
 
-import closeIcon from '@/assets/iconify/single-color/close.svg';
 
 import { useAction } from './useAction';
 

+ 2 - 2
src/components/Topbar/useService.ts

@@ -10,7 +10,7 @@ import { getNavMenuItems, type NavMenuItem } from '@/utils/navUtils';
  * 处理菜单项获取、路由匹配、翻译等业务逻辑
  */
 export function useService() {
-    const { t, i18n } = useTranslation();
+    const { t } = useTranslation();
     const location = useLocation();
 
     const menuItems = useMemo(() => getNavMenuItems(), []);
@@ -27,7 +27,7 @@ export function useService() {
             // getNavMenuItems 中已经设置了 locale,item.locale 总是存在
             return t(item.locale!, { defaultValue: item.name });
         },
-        [t, i18n.language]
+        [t]
     );
 
     return {

+ 3 - 3
src/config/request/encryptionInterceptors.ts

@@ -3,15 +3,15 @@ import isNil from 'ramda/es/isNil';
 import globalConfig from '@/config';
 import { CompressMethod } from '@/config/types';
 // import { bytesBase64decode } from '@/utils/crypto';
-import { decryptResponsePayload, encryptRequestPayload } from '@/utils/requestCrypto';
 import { bytesToString, stringToBytes } from '@/utils/bytesUtils';
-import { currentUnixTimestamp } from '@/utils/timeUtils';
+import { bytesBase64encode } from '@/utils/crypto';
 import {
     IRequestInterceptorAxios,
     IResponseInterceptor,
     RequestConfig,
 } from '@/utils/request/types';
-import { bytesBase64encode } from '@/utils/crypto';
+import { decryptResponsePayload, encryptRequestPayload } from '@/utils/requestCrypto';
+import { currentUnixTimestamp } from '@/utils/timeUtils';
 
 /**
  * 请求数据加密拦截器

+ 1 - 1
src/config/request/requestErrorConfig.ts

@@ -2,10 +2,10 @@ import axios from 'axios';
 
 import { ErrorShowType } from '@/defines';
 import { reportError } from '@/firebase';
+import { message, notification } from '@/utils/antdAppInstance';
 import { RequestConfig } from '@/utils/request/types';
 // import { toLoginPage } from '@/utils/routerUtils';
 
-import { message, notification } from '@/utils/antdAppInstance';
 
 /**
  * @name 错误处理

+ 2 - 1
src/models/dialogModel.ts

@@ -1,8 +1,9 @@
 import { useState, useCallback } from 'react';
 
+import type { DialogProps } from '@/components/Dialog';
+
 import { createModel } from '../utils/model/createModel';
 
-import type { DialogProps } from '@/components/Dialog';
 
 interface DialogItem extends Omit<DialogProps, 'open' | 'id' | 'zIndex'> {
     id: string;

+ 1 - 1
src/pages/pricing/components/OrderSummary/PayQrContent.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useRef } from 'react';
 
-import { useTranslation } from 'react-i18next';
 import { QRCodeSVG } from 'qrcode.react';
+import { useTranslation } from 'react-i18next';
 
 import { PayOrderStatus } from '@/defines';
 import { fetchPayOrderStatus } from '@/services/config';

+ 3 - 1
src/pages/pricing/components/OrderSummary/index.tsx

@@ -6,9 +6,11 @@ import { useTranslation } from 'react-i18next';
 import { useResponsive } from '@/hooks/useSize';
 
 import LabelValueItem from '../LabelValueItem';
-import type { Plan } from '../../useService';
+
 import { useService } from './useService';
 
+import type { Plan } from '../../useService';
+
 const PRICING_FORM_ID = 'pricing-form';
 
 export interface OrderSummaryProps {

+ 2 - 2
src/pages/pricing/components/PayMethodCard/index.tsx

@@ -2,9 +2,9 @@ import { memo } from 'react';
 
 import { Icon } from '@iconify/react';
 
-import { useResponsive } from '@/hooks/useSize';
-import checkOnIcon from '@/assets/iconify/multi-color/check-on.svg';
 import checkOffIcon from '@/assets/iconify/multi-color/check-off.svg';
+import checkOnIcon from '@/assets/iconify/multi-color/check-on.svg';
+import { useResponsive } from '@/hooks/useSize';
 
 export interface PayMethodCardProps {
     item: API.PayTypeItem;

+ 2 - 2
src/pages/pricing/components/PlanCard/index.tsx

@@ -3,11 +3,11 @@ import { memo } from 'react';
 import { Icon } from '@iconify/react';
 import { useTranslation } from 'react-i18next';
 
-import { useResponsive } from '@/hooks/useSize';
 
-import checkOnIcon from '@/assets/iconify/multi-color/check-on.svg';
 import checkOffIcon from '@/assets/iconify/multi-color/check-off.svg';
+import checkOnIcon from '@/assets/iconify/multi-color/check-on.svg';
 import { PlanTagType } from '@/defines';
+import { useResponsive } from '@/hooks/useSize';
 
 const PLAN_TAG_TYPE_I18N_KEY: Record<PlanTagType, string> = {
     [PlanTagType.NONE]: '',

+ 1 - 0
src/pages/pricing/components/UserInfo/index.tsx

@@ -7,6 +7,7 @@ import { maskAccount } from '@/utils/stringUtils';
 import { unixTimeFormat } from '@/utils/timeUtils';
 
 import LabelValueItem from '../LabelValueItem';
+
 import { useService } from './useService';
 
 const UserInfo = memo(() => {

+ 1 - 0
src/pages/pricing/index.tsx

@@ -11,6 +11,7 @@ import PlanCard from './components/PlanCard';
 import UserInfo from './components/UserInfo';
 import { useAction } from './useAction';
 import { useService } from './useService';
+
 import type { Plan } from './useService';
 
 const PRICING_FORM_ID = 'pricing-form';

+ 2 - 2
src/pages/redirect/index.tsx

@@ -2,9 +2,9 @@ import React, { useEffect } from 'react';
 
 import { useNavigate, useSearchParams } from 'react-router-dom';
 
+import { userConfigModel } from '@/models/userConfigModel';
 import { removeToken, setToken } from '@/utils/authUtils';
 import { decryptUrlParams } from '@/utils/requestCrypto';
-import { userConfigModel } from '@/models/userConfigModel';
 
 /**
  * 解密重定向参数
@@ -66,7 +66,7 @@ const Redirect: React.FC = () => {
         };
 
         handleRedirect();
-    }, [navigate, searchParams]);
+    }, [navigate, searchParams, setUserConfig]);
 
     // 空白页面,不显示任何内容
     return null;

+ 37 - 0
src/styles/global.scss

@@ -1,3 +1,38 @@
+/* REM — variable font, covers weight 100-900 */
+@font-face {
+    font-family: REM;
+    font-style: normal;
+    font-weight: 100 900;
+    font-display: swap;
+    src: url('@/assets/fonts/rem-latin-normal.woff2') format('woff2');
+    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
+        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+        U+FEFF, U+FFFD;
+}
+
+@font-face {
+    font-family: REM;
+    font-style: italic;
+    font-weight: 100 900;
+    font-display: swap;
+    src: url('@/assets/fonts/rem-latin-italic.woff2') format('woff2');
+    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
+        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+        U+FEFF, U+FFFD;
+}
+
+/* Barlow — latin subset, regular 400 */
+@font-face {
+    font-family: Barlow;
+    font-style: normal;
+    font-weight: 400;
+    font-display: swap;
+    src: url('@/assets/fonts/barlow-latin-normal.woff2') format('woff2');
+    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
+        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+        U+FEFF, U+FFFD;
+}
+
 *,
 ::before,
 ::after {
@@ -14,6 +49,7 @@ html {
     line-height: 1.5;
     tab-size: 4;
     text-size-adjust: 100%;
+    background-color: black;
 }
 
 body {
@@ -377,3 +413,4 @@ div:focus {
         direction: rtl;
     }
 }
+

+ 2 - 0
src/utils/antdAppInstance.tsx

@@ -3,6 +3,7 @@ import {
     message as defaultMessage,
     notification as defaultNotification,
 } from 'antd';
+
 import type { MessageInstance } from 'antd/es/message/interface';
 import type { NotificationInstance } from 'antd/es/notification/interface';
 
@@ -14,6 +15,7 @@ import type { NotificationInstance } from 'antd/es/notification/interface';
 let message: MessageInstance = defaultMessage;
 let notification: NotificationInstance = defaultNotification;
 
+// eslint-disable-next-line react-refresh/only-export-components
 export { message, notification };
 
 /**

+ 9 - 1
src/utils/authUtils.ts

@@ -3,7 +3,7 @@ import Cookies from 'js-cookie';
 import { secureLocalStorage as ls } from '@/utils/localUtils';
 import { encryptKey, encryptData, decryptData } from '@/utils/storage';
 
-import { currentJsTimestamp } from './timeUtils';
+import { currentJsTimestamp, currentUnixTimestamp } from './timeUtils';
 
 export const userKey = 'user-info';
 export const tokenKey = 'authorized-token';
@@ -59,6 +59,14 @@ export function removeToken() {
     ls.removeLocal(userKey);
 }
 
+/** 实时判断当前是否已认证(accessToken 存在且未过期) */
+export function isAuthenticated(): boolean {
+    const token = getToken();
+    if (!token?.accessToken) return false;
+    if (!token.accessExpires) return false;
+    return (token.accessExpires - currentUnixTimestamp()) > 0;
+}
+
 /** 格式化token(jwt格式) */
 export const formatToken = (token: string): string => {
     return 'Bearer ' + token;

+ 1 - 1
src/utils/crypto/base64url.ts

@@ -13,7 +13,7 @@ function padString(input: string): string {
  */
 export function toBase64(base64url: string): string {
     base64url = base64url.toString();
-    return padString(base64url).replace(/\-/g, '+').replace(/_/g, '/');
+    return padString(base64url).replace(/-/g, '+').replace(/_/g, '/');
 }
 
 /**

+ 7 - 5
src/utils/crypto/index.ts

@@ -1,19 +1,21 @@
-import CryptoJS from 'crypto-js/core';
-import 'crypto-js/lib-typedarrays';
-import 'crypto-js/cipher-core';
 import AES from 'crypto-js/aes';
+import 'crypto-js/cipher-core';
+import CryptoJS from 'crypto-js/core';
 import encBase64 from 'crypto-js/enc-base64';
 import encUtf8 from 'crypto-js/enc-utf8';
+import 'crypto-js/lib-typedarrays';
+import MD5 from 'crypto-js/md5';
 import modeCtr from 'crypto-js/mode-ctr';
 import modeEcb from 'crypto-js/mode-ecb';
-import MD5 from 'crypto-js/md5';
 import Rabbit from 'crypto-js/rabbit';
 import SHA1 from 'crypto-js/sha1';
 import SHA256 from 'crypto-js/sha256';
-import { toBase64, fromBase64 } from './base64url';
 
 import { bigEndianToLittleEndian, littleEndianToBigEndian } from '@/utils/bytesUtils';
 
+import { toBase64, fromBase64 } from './base64url';
+
+
 // ---------------------------------------------------------------------------
 // 常量
 // ---------------------------------------------------------------------------

+ 2 - 2
src/utils/httpUtils.ts

@@ -2,8 +2,8 @@ import saveAs from 'file-saver';
 
 import { ErrorShowType } from '@/defines';
 
-import { toLoginPage } from './routerUtils';
 import { request, RequestConfig } from './request';
+import { toLoginPage } from './routerUtils';
 
 type RequestMethods =
     | 'get'
@@ -76,7 +76,7 @@ async function handleBlobErrorResponse(blob: Blob, config?: RequestConfig) {
         const showType = errorData.showType ?? ErrorShowType.ERROR_MESSAGE;
 
         return handleDownloadError(errorMessage, errorCode, showType, config, errorData.data);
-    } catch (parseError) {
+    } catch {
         return handleDownloadError(
             'Failed to parse error response',
             500,

+ 1 - 1
src/utils/jsonUtils.ts

@@ -86,7 +86,7 @@ export const getJSONHighlight = (jsonString: string): string => {
             .replace(/:\s*(true|false|null)/g, ': <span style="color: #7c3aed;">$1</span>')
             .replace(/\n/g, '<br>')
             .replace(/ /g, '&nbsp;');
-    } catch (error) {
+    } catch {
         return jsonString;
     }
 };

+ 0 - 1
src/utils/navUtils.ts

@@ -1,6 +1,5 @@
 import routerConfig from '@/router/routes';
 import { getLocaleByPath } from '@/router/titles';
-
 import type { AppRouteObject } from '@/router/types';
 
 export interface NavMenuItem {

+ 10 - 10
src/utils/requestCrypto.ts

@@ -1,9 +1,6 @@
-import {
-    aesCbcDecryptBytes,
-    aesCbcEncryptBytes,
-    bytesBase64decode,
-    bytesBase64encode,
-} from '@/utils/crypto';
+
+import globalConfig from '@/config';
+import { CompressMethod } from '@/config/types';
 import {
     bytesToNumber,
     numberToBytesAt,
@@ -11,9 +8,12 @@ import {
     bytesToString,
     stringToBytes,
 } from '@/utils/bytesUtils';
-
-import globalConfig from '@/config';
-import { CompressMethod } from '@/config/types';
+import {
+    aesCbcDecryptBytes,
+    aesCbcEncryptBytes,
+    bytesBase64decode,
+    bytesBase64encode,
+} from '@/utils/crypto';
 
 import { compressBytes, decompressBytes, CompressFormat } from './compress';
 import { currentUnixTimestamp } from './timeUtils';
@@ -129,7 +129,7 @@ export async function decryptUrlParams<T = any>(params: string): Promise<T | nul
         const { data } = await decryptResponsePayload(dataBytes, key, compressMethod);
         const result = bytesToString(data);
         return JSON.parse(result) as T;
-    } catch (error) {
+    } catch {
         return null;
     }
 }

+ 10 - 0
tailwind.config.js

@@ -23,12 +23,22 @@ export default {
                     from: { transform: 'translateX(0)' },
                     to: { transform: 'translateX(-100%)' },
                 },
+                'expand-down': {
+                    from: { opacity: '0', transform: 'scaleY(0)' },
+                    to: { opacity: '1', transform: 'scaleY(1)' },
+                },
+                'collapse-up': {
+                    from: { opacity: '1', transform: 'scaleY(1)' },
+                    to: { opacity: '0', transform: 'scaleY(0)' },
+                },
             },
             animation: {
                 'slide-in-from-end': 'slide-in-from-end 0.25s ease-out forwards',
                 'slide-in-from-start': 'slide-in-from-start 0.25s ease-out forwards',
                 'slide-out-to-end': 'slide-out-to-end 0.25s ease-out forwards',
                 'slide-out-to-start': 'slide-out-to-start 0.25s ease-out forwards',
+                'expand-down': 'expand-down 0.2s ease-out forwards',
+                'collapse-up': 'collapse-up 0.15s ease-in forwards',
             },
         },
     },

+ 1 - 1
vite.config.ts

@@ -1,9 +1,9 @@
+import legacy from '@vitejs/plugin-legacy';
 import react from '@vitejs/plugin-react';
 import { defineConfig, loadEnv } from 'vite';
 import removeConsole from 'vite-plugin-remove-console';
 import topLevelAwait from 'vite-plugin-top-level-await';
 import wasm from 'vite-plugin-wasm';
-import legacy from '@vitejs/plugin-legacy';
 
 import { viteBuildInfo } from './build/buildInfo';
 import { configCompressPlugin } from './build/compress';

Vissa filer visades inte eftersom för många filer har ändrats