抽离登陆模块
This commit is contained in:
		
							
								
								
									
										12
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "kevisual dev",
 | 
				
			||||||
 | 
					  "image": "docker.cnb.cool/kevisual/dev-env:latest",
 | 
				
			||||||
 | 
					  "customizations": {
 | 
				
			||||||
 | 
					    "vscode": {
 | 
				
			||||||
 | 
					      "extensions": [
 | 
				
			||||||
 | 
					        "dbaeumer.vscode-eslint"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "postCreateCommand": ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
							
								
								
									
										4
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					# app-template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`/system/lib/app.js` 包函的模块是 `QueryRouterServer` 和 `Page` 和 `useConfigKey`
 | 
				
			||||||
							
								
								
									
										11
									
								
								config/esbuild.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								config/esbuild.config.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { build } from 'esbuild';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					build({
 | 
				
			||||||
 | 
					  entryPoints: ['src/index.ts'],
 | 
				
			||||||
 | 
					  bundle: true,
 | 
				
			||||||
 | 
					  outfile: 'dist/index.js',
 | 
				
			||||||
 | 
					  platform: 'browser',
 | 
				
			||||||
 | 
					  target: 'esnext',
 | 
				
			||||||
 | 
					  sourcemap: false,
 | 
				
			||||||
 | 
					  format: 'esm',
 | 
				
			||||||
 | 
					}).catch(() => process.exit(1));
 | 
				
			||||||
							
								
								
									
										33
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<meta charset="UTF-8">
 | 
				
			||||||
 | 
					<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					<title>AI Apps</title>
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="./src/assets/index.css">
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  html,
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #ai-root {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #root {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  <div id="root"></div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					<script src="./src/main.tsx" type="module"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "@kevisual/kevisual-login",
 | 
				
			||||||
 | 
					  "version": "0.0.4",
 | 
				
			||||||
 | 
					  "description": "",
 | 
				
			||||||
 | 
					  "main": "index.js",
 | 
				
			||||||
 | 
					  "basename": "/user/login",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "dev": "vite",
 | 
				
			||||||
 | 
					    "dev:web": "cross-env WEB_DEV=true vite --mode web",
 | 
				
			||||||
 | 
					    "build": "vite build",
 | 
				
			||||||
 | 
					    "esbuild": "node esbuild.config.mjs",
 | 
				
			||||||
 | 
					    "preview": "vite preview",
 | 
				
			||||||
 | 
					    "pub": "envision deploy ./dist -k login -v 0.0.4 -u -o user"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "keywords": [],
 | 
				
			||||||
 | 
					  "author": "abearxiong <xiongxiao@xiongxiao.me>",
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@floating-ui/dom": "^1.7.0",
 | 
				
			||||||
 | 
					    "@kevisual/query": "0.0.18",
 | 
				
			||||||
 | 
					    "@kevisual/query-login": "^0.0.6",
 | 
				
			||||||
 | 
					    "@kevisual/system-lib": "^0.0.22",
 | 
				
			||||||
 | 
					    "@kevisual/system-ui": "^0.0.3",
 | 
				
			||||||
 | 
					    "clsx": "^2.1.1",
 | 
				
			||||||
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
 | 
					    "lodash-es": "^4.17.21",
 | 
				
			||||||
 | 
					    "qrcode": "^1.5.4",
 | 
				
			||||||
 | 
					    "react-dom": "^19.1.0",
 | 
				
			||||||
 | 
					    "zustand": "^5.0.4"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@kevisual/router": "0.0.20",
 | 
				
			||||||
 | 
					    "@kevisual/store": "0.0.4",
 | 
				
			||||||
 | 
					    "@kevisual/types": "^0.0.10",
 | 
				
			||||||
 | 
					    "@tailwindcss/vite": "^4.1.7",
 | 
				
			||||||
 | 
					    "@types/qrcode": "^1.5.5",
 | 
				
			||||||
 | 
					    "@types/react": "^19.1.4",
 | 
				
			||||||
 | 
					    "@types/react-dom": "^19.1.5",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-basic-ssl": "^2.0.0",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react": "^4.4.1",
 | 
				
			||||||
 | 
					    "cross-env": "^7.0.3",
 | 
				
			||||||
 | 
					    "esbuild": "^0.25.4",
 | 
				
			||||||
 | 
					    "react": "^19.1.0",
 | 
				
			||||||
 | 
					    "tailwindcss": "^4.1.7",
 | 
				
			||||||
 | 
					    "vite": "^6.3.5"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "pnpm": {
 | 
				
			||||||
 | 
					    "onlyBuiltDependencies": [
 | 
				
			||||||
 | 
					      "@tailwindcss/oxide",
 | 
				
			||||||
 | 
					      "esbuild"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2522
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2522
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								public/MP_verify_NGWvli5lGpEkByyt.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/MP_verify_NGWvli5lGpEkByyt.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					NGWvli5lGpEkByyt
 | 
				
			||||||
							
								
								
									
										52
									
								
								public/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								public/config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					export const silkyConfig = {
 | 
				
			||||||
 | 
					  captchaAppId: '196626989',
 | 
				
			||||||
 | 
					  wxLogin: {
 | 
				
			||||||
 | 
					    appid: 'wxea912643e8747b44',
 | 
				
			||||||
 | 
					    redirect_uri: 'https://kevisual.silkyai.cn/api/wx/login',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  wxmpLogin: {
 | 
				
			||||||
 | 
					    loginUrl: `https://kevisual.xiongxiao.me/root/mini-web/login.html`,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  loginWay: ['account', 'wechat', 'phone', 'wechat-mp'],
 | 
				
			||||||
 | 
					  loginSuccess: '/root/center/',
 | 
				
			||||||
 | 
					  loginSuccessIsNew: '/root/center/',
 | 
				
			||||||
 | 
					  beian: '浙ICP备2024137660号-1',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const currentUrl = new URL(window.location.href);
 | 
				
			||||||
 | 
					const redirect = currentUrl.origin + currentUrl.pathname;
 | 
				
			||||||
 | 
					export const kevisualConfig = {
 | 
				
			||||||
 | 
					  loginWay: ['account', 'wechat'],
 | 
				
			||||||
 | 
					  loginSuccess: '/root/center/',
 | 
				
			||||||
 | 
					  loginSuccessIsNew: '/root/center/',
 | 
				
			||||||
 | 
					  wxLogin: {
 | 
				
			||||||
 | 
					    appid: 'wx9378885c8390e09b',
 | 
				
			||||||
 | 
					    redirect_uri: redirect,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  beian: '浙ICP备2025158778号',
 | 
				
			||||||
 | 
					  logo: 'https://kevisual.xiongxiao.me/root/center/panda.png',
 | 
				
			||||||
 | 
					  logoStyle: {
 | 
				
			||||||
 | 
					    borderRadius: '50%',
 | 
				
			||||||
 | 
					    marginTop: '10px',
 | 
				
			||||||
 | 
					    margin: '30px auto',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const kevisualXiongxiaoConfig = {
 | 
				
			||||||
 | 
					  ...kevisualConfig,
 | 
				
			||||||
 | 
					  loginWay: ['account', 'wechat-mp'],
 | 
				
			||||||
 | 
					  wxLogin: {
 | 
				
			||||||
 | 
					    appid: 'wxff97d569b1db16b6',
 | 
				
			||||||
 | 
					    redirect_uri: redirect,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  wxmpLogin: {
 | 
				
			||||||
 | 
					    loginUrl: `${currentUrl.origin}/root/mini-web/login.html`,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  logo: 'https://kevisual.xiongxiao.me/root/center/panda.png',
 | 
				
			||||||
 | 
					  beian: '蜀ICP备16031039号-2',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const url = new URL(window.location.href);
 | 
				
			||||||
 | 
					const isKevisual = url.hostname === 'kevisual.cn';
 | 
				
			||||||
 | 
					const isKevisualXiongxiao = url.hostname === 'kevisual.xiongxiao.me';
 | 
				
			||||||
 | 
					export const config = isKevisual ? kevisualConfig : isKevisualXiongxiao ? kevisualXiongxiaoConfig : silkyConfig;
 | 
				
			||||||
							
								
								
									
										0
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										16
									
								
								src/assets/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/assets/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					@import "tailwindcss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@layer components {
 | 
				
			||||||
 | 
					  .test-loading {
 | 
				
			||||||
 | 
					    @apply w-20 h-20 bg-gray-300 rounded-full animate-spin;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ai-bot-root {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: -100px;
 | 
				
			||||||
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/logo-baner-h.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/logo-baner-h.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										5
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					import { createRoot } from 'react-dom/client';
 | 
				
			||||||
 | 
					import { App } from './user/index.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					createRoot(document.getElementById('root')!).render(<App />);
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/modules/basename.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/modules/basename.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					export const basename = DEV_SERVER ? '/' : BASE_NAME;
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/modules/beian/beian.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/modules/beian/beian.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					.beian2 {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  z-index: 99;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 640px) {
 | 
				
			||||||
 | 
					  .beiann2 {
 | 
				
			||||||
 | 
					    position: static !important; /* 或者 relative,根据需求修改 */
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/modules/beian/beian.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/modules/beian/beian.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import clsx from 'clsx';
 | 
				
			||||||
 | 
					import './beian.css';
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  text?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const Beian = (props: Props) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={clsx('beian text-sm w-full flex justify-center text-[#e69c36]', props.className)}>
 | 
				
			||||||
 | 
					      <a href='//beian.miit.gov.cn' target='_blank'>
 | 
				
			||||||
 | 
					        {props.text}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										3
									
								
								src/modules/message.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/modules/message.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					import { message } from '@kevisual/system-ui/dist/message';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { message };
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/modules/query.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/modules/query.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { QueryClient } from '@kevisual/query';
 | 
				
			||||||
 | 
					import { QueryLoginBrowser } from '@kevisual/query-login';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const query = new QueryClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const queryLogin = new QueryLoginBrowser({
 | 
				
			||||||
 | 
					  query: query as any,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					query.after(async (res, ctx) => {
 | 
				
			||||||
 | 
					  if (res.code === 401) {
 | 
				
			||||||
 | 
					    if (query.stop) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        code: 500,
 | 
				
			||||||
 | 
					        success: false,
 | 
				
			||||||
 | 
					        message: '登录已过期.',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    query.stop = true;
 | 
				
			||||||
 | 
					    const result = await queryLogin.afterCheck401ToRefreshToken(res, ctx);
 | 
				
			||||||
 | 
					    query.stop = false;
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return res;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										102
									
								
								src/user/Info.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/user/Info.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					import React, { useEffect, useLayoutEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import { Layout, MainLayout } from './layout/UserLayout';
 | 
				
			||||||
 | 
					import { useUserStore } from './store';
 | 
				
			||||||
 | 
					import { message } from '@/modules/message';
 | 
				
			||||||
 | 
					export const Info = () => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Layout>
 | 
				
			||||||
 | 
					      <MainLayout className='bg-yellow-50'>
 | 
				
			||||||
 | 
					        <ProfileForm />
 | 
				
			||||||
 | 
					      </MainLayout>
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const ProfileForm: React.FC = () => {
 | 
				
			||||||
 | 
					  const [nickname, setNickname] = useState('');
 | 
				
			||||||
 | 
					  const [name, setName] = useState('');
 | 
				
			||||||
 | 
					  const [avatar, setAvatar] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const userStore = useUserStore();
 | 
				
			||||||
 | 
					  const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
 | 
					    if (e.target.files && e.target.files[0]) {
 | 
				
			||||||
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
 | 
					      // 如果文件大于 2MB,提示用户
 | 
				
			||||||
 | 
					      if (file.size > 2 * 1024 * 1024) {
 | 
				
			||||||
 | 
					        message.error('文件大小不能超过 2MB');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const reader = new FileReader();
 | 
				
			||||||
 | 
					      reader.onload = () => {
 | 
				
			||||||
 | 
					        setAvatar(reader.result as string);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      reader.readAsDataURL(file);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = () => {
 | 
				
			||||||
 | 
					    // alert(`昵称: ${nickname}, 姓名: ${name}`)
 | 
				
			||||||
 | 
					    userStore.updateUser({
 | 
				
			||||||
 | 
					      nickname,
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        personalname: name,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      avatar,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    userStore.getUpdateUser();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (userStore.data) {
 | 
				
			||||||
 | 
					      setNickname(userStore.data.nickname);
 | 
				
			||||||
 | 
					      setName(userStore.data?.data?.personalname);
 | 
				
			||||||
 | 
					      setAvatar(userStore.data.avatar);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [userStore.data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='w-full h-full mx-auto  p-6 rounded-lg'>
 | 
				
			||||||
 | 
					      <h2 className='text-center text-[#F39800] text-2xl font-bold mb-2'>完善个人信息</h2>
 | 
				
			||||||
 | 
					      <p className='text-center text-yellow-400 mb-6'>请设置您的基本信息</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Avatar Section */}
 | 
				
			||||||
 | 
					      <div className='text-center mb-6'>
 | 
				
			||||||
 | 
					        <label className='block'>
 | 
				
			||||||
 | 
					          <div className='w-24 h-24 my-2 mx-auto rounded-full bg-yellow-200 flex items-center justify-center text-4xl text-yellow-500 cursor-pointer'>
 | 
				
			||||||
 | 
					            {avatar ? <img src={avatar} alt='Avatar' className='rounded-full w-full h-full object-cover' /> : <span>👤</span>}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <p className='text-sm text-gray-500 mt-2'>点击更换头像</p>
 | 
				
			||||||
 | 
					          <input type='file' accept='image/*' className='hidden' onChange={handleAvatarChange} />
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Form Fields */}
 | 
				
			||||||
 | 
					      <div className='mb-4'>
 | 
				
			||||||
 | 
					        <label className='block text-[#F39800] mb-2'>昵称 *</label>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type='text'
 | 
				
			||||||
 | 
					          placeholder='请设置您的昵称'
 | 
				
			||||||
 | 
					          value={nickname}
 | 
				
			||||||
 | 
					          onChange={(e) => setNickname(e.target.value)}
 | 
				
			||||||
 | 
					          className='w-full border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className='mb-6'>
 | 
				
			||||||
 | 
					        <label className='block text-[#F39800] mb-2'>姓名 *</label>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type='text'
 | 
				
			||||||
 | 
					          placeholder='请输入您的姓名'
 | 
				
			||||||
 | 
					          value={name}
 | 
				
			||||||
 | 
					          onChange={(e) => setName(e.target.value)}
 | 
				
			||||||
 | 
					          className='w-full border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Submit Button */}
 | 
				
			||||||
 | 
					      <button onClick={handleSubmit} className='w-full py-2 bg-[#F39800] text-white rounded hover:bg-orange-600'>
 | 
				
			||||||
 | 
					        完成设置
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/user/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/user/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					@media (min-width: 1000px) and (max-width: 1440px) {
 | 
				
			||||||
 | 
					  .login-main {
 | 
				
			||||||
 | 
					    /* background-color: red !important; */
 | 
				
			||||||
 | 
					    scale: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (min-width: 1500px) and (max-width: 2000px) {
 | 
				
			||||||
 | 
					  .login-main {
 | 
				
			||||||
 | 
					    /* background-color: red !important; */
 | 
				
			||||||
 | 
					    scale: 1.2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/user/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/user/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import './index.css';
 | 
				
			||||||
 | 
					import { Login } from './login';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const App = () => {
 | 
				
			||||||
 | 
					  return <Login />;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										81
									
								
								src/user/layout/UserLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/user/layout/UserLayout.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					import clsx from 'clsx';
 | 
				
			||||||
 | 
					import LogoBannerH from '@/assets/logo-baner-h.png';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Beian } from '@/modules/beian/beian';
 | 
				
			||||||
 | 
					import { useUserStore } from '../store';
 | 
				
			||||||
 | 
					import { useShallow } from 'zustand/shallow';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  children?: React.ReactNode;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const Layout = (props: Props) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx(
 | 
				
			||||||
 | 
					        'w-full h-full sm:bg-amber-100  flex sm:items-center sm:justify-center justify-normal items-start relative overflow-hidden',
 | 
				
			||||||
 | 
					        props?.className,
 | 
				
			||||||
 | 
					      )}>
 | 
				
			||||||
 | 
					      <div className='w-full h-full overflow-scroll sm:overflow-hidden sm:scrollbar flex sm:items-center sm:justify-center'>{props.children}</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MainLayout = (props: Props) => {
 | 
				
			||||||
 | 
					  const config = useUserStore((state) => state.config);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx(
 | 
				
			||||||
 | 
					        'login-main w-[450px] min-h-[660px] bg-white mt-10 sm:mt-0 sm:shadow-lg rounded-md flex items-center flex-col relative ',
 | 
				
			||||||
 | 
					        props?.className,
 | 
				
			||||||
 | 
					      )}>
 | 
				
			||||||
 | 
					      {props.children}
 | 
				
			||||||
 | 
					      <p className='mt-5 text-xs text-center text-[#e69c36]'>
 | 
				
			||||||
 | 
					        登录即表示同意 <span className='font-medium text-[#e69c36]'>服务条款</span> 和 <span className='font-medium text-[#e69c36]'>隐私政策</span>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					      <Beian className=' mb-4' text={config?.beian} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const LoginWrapper = (props: Props) => {
 | 
				
			||||||
 | 
					  const config = useUserStore((state) => state.config);
 | 
				
			||||||
 | 
					  const loginWay = config?.loginWay || ['account'];
 | 
				
			||||||
 | 
					  const store = useUserStore(
 | 
				
			||||||
 | 
					    useShallow((state) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        loginByWechat: state.loginByWechat,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const checkWechat = () => {
 | 
				
			||||||
 | 
					    const url = new URL(window.location.href);
 | 
				
			||||||
 | 
					    const code = url.searchParams.get('code');
 | 
				
			||||||
 | 
					    const state = url.searchParams.get('state');
 | 
				
			||||||
 | 
					    if (code && state) {
 | 
				
			||||||
 | 
					      store.loginByWechat(code);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    checkWechat();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Layout>
 | 
				
			||||||
 | 
					      <MainLayout className=''>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className='mt-4'
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            width: '90px',
 | 
				
			||||||
 | 
					            height: '90px',
 | 
				
			||||||
 | 
					            overflow: 'hidden',
 | 
				
			||||||
 | 
					            ...config?.logoStyle,
 | 
				
			||||||
 | 
					          }}>
 | 
				
			||||||
 | 
					          <img src={config?.logo || LogoBannerH} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className='mt-4 text-[#F39800] font-bold text-xl'>欢迎回来</div>
 | 
				
			||||||
 | 
					        {loginWay.length > 1 && <div className='text-sm text-yellow-400 mt-2'>请选择登陆方式</div>}
 | 
				
			||||||
 | 
					        <div>{props.children}</div>
 | 
				
			||||||
 | 
					      </MainLayout>
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										331
									
								
								src/user/login/Login.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								src/user/login/Login.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,331 @@
 | 
				
			|||||||
 | 
					import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 | 
				
			||||||
 | 
					import { useUserStore } from '../store';
 | 
				
			||||||
 | 
					import { setWxerwma, wxId } from '@/wx/ws-login.ts';
 | 
				
			||||||
 | 
					import { checkCaptcha } from '@/wx/tencent-captcha.ts';
 | 
				
			||||||
 | 
					import { dynimicLoadTcapTcha } from '@/wx/load-js.ts';
 | 
				
			||||||
 | 
					import { message } from '@/modules/message';
 | 
				
			||||||
 | 
					import { useShallow } from 'zustand/react/shallow';
 | 
				
			||||||
 | 
					import { WeChatMpLogin } from './modules/WeChatMpLogin';
 | 
				
			||||||
 | 
					const WeChatLogin: React.FC = () => {
 | 
				
			||||||
 | 
					  const userStore = useUserStore(
 | 
				
			||||||
 | 
					    useShallow((state) => {
 | 
				
			||||||
 | 
					      return { config: state.config! };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setWxerwma({
 | 
				
			||||||
 | 
					      ...userStore.config.wxLogin,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <div id={wxId} className='max-w-sm mx-auto bg-white rounded-lg text-center'></div>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type VerificationCodeInputProps = {
 | 
				
			||||||
 | 
					  onGetCode: () => void;
 | 
				
			||||||
 | 
					  verificationCode: string;
 | 
				
			||||||
 | 
					  setVerificationCode: (value: string) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const VerificationCodeInput = forwardRef(({ onGetCode, verificationCode, setVerificationCode }: VerificationCodeInputProps, ref) => {
 | 
				
			||||||
 | 
					  // const [verificationCode, setVerificationCode] = useState('')
 | 
				
			||||||
 | 
					  const [isCounting, setIsCounting] = useState(false);
 | 
				
			||||||
 | 
					  const [countdown, setCountdown] = useState(60);
 | 
				
			||||||
 | 
					  useImperativeHandle(ref, () => ({
 | 
				
			||||||
 | 
					    isCounting,
 | 
				
			||||||
 | 
					    setIsCounting,
 | 
				
			||||||
 | 
					    setCountdown,
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					  const handleGetCode = () => {
 | 
				
			||||||
 | 
					    if (!isCounting) {
 | 
				
			||||||
 | 
					      // setIsCounting(true)
 | 
				
			||||||
 | 
					      // setCountdown(60)
 | 
				
			||||||
 | 
					      onGetCode(); // 调用父组件传入的获取验证码逻辑
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    let timer;
 | 
				
			||||||
 | 
					    if (isCounting) {
 | 
				
			||||||
 | 
					      timer = setInterval(() => {
 | 
				
			||||||
 | 
					        setCountdown((prev) => {
 | 
				
			||||||
 | 
					          if (prev <= 1) {
 | 
				
			||||||
 | 
					            setIsCounting(false);
 | 
				
			||||||
 | 
					            clearInterval(timer);
 | 
				
			||||||
 | 
					            return 60;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return prev - 1;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }, 1000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return () => clearInterval(timer);
 | 
				
			||||||
 | 
					  }, [isCounting]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='mb-4 items-center'>
 | 
				
			||||||
 | 
					      <label className='block text-[#F39800] py-1 mb-1'>验证码</label>
 | 
				
			||||||
 | 
					      <div className='flex'>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type='text'
 | 
				
			||||||
 | 
					          className='border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					          placeholder='请输入验证码'
 | 
				
			||||||
 | 
					          value={verificationCode}
 | 
				
			||||||
 | 
					          onChange={(e) => setVerificationCode(e.target.value)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          className={`ml-2 px-4 py-2 w-[120px] rounded-md text-white ${isCounting ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#F39800] hover:bg-yellow-400'}`}
 | 
				
			||||||
 | 
					          onClick={handleGetCode}
 | 
				
			||||||
 | 
					          disabled={isCounting}>
 | 
				
			||||||
 | 
					          {isCounting ? `${countdown}s 后重试` : '获取验证码'}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function PhoneNumberValidation({ phoneNumber, setPhoneNumber }) {
 | 
				
			||||||
 | 
					  // const [phoneNumber, setPhoneNumber] = useState('')
 | 
				
			||||||
 | 
					  const [errorMessage, setErrorMessage] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const validatePhoneNumber = (number) => {
 | 
				
			||||||
 | 
					    // 假设手机号的格式为中国的11位数字
 | 
				
			||||||
 | 
					    const phoneRegex = /^1[3-9]\d{9}$/;
 | 
				
			||||||
 | 
					    if (!phoneRegex.test(number)) {
 | 
				
			||||||
 | 
					      setErrorMessage('请输入有效的手机号');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setErrorMessage('');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = (e) => {
 | 
				
			||||||
 | 
					    const value = e.target.value;
 | 
				
			||||||
 | 
					    setPhoneNumber(value);
 | 
				
			||||||
 | 
					    validatePhoneNumber(value);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className=''>
 | 
				
			||||||
 | 
					      <label className='block text-[#F39800] py-1 mb-1'>手机号</label>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type='text'
 | 
				
			||||||
 | 
					        className={`w-full border rounded-lg p-2 focus:outline-none focus:ring-2 ${
 | 
				
			||||||
 | 
					          errorMessage ? 'border-red-500 focus:ring-red-500' : 'border-[#FBBF24] focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					        }`}
 | 
				
			||||||
 | 
					        placeholder='请输入手机号'
 | 
				
			||||||
 | 
					        value={phoneNumber}
 | 
				
			||||||
 | 
					        onChange={handleChange}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      {errorMessage && <p className='text-red-500 text-xs mt-1'>{errorMessage}</p>}
 | 
				
			||||||
 | 
					      {!errorMessage && <p className='text-gray-500 text-xs mt-1 invisible'>请输入11位手机号</p>}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function AccountLogin({ accountName, setAccountName, password, setPassword }) {
 | 
				
			||||||
 | 
					  const [errorMessage, setErrorMessage] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const validateAccountName = (name) => {
 | 
				
			||||||
 | 
					    if (name.length < 3) {
 | 
				
			||||||
 | 
					      setErrorMessage('账户名至少需要3个字符');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setErrorMessage('');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleAccountChange = (e) => {
 | 
				
			||||||
 | 
					    const value = e.target.value;
 | 
				
			||||||
 | 
					    setAccountName(value);
 | 
				
			||||||
 | 
					    validateAccountName(value);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlePasswordChange = (e) => {
 | 
				
			||||||
 | 
					    setPassword(e.target.value);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='flex flex-col gap-1'>
 | 
				
			||||||
 | 
					      <label className='block text-[#F39800] py-1 mb-1'>账户名</label>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type='text'
 | 
				
			||||||
 | 
					        className={`w-full border rounded-lg p-2 focus:outline-none focus:ring-2 ${
 | 
				
			||||||
 | 
					          errorMessage ? 'border-red-500 focus:ring-red-500' : 'border-[#FBBF24] focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					        }`}
 | 
				
			||||||
 | 
					        placeholder='请输入账户名'
 | 
				
			||||||
 | 
					        value={accountName}
 | 
				
			||||||
 | 
					        onChange={handleAccountChange}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      {errorMessage && <p className='text-red-500 text-xs mt-1'>{errorMessage}</p>}
 | 
				
			||||||
 | 
					      {!errorMessage && <p className='text-gray-500 text-xs mt-1 invisible'>账户名至少需要3个字符</p>}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <label className='block text-[#F39800] py-1 mb-1 mt-2'>密码</label>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type='password'
 | 
				
			||||||
 | 
					        className='w-full border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
 | 
				
			||||||
 | 
					        placeholder='请输入密码'
 | 
				
			||||||
 | 
					        value={password}
 | 
				
			||||||
 | 
					        onChange={handlePasswordChange}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const LoginForm: React.FC = () => {
 | 
				
			||||||
 | 
					  const [phoneNumber, setPhoneNumber] = useState('');
 | 
				
			||||||
 | 
					  const [verificationCode, setVerificationCode] = useState('');
 | 
				
			||||||
 | 
					  const [accountName, setAccountName] = useState('');
 | 
				
			||||||
 | 
					  const [password, setPassword] = useState('');
 | 
				
			||||||
 | 
					  const [activeTab, setActiveTab] = useState<'phone' | 'wechat' | 'wechat-mp' | 'account'>('phone');
 | 
				
			||||||
 | 
					  const userStore = useUserStore(
 | 
				
			||||||
 | 
					    useShallow((state) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        config: state.config! || {},
 | 
				
			||||||
 | 
					        getCode: state.getCode,
 | 
				
			||||||
 | 
					        login: state.login,
 | 
				
			||||||
 | 
					        loginByAccount: state.loginByAccount,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const ref = useRef<any>(null);
 | 
				
			||||||
 | 
					  const handleGetCode = async () => {
 | 
				
			||||||
 | 
					    const loaded = await dynimicLoadTcapTcha();
 | 
				
			||||||
 | 
					    if (!loaded) {
 | 
				
			||||||
 | 
					      message.error('验证码加载失败');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const captcha = await checkCaptcha(userStore.config.captchaAppId);
 | 
				
			||||||
 | 
					    if (captcha.ret !== 0) {
 | 
				
			||||||
 | 
					      message.error('验证码发送失败');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ref.current.setIsCounting(true);
 | 
				
			||||||
 | 
					    ref.current.setCountdown(60);
 | 
				
			||||||
 | 
					    userStore.getCode(phoneNumber, captcha);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    dynimicLoadTcapTcha();
 | 
				
			||||||
 | 
					    if (userStore.config.loginWay?.length > 0) {
 | 
				
			||||||
 | 
					      setActiveTab(userStore.config.loginWay[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [userStore.config.loginWay]);
 | 
				
			||||||
 | 
					  const handleLogin = () => {
 | 
				
			||||||
 | 
					    // alert(`登录中:手机号: ${phoneNumber}, 验证码: ${verificationCode}`)
 | 
				
			||||||
 | 
					    userStore.login(phoneNumber, verificationCode);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const inLoginWay = (way: string) => {
 | 
				
			||||||
 | 
					    const loginWay = userStore.config?.loginWay || [];
 | 
				
			||||||
 | 
					    return loginWay.includes(way);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const handleAccountLogin = () => {
 | 
				
			||||||
 | 
					    if (!accountName || !password) {
 | 
				
			||||||
 | 
					      message.error('请输入账户名和密码');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    userStore.loginByAccount(accountName, password);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  useListenEnter({ active: activeTab === 'phone', handleLogin });
 | 
				
			||||||
 | 
					  useListenEnter({ active: activeTab === 'account', handleLogin: handleAccountLogin });
 | 
				
			||||||
 | 
					  const tab = useMemo(() => {
 | 
				
			||||||
 | 
					    const phoneCom = (
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        key='phone'
 | 
				
			||||||
 | 
					        className={`flex-1 py-2 font-medium ${activeTab === 'phone' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
 | 
				
			||||||
 | 
					        onClick={() => setActiveTab('phone')}>
 | 
				
			||||||
 | 
					        手机号登录
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const wechatCom = (
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        key='wechat'
 | 
				
			||||||
 | 
					        className={`flex-1 py-2 font-medium ${activeTab === 'wechat' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
 | 
				
			||||||
 | 
					        onClick={() => setActiveTab('wechat')}>
 | 
				
			||||||
 | 
					        微信登录
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const wechatMpCom = (
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        key='wechat-mp'
 | 
				
			||||||
 | 
					        className={`flex-1 py-2 font-medium ${activeTab === 'wechat-mp' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
 | 
				
			||||||
 | 
					        onClick={() => setActiveTab('wechat-mp')}>
 | 
				
			||||||
 | 
					        微信公众号登录
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const accountCom = (
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        key='account'
 | 
				
			||||||
 | 
					        className={`flex-1 py-2 font-medium ${activeTab === 'account' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
 | 
				
			||||||
 | 
					        onClick={() => setActiveTab('account')}>
 | 
				
			||||||
 | 
					        账号登录
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const coms: React.ReactNode[] = [];
 | 
				
			||||||
 | 
					    for (const way of userStore.config.loginWay) {
 | 
				
			||||||
 | 
					      if (way === 'phone') {
 | 
				
			||||||
 | 
					        coms.push(phoneCom);
 | 
				
			||||||
 | 
					      } else if (way === 'wechat') {
 | 
				
			||||||
 | 
					        coms.push(wechatCom);
 | 
				
			||||||
 | 
					      } else if (way === 'account') {
 | 
				
			||||||
 | 
					        coms.push(accountCom);
 | 
				
			||||||
 | 
					      } else if (way === 'wechat-mp') {
 | 
				
			||||||
 | 
					        coms.push(wechatMpCom);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return coms;
 | 
				
			||||||
 | 
					  }, [userStore.config.loginWay, activeTab]);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='max-w-sm mx-auto p-6 bg-white  rounded-lg flex flex-col items-center justify-center'>
 | 
				
			||||||
 | 
					      {/* Tabs */}
 | 
				
			||||||
 | 
					      <div className='flex text-gray-400 min-w-[360px]'>{tab}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className='mt-4 min-h-[300px] w-full relative'>
 | 
				
			||||||
 | 
					        {/* Phone Login Form */}
 | 
				
			||||||
 | 
					        {activeTab === 'phone' && inLoginWay('phone') && (
 | 
				
			||||||
 | 
					          <div className='mt-4 pt-4  '>
 | 
				
			||||||
 | 
					            <PhoneNumberValidation phoneNumber={phoneNumber} setPhoneNumber={setPhoneNumber} />
 | 
				
			||||||
 | 
					            <VerificationCodeInput ref={ref} onGetCode={handleGetCode} verificationCode={verificationCode} setVerificationCode={setVerificationCode} />
 | 
				
			||||||
 | 
					            <button className='w-full mt-3 py-2 bg-[#F39800] text-white rounded-lg hover:bg-yellow-400' onClick={handleLogin}>
 | 
				
			||||||
 | 
					              登录
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* WeChat Login Placeholder */}
 | 
				
			||||||
 | 
					        {activeTab === 'wechat' && inLoginWay('wechat') && (
 | 
				
			||||||
 | 
					          <div className='-mt-2 w-[310px] ml-[12px] flex flex-col justify-center text-center text-gray-500 absolute top-0 left-0 z-index-10'>
 | 
				
			||||||
 | 
					            <WeChatLogin />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {activeTab === 'wechat-mp' && inLoginWay('wechat-mp') && (
 | 
				
			||||||
 | 
					          <div className='mt-2 w-[310px] ml-[12px] flex flex-col justify-center text-center '>
 | 
				
			||||||
 | 
					            <WeChatMpLogin />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {activeTab === 'account' && inLoginWay('account') && (
 | 
				
			||||||
 | 
					          <div className='mt-4 pt-4 w-full '>
 | 
				
			||||||
 | 
					            <AccountLogin accountName={accountName} setAccountName={setAccountName} password={password} setPassword={setPassword} />
 | 
				
			||||||
 | 
					            <button className='w-full mt-3 py-2 bg-[#F39800] text-white rounded-lg hover:bg-yellow-400' onClick={handleAccountLogin}>
 | 
				
			||||||
 | 
					              登录
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default LoginForm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useListenEnter = (opts?: { active: boolean; handleLogin: () => void }) => {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!opts?.active) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const handleEnter = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key === 'Enter') {
 | 
				
			||||||
 | 
					        opts?.handleLogin?.();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    window.addEventListener('keydown', handleEnter);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener('keydown', handleEnter);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [opts?.active, opts?.handleLogin]);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										49
									
								
								src/user/login/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/user/login/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import { useEffect, useLayoutEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import { Layout, LoginWrapper, MainLayout } from '../layout/UserLayout';
 | 
				
			||||||
 | 
					import LoginForm from './Login';
 | 
				
			||||||
 | 
					import { useUserStore } from '../store';
 | 
				
			||||||
 | 
					import { useShallow } from 'zustand/react/shallow';
 | 
				
			||||||
 | 
					import { Suspense } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Login = () => {
 | 
				
			||||||
 | 
					  const userStore = useUserStore(
 | 
				
			||||||
 | 
					    useShallow((state) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        config: state.config,
 | 
				
			||||||
 | 
					        setConfig: state.setConfig,
 | 
				
			||||||
 | 
					        loadedConfig: state.loadedConfig,
 | 
				
			||||||
 | 
					        setLoadedConfig: state.setLoadedConfig,
 | 
				
			||||||
 | 
					        queryCheck: state.queryCheck,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    fetchConfig();
 | 
				
			||||||
 | 
					    userStore.queryCheck();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  const fetchConfig = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await fetch('./config.js');
 | 
				
			||||||
 | 
					      const configScript = await res.text();
 | 
				
			||||||
 | 
					      const blob = new Blob([configScript], { type: 'application/javascript' });
 | 
				
			||||||
 | 
					      const moduleUrl = URL.createObjectURL(blob);
 | 
				
			||||||
 | 
					      const module = await import(/* @vite-ignore */ moduleUrl);
 | 
				
			||||||
 | 
					      URL.revokeObjectURL(moduleUrl); // Clean up the object URL
 | 
				
			||||||
 | 
					      userStore.setConfig(module.config);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('Failed to load config:', error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Suspense fallback={<div></div>}>
 | 
				
			||||||
 | 
					      {userStore.loadedConfig ? (
 | 
				
			||||||
 | 
					        <LoginWrapper>
 | 
				
			||||||
 | 
					          <LoginForm />
 | 
				
			||||||
 | 
					        </LoginWrapper>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <div>Loading...</div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </Suspense>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/user/login/modules/WeChatMpLogin.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/user/login/modules/WeChatMpLogin.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import { useUserStore, Config } from '@/user/store';
 | 
				
			||||||
 | 
					import { useShallow } from 'zustand/shallow';
 | 
				
			||||||
 | 
					import QRCode, { QRCodeToDataURLOptions } from 'qrcode';
 | 
				
			||||||
 | 
					import { message } from '@/modules/message';
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from 'react';
 | 
				
			||||||
 | 
					import { query, queryLogin } from '@/modules/query';
 | 
				
			||||||
 | 
					const useCreateLoginQRCode = (config: Config) => {
 | 
				
			||||||
 | 
					  const url = new URL(window.location.href);
 | 
				
			||||||
 | 
					  const redirect = url.searchParams.get('redirect');
 | 
				
			||||||
 | 
					  const loginSuccessUrl = config.loginSuccess;
 | 
				
			||||||
 | 
					  const redirectURL = redirect ? decodeURIComponent(redirect) : loginSuccessUrl;
 | 
				
			||||||
 | 
					  var opts: QRCodeToDataURLOptions = {
 | 
				
			||||||
 | 
					    errorCorrectionLevel: 'H',
 | 
				
			||||||
 | 
					    type: 'image/jpeg',
 | 
				
			||||||
 | 
					    margin: 1,
 | 
				
			||||||
 | 
					    width: 300,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const [state, setState] = useState('');
 | 
				
			||||||
 | 
					  let timer = useRef<any>(null);
 | 
				
			||||||
 | 
					  const loginUrl = config?.wxmpLogin?.loginUrl || '';
 | 
				
			||||||
 | 
					  if (!loginUrl) {
 | 
				
			||||||
 | 
					    message.error('没有配置微信登陆配置');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const createQrcode = async (state: string) => {
 | 
				
			||||||
 | 
					    const text = `${loginUrl}?state=${state}`;
 | 
				
			||||||
 | 
					    var img = document.getElementById('qrcode')! as HTMLCanvasElement;
 | 
				
			||||||
 | 
					    // img.src = url;
 | 
				
			||||||
 | 
					    const res = await QRCode.toDataURL(img, text, opts);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const checkLogin = async (state: string) => {
 | 
				
			||||||
 | 
					    const res = await fetch(`/api/router?path=wx&key=checkLogin&state=${state}`).then((res) => res.json());
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      console.log(res);
 | 
				
			||||||
 | 
					      const token = res.data;
 | 
				
			||||||
 | 
					      if (token) {
 | 
				
			||||||
 | 
					        localStorage.setItem('token', token.accessToken);
 | 
				
			||||||
 | 
					        await queryLogin.setLoginToken(token);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        window.location.href = redirectURL;
 | 
				
			||||||
 | 
					      }, 1000);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      timer.current = setTimeout(() => {
 | 
				
			||||||
 | 
					        checkLogin(state);
 | 
				
			||||||
 | 
					      }, 2000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 随机生成一个state
 | 
				
			||||||
 | 
					    const state = Math.random().toString(36).substring(2, 15);
 | 
				
			||||||
 | 
					    createQrcode(state);
 | 
				
			||||||
 | 
					    checkLogin(state);
 | 
				
			||||||
 | 
					    const timer2 = setInterval(() => {
 | 
				
			||||||
 | 
					      const state = Math.random().toString(36).substring(2, 15);
 | 
				
			||||||
 | 
					      clearTimeout(timer.current); // 清除定时器
 | 
				
			||||||
 | 
					      createQrcode(state); // 90秒后更新二维码
 | 
				
			||||||
 | 
					      checkLogin(state);
 | 
				
			||||||
 | 
					    }, 90000);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      clearInterval(timer.current);
 | 
				
			||||||
 | 
					      clearInterval(timer2);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  return { createQrcode };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const WeChatMpLogin = () => {
 | 
				
			||||||
 | 
					  const userStore = useUserStore(
 | 
				
			||||||
 | 
					    useShallow((state) => {
 | 
				
			||||||
 | 
					      return { config: state.config! };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  useCreateLoginQRCode(userStore.config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <canvas id='qrcode' width='300' height='300'></canvas>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/user/module/load-js.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/user/module/load-js.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					// <script src="https://turing.captcha.qcloud.com/TCaptcha.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dynimicLoadTcapTcha = async (): Promise<boolean> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const script = document.createElement('script')
 | 
				
			||||||
 | 
					    script.type = 'text/javascript'
 | 
				
			||||||
 | 
					    script.id = 'tencent-captcha'
 | 
				
			||||||
 | 
					    if (document.getElementById('tencent-captcha')) {
 | 
				
			||||||
 | 
					      resolve(true)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    script.src = 'https://turing.captcha.qcloud.com/TCaptcha.js'
 | 
				
			||||||
 | 
					    script.onload = () => {
 | 
				
			||||||
 | 
					      resolve(true)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    script.onerror = (error) => {
 | 
				
			||||||
 | 
					      reject(error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    document.body.appendChild(script)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										209
									
								
								src/user/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/user/store/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,209 @@
 | 
				
			|||||||
 | 
					import { query, queryLogin } from '@/modules/query';
 | 
				
			||||||
 | 
					import { TencentCaptcha } from '@/wx/tencent-captcha.ts';
 | 
				
			||||||
 | 
					import { message } from '@/modules/message';
 | 
				
			||||||
 | 
					import { create } from 'zustand';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Config = {
 | 
				
			||||||
 | 
					  loginWay: any[];
 | 
				
			||||||
 | 
					  wxLogin: {
 | 
				
			||||||
 | 
					    appid: string;
 | 
				
			||||||
 | 
					    redirect_uri: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  wxmpLogin: {
 | 
				
			||||||
 | 
					    loginUrl?: string; // 微信公众号的网页授权登陆
 | 
				
			||||||
 | 
					    appid?: string; // 微信公众号的appid
 | 
				
			||||||
 | 
					    redirect_uri?: string; // 微信公众号的网页授权登陆
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  captchaAppId: string;
 | 
				
			||||||
 | 
					  loginSuccess: string;
 | 
				
			||||||
 | 
					  loginSuccessIsNew: string;
 | 
				
			||||||
 | 
					  logo: string;
 | 
				
			||||||
 | 
					  logoStyle: {
 | 
				
			||||||
 | 
					    borderRadius: string;
 | 
				
			||||||
 | 
					    width?: string;
 | 
				
			||||||
 | 
					    height?: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  beian: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const redirectToSuccess = async (config: Config) => {
 | 
				
			||||||
 | 
					  const href = location.href;
 | 
				
			||||||
 | 
					  const url = new URL(href);
 | 
				
			||||||
 | 
					  const redirect = url.searchParams.get('redirect');
 | 
				
			||||||
 | 
					  if (redirect) {
 | 
				
			||||||
 | 
					    const href = decodeURIComponent(redirect);
 | 
				
			||||||
 | 
					    window.open(href, '_self');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await new Promise((resolve) => {
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      resolve(true);
 | 
				
			||||||
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (config?.loginSuccess) {
 | 
				
			||||||
 | 
					    location.href = config?.loginSuccess;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    location.href = '/';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					type UserStore = {
 | 
				
			||||||
 | 
					  isAuthenticated: boolean;
 | 
				
			||||||
 | 
					  qrCodeUrl: string;
 | 
				
			||||||
 | 
					  checkAuthStatus: () => void;
 | 
				
			||||||
 | 
					  getCode: (phone: string, captcha: TencentCaptcha) => void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 手机号登录
 | 
				
			||||||
 | 
					   * @param phone 手机号
 | 
				
			||||||
 | 
					   * @param code 验证码
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  login: (phone: string, code: string) => void;
 | 
				
			||||||
 | 
					  updateUser: (data: any, opts?: { needRedirect?: boolean }) => void;
 | 
				
			||||||
 | 
					  getUpdateUser: () => void;
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  setData: (data: any) => void;
 | 
				
			||||||
 | 
					  config: Config | null;
 | 
				
			||||||
 | 
					  setConfig: (config: any) => void;
 | 
				
			||||||
 | 
					  loadedConfig: boolean;
 | 
				
			||||||
 | 
					  setLoadedConfig: (loadedConfig: boolean) => void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 账号密码登录
 | 
				
			||||||
 | 
					   * @param username 账号
 | 
				
			||||||
 | 
					   * @param password 密码
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  loginByAccount: (username: string, password: string) => void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 检查是否需要跳转, 插件登陆
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  queryCheck: () => void;
 | 
				
			||||||
 | 
					  loginByWechat: (code: string) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const useUserStore = create<UserStore>((set, get) => ({
 | 
				
			||||||
 | 
					  isAuthenticated: false,
 | 
				
			||||||
 | 
					  qrCodeUrl: '',
 | 
				
			||||||
 | 
					  checkAuthStatus: () => {
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  getCode: async (phone, captcha) => {
 | 
				
			||||||
 | 
					    const res = await query.post({
 | 
				
			||||||
 | 
					      path: 'sms',
 | 
				
			||||||
 | 
					      key: 'send',
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        phone,
 | 
				
			||||||
 | 
					        captcha,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      // do something
 | 
				
			||||||
 | 
					      message.success('验证码发送成功');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '验证码发送失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  login: async (phone, code) => {
 | 
				
			||||||
 | 
					    const config = get().config!;
 | 
				
			||||||
 | 
					    const res = await query.post({
 | 
				
			||||||
 | 
					      path: 'sms',
 | 
				
			||||||
 | 
					      key: 'login',
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        phone,
 | 
				
			||||||
 | 
					        code,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      message.success('登录成功');
 | 
				
			||||||
 | 
					      set({ isAuthenticated: true });
 | 
				
			||||||
 | 
					      redirectToSuccess(config);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '登录失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  updateUser: async (data, opts) => {
 | 
				
			||||||
 | 
					    const config = get().config!;
 | 
				
			||||||
 | 
					    const res = await query.post({
 | 
				
			||||||
 | 
					      path: 'user',
 | 
				
			||||||
 | 
					      key: 'updateInfo',
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      message.success('更新成功');
 | 
				
			||||||
 | 
					      if (opts?.needRedirect) {
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          location.href = config?.loginSuccess;
 | 
				
			||||||
 | 
					        }, 1000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '更新失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  getUpdateUser: async () => {
 | 
				
			||||||
 | 
					    const res = await query.post({
 | 
				
			||||||
 | 
					      path: 'user',
 | 
				
			||||||
 | 
					      key: 'getUpdateInfo',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      set({ data: res.data });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '获取用户信息失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data: {},
 | 
				
			||||||
 | 
					  setData: (data) => set({ data }),
 | 
				
			||||||
 | 
					  loadedConfig: false,
 | 
				
			||||||
 | 
					  setLoadedConfig: (loadedConfig) => set({ loadedConfig }),
 | 
				
			||||||
 | 
					  config: null,
 | 
				
			||||||
 | 
					  setConfig: (config) => set({ config, loadedConfig: true }),
 | 
				
			||||||
 | 
					  loginByAccount: async (username, password) => {
 | 
				
			||||||
 | 
					    const config = get().config!;
 | 
				
			||||||
 | 
					    const isEmail = username.includes('@');
 | 
				
			||||||
 | 
					    const data: any = { password };
 | 
				
			||||||
 | 
					    if (isEmail) {
 | 
				
			||||||
 | 
					      data.email = username;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      data.username = username;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await queryLogin.login(data);
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      message.success('登录成功');
 | 
				
			||||||
 | 
					      set({ isAuthenticated: true });
 | 
				
			||||||
 | 
					      redirectToSuccess(config);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '登录失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  queryCheck: async () => {
 | 
				
			||||||
 | 
					    // const
 | 
				
			||||||
 | 
					    const userCheck = 'user-check';
 | 
				
			||||||
 | 
					    const url = new URL(location.href);
 | 
				
			||||||
 | 
					    const redirect = url.searchParams.get('redirect');
 | 
				
			||||||
 | 
					    const redirectUrl = redirect ? decodeURIComponent(redirect) : '';
 | 
				
			||||||
 | 
					    const checkKey = url.searchParams.get(userCheck);
 | 
				
			||||||
 | 
					    if (redirect && checkKey) {
 | 
				
			||||||
 | 
					      // 通过refresh_token 刷新token
 | 
				
			||||||
 | 
					      const me = await queryLogin.getMe();
 | 
				
			||||||
 | 
					      if (me.code === 200) {
 | 
				
			||||||
 | 
					        message.success('登录插件中...');
 | 
				
			||||||
 | 
					        const token = await queryLogin.cacheStore.getAccessToken();
 | 
				
			||||||
 | 
					        const newRedirectUrl = new URL(redirectUrl);
 | 
				
			||||||
 | 
					        newRedirectUrl.searchParams.set('token', token + '');
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          window.open(newRedirectUrl.toString(), '_blank');
 | 
				
			||||||
 | 
					        }, 2000);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // 刷新token失败,登陆页自己跳转
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    console.log('checkKey', checkKey, redirectUrl);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  loginByWechat: async (code) => {
 | 
				
			||||||
 | 
					    const config = get().config!;
 | 
				
			||||||
 | 
					    if (!code) {
 | 
				
			||||||
 | 
					      message.error('code is required');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await queryLogin.loginByWechat({ code });
 | 
				
			||||||
 | 
					    if (res.code === 200) {
 | 
				
			||||||
 | 
					      message.success('登录成功');
 | 
				
			||||||
 | 
					      redirectToSuccess(config);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      message.error(res.message || '登录失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
 | 
					type SimpleObject = {
 | 
				
			||||||
 | 
					  [key: string | number]: any;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare let DEV_SERVER: boolean;
 | 
				
			||||||
 | 
					declare let BASE_NAME: string;
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/wx/load-js.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/wx/load-js.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					// <script src="https://turing.captcha.qcloud.com/TCaptcha.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dynimicLoadTcapTcha = async (): Promise<boolean> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const script = document.createElement('script')
 | 
				
			||||||
 | 
					    script.type = 'text/javascript'
 | 
				
			||||||
 | 
					    script.id = 'tencent-captcha'
 | 
				
			||||||
 | 
					    if (document.getElementById('tencent-captcha')) {
 | 
				
			||||||
 | 
					      resolve(true)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    script.src = 'https://turing.captcha.qcloud.com/TCaptcha.js'
 | 
				
			||||||
 | 
					    script.onload = () => {
 | 
				
			||||||
 | 
					      resolve(true)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    script.onerror = (error) => {
 | 
				
			||||||
 | 
					      reject(error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    document.body.appendChild(script)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								src/wx/tencent-captcha.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/wx/tencent-captcha.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					// 定义回调函数
 | 
				
			||||||
 | 
					export function callback(res) {
 | 
				
			||||||
 | 
					  // 第一个参数传入回调结果,结果如下:
 | 
				
			||||||
 | 
					  // ret         Int       验证结果,0:验证成功。2:用户主动关闭验证码。
 | 
				
			||||||
 | 
					  // ticket      String    验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
 | 
				
			||||||
 | 
					  // CaptchaAppId       String    验证码应用ID。
 | 
				
			||||||
 | 
					  // bizState    Any       自定义透传参数。
 | 
				
			||||||
 | 
					  // randstr     String    本次验证的随机串,后续票据校验时需传递该参数。
 | 
				
			||||||
 | 
					  console.log('callback:', res);
 | 
				
			||||||
 | 
					  // res(用户主动关闭验证码)= {ret: 2, ticket: null}
 | 
				
			||||||
 | 
					  // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
 | 
				
			||||||
 | 
					  // res(请求验证码发生错误,验证码自动返回terror_前缀的容灾票据) = {ret: 0, ticket: "String", randstr: "String",  errorCode: Number, errorMessage: "String"}
 | 
				
			||||||
 | 
					  // 此处代码仅为验证结果的展示示例,真实业务接入,建议基于ticket和errorCode情况做不同的业务处理
 | 
				
			||||||
 | 
					  if (res.ret === 0) {
 | 
				
			||||||
 | 
					    // 复制结果至剪切板
 | 
				
			||||||
 | 
					    var str = '【randstr】->【' + res.randstr + '】      【ticket】->【' + res.ticket + '】';
 | 
				
			||||||
 | 
					    var ipt = document.createElement('input');
 | 
				
			||||||
 | 
					    ipt.value = str;
 | 
				
			||||||
 | 
					    document.body.appendChild(ipt);
 | 
				
			||||||
 | 
					    ipt.select();
 | 
				
			||||||
 | 
					    document.body.removeChild(ipt);
 | 
				
			||||||
 | 
					    alert('1. 返回结果(randstr、ticket)已复制到剪切板,ctrl+v 查看。 2. 打开浏览器控制台,查看完整返回结果。');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export type TencentCaptcha = {
 | 
				
			||||||
 | 
					  actionDuration?: number;
 | 
				
			||||||
 | 
					  appid?: string;
 | 
				
			||||||
 | 
					  bizState?: any;
 | 
				
			||||||
 | 
					  randstr?: string;
 | 
				
			||||||
 | 
					  ret: number;
 | 
				
			||||||
 | 
					  sid?: string;
 | 
				
			||||||
 | 
					  ticket?: string;
 | 
				
			||||||
 | 
					  errorCode?: number;
 | 
				
			||||||
 | 
					  errorMessage?: string;
 | 
				
			||||||
 | 
					  verifyDuration?: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					// 定义验证码触发事件
 | 
				
			||||||
 | 
					export const checkCaptcha = (captchaAppId: string): Promise<TencentCaptcha> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const callback = (res: TencentCaptcha) => {
 | 
				
			||||||
 | 
					      console.log('callback:', res);
 | 
				
			||||||
 | 
					      if (res.ret === 0) {
 | 
				
			||||||
 | 
					        resolve(res);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        reject(res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const appid = captchaAppId;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // 生成一个验证码对象
 | 
				
			||||||
 | 
					      // CaptchaAppId:登录验证码控制台,从【验证管理】页面进行查看。如果未创建过验证,请先新建验证。注意:不可使用客户端类型为小程序的CaptchaAppId,会导致数据统计错误。
 | 
				
			||||||
 | 
					      //callback:定义的回调函数
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      var captcha = new TencentCaptcha(appid, callback, {});
 | 
				
			||||||
 | 
					      // 调用方法,显示验证码
 | 
				
			||||||
 | 
					      captcha.show();
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // 加载异常,调用验证码js加载错误处理函数
 | 
				
			||||||
 | 
					      var ticket = 'terror_1001_' + appid + '_' + Math.floor(new Date().getTime() / 1000);
 | 
				
			||||||
 | 
					      // 生成容灾票据或自行做其它处理
 | 
				
			||||||
 | 
					      callback({
 | 
				
			||||||
 | 
					        ret: 0,
 | 
				
			||||||
 | 
					        randstr: '@' + Math.random().toString(36).substring(2),
 | 
				
			||||||
 | 
					        ticket: ticket,
 | 
				
			||||||
 | 
					        errorCode: 1001,
 | 
				
			||||||
 | 
					        errorMessage: 'jsload_error',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/wx/ws-login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/wx/ws-login.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					type WxLoginConfig = {
 | 
				
			||||||
 | 
					  redirect_uri?: string;
 | 
				
			||||||
 | 
					  appid?: string;
 | 
				
			||||||
 | 
					  scope?: string;
 | 
				
			||||||
 | 
					  state?: string;
 | 
				
			||||||
 | 
					  style?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const createLogin = async (config?: WxLoginConfig) => {
 | 
				
			||||||
 | 
					  let redirect_uri = config?.redirect_uri;
 | 
				
			||||||
 | 
					  const { appid } = config || {};
 | 
				
			||||||
 | 
					  if (!redirect_uri) {
 | 
				
			||||||
 | 
					    redirect_uri = new URL(window.location.href).origin + '/api/s1/wx/login';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!appid) {
 | 
				
			||||||
 | 
					    console.error('appid is not cant be empty');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // @ts-ignore
 | 
				
			||||||
 | 
					  const obj = new WxLogin({
 | 
				
			||||||
 | 
					    self_redirect: false,
 | 
				
			||||||
 | 
					    id: 'weixinLogin', // 需要显示的容器id
 | 
				
			||||||
 | 
					    appid: appid, // 微信开放平台appid wx*******
 | 
				
			||||||
 | 
					    scope: 'snsapi_login', // 网页默认即可 snsapi_userinfo
 | 
				
			||||||
 | 
					    redirect_uri: encodeURIComponent(redirect_uri), // 授权成功后回调的url
 | 
				
			||||||
 | 
					    state: Math.ceil(Math.random() * 1000), // 可设置为简单的随机数加session用来校验
 | 
				
			||||||
 | 
					    stylelite: true, // 是否使用简洁模式
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return obj;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const wxId = 'weixinLogin';
 | 
				
			||||||
 | 
					export function setWxerwma(config?: WxLoginConfig) {
 | 
				
			||||||
 | 
					  const s = document.createElement('script');
 | 
				
			||||||
 | 
					  s.type = 'text/javascript';
 | 
				
			||||||
 | 
					  s.src = '//res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js';
 | 
				
			||||||
 | 
					  s.id = 'weixinLogin-js';
 | 
				
			||||||
 | 
					  if (document.getElementById('weixinLogin-js')) {
 | 
				
			||||||
 | 
					    createLogin(config);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const wxElement = document.body.appendChild(s);
 | 
				
			||||||
 | 
					  wxElement.onload = function () {
 | 
				
			||||||
 | 
					    createLogin(config);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "jsx": "react-jsx",
 | 
				
			||||||
 | 
					    "target": "ES2020",
 | 
				
			||||||
 | 
					    "useDefineForClassFields": true,
 | 
				
			||||||
 | 
					    "lib": [
 | 
				
			||||||
 | 
					      "ES2020",
 | 
				
			||||||
 | 
					      "DOM",
 | 
				
			||||||
 | 
					      "DOM.Iterable"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    /* Bundler mode */
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "allowImportingTsExtensions": true,
 | 
				
			||||||
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "moduleDetection": "force",
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "baseUrl": "./",
 | 
				
			||||||
 | 
					    "typeRoots": [
 | 
				
			||||||
 | 
					      "node_modules/@types",
 | 
				
			||||||
 | 
					      "node_modules/@kevisual/types",
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@/*": [
 | 
				
			||||||
 | 
					        "src/*"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    /* Linting */
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "noImplicitAny": false,
 | 
				
			||||||
 | 
					    "noUnusedLocals": false,
 | 
				
			||||||
 | 
					    "noUnusedParameters": false,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": [
 | 
				
			||||||
 | 
					    "src",
 | 
				
			||||||
 | 
					    "typings.d.ts",
 | 
				
			||||||
 | 
					    "snippets"
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								vite.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								vite.config.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { defineConfig } from 'vite';
 | 
				
			||||||
 | 
					import basicSsl from '@vitejs/plugin-basic-ssl';
 | 
				
			||||||
 | 
					// import react from '@vitejs/plugin-react';
 | 
				
			||||||
 | 
					import dayjs from 'dayjs';
 | 
				
			||||||
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					import tailwindcss from '@tailwindcss/vite';
 | 
				
			||||||
 | 
					import pkgs from './package.json' with { type: 'json' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isDev = process.env.NODE_ENV === 'development';
 | 
				
			||||||
 | 
					const isWebDev = process.env.WEB_DEV === 'true';
 | 
				
			||||||
 | 
					const BUILD_TIME = dayjs().format('YYYY-MM-DD HH:mm:ss');
 | 
				
			||||||
 | 
					const basename = pkgs.basename;
 | 
				
			||||||
 | 
					let plugins = [tailwindcss()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!isWebDev) {
 | 
				
			||||||
 | 
					  // 在bolt的web开发环境下不需要ssl
 | 
				
			||||||
 | 
					  plugins.push(basicSsl());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  plugins: plugins,
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      '@': path.resolve(__dirname, './src'),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  base: isDev ? '/' : basename,
 | 
				
			||||||
 | 
					  define: {
 | 
				
			||||||
 | 
					    DEV_SERVER: JSON.stringify(isDev),
 | 
				
			||||||
 | 
					    BUILD_TIME: JSON.stringify(BUILD_TIME),
 | 
				
			||||||
 | 
					    BASE_NAME: JSON.stringify(basename),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  optimizeDeps: {
 | 
				
			||||||
 | 
					    // exclude: ['react'], // 排除 react 和 react-dom 以避免打包
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  // esbuild: {
 | 
				
			||||||
 | 
					  //   jsxFactory: 'h',
 | 
				
			||||||
 | 
					  //   jsxFragment: 'Fragment',
 | 
				
			||||||
 | 
					  // },
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    port: 6025,
 | 
				
			||||||
 | 
					    host: '0.0.0.0',
 | 
				
			||||||
 | 
					    proxy: {
 | 
				
			||||||
 | 
					      '/api': {
 | 
				
			||||||
 | 
					        target: 'https://kevisual.xiongxiao.me',
 | 
				
			||||||
 | 
					        changeOrigin: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Reference in New Issue
	
	Block a user