Browse Source

feat: 添加认证相关功能和前端配置

web
hyh 3 months ago
parent
commit
fd14a6acfc
  1. 237
      src/CellularManagement.WebUI/package-lock.json
  2. 1
      src/CellularManagement.WebUI/package.json
  3. 24
      src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx
  4. 61
      src/CellularManagement.WebUI/src/config/http-client.config.ts
  5. 6
      src/CellularManagement.WebUI/src/constants/auth.ts
  6. 114
      src/CellularManagement.WebUI/src/hooks/useAuth.ts
  7. 101
      src/CellularManagement.WebUI/src/lib/http-client.ts
  8. 57
      src/CellularManagement.WebUI/src/services/authService.ts
  9. 26
      src/CellularManagement.WebUI/src/types/auth.ts
  10. 32
      src/CellularManagement.WebUI/src/types/env.d.ts

237
src/CellularManagement.WebUI/package-lock.json

@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.323.0",
@ -2321,6 +2322,11 @@
"node": ">=8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -2358,6 +2364,16 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2425,6 +2441,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
@ -2547,6 +2575,17 @@
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
@ -2620,6 +2659,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -2659,6 +2706,19 @@
"node": ">=6.0.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -2675,6 +2735,47 @@
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
@ -3046,6 +3147,25 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
@ -3061,6 +3181,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
@ -3110,6 +3244,29 @@
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz",
@ -3118,6 +3275,18 @@
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
@ -3201,6 +3370,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
@ -3221,6 +3401,31 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
@ -3525,6 +3730,14 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
@ -3545,6 +3758,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz",
@ -3956,6 +4188,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",

1
src/CellularManagement.WebUI/package.json

@ -15,6 +15,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.323.0",

24
src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx

@ -1,17 +1,15 @@
import { useState } from 'react';
import { DEFAULT_CREDENTIALS } from '@/constants/auth';
import { useAuth } from '@/hooks/useAuth';
interface LoginFormProps {
onSubmit: (username: string, password: string) => void;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
export function LoginForm() {
const [username, setUsername] = useState(DEFAULT_CREDENTIALS.username);
const [password, setPassword] = useState(DEFAULT_CREDENTIALS.password);
const { login, isLoading, error } = useAuth();
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
onSubmit(username, password);
await login({ username, password });
};
return (
@ -29,6 +27,7 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
onChange={(e) => setUsername(e.target.value)}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="请输入用户名或邮箱"
disabled={isLoading}
/>
</div>
<div>
@ -43,14 +42,21 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="请输入密码"
disabled={isLoading}
/>
</div>
{error && (
<div className="text-sm text-red-500">
{error}
</div>
)}
</div>
<button
type="submit"
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50"
disabled={isLoading}
>
{isLoading ? '登录中...' : '登录'}
</button>
</form>
);

61
src/CellularManagement.WebUI/src/config/http-client.config.ts

@ -0,0 +1,61 @@
import { HttpClientConfig } from '@/lib/http-client';
export class HttpClientConfigManager {
private static instance: HttpClientConfigManager;
private config: HttpClientConfig;
private constructor() {
this.config = {
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5202/api',
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 10000,
headers: {
'Content-Type': 'application/json',
'X-API-Version': import.meta.env.VITE_API_VERSION || 'v1',
},
};
}
public static getInstance(): HttpClientConfigManager {
if (!HttpClientConfigManager.instance) {
HttpClientConfigManager.instance = new HttpClientConfigManager();
}
return HttpClientConfigManager.instance;
}
public getConfig(): HttpClientConfig {
return { ...this.config };
}
public updateConfig(newConfig: Partial<HttpClientConfig>): void {
this.config = {
...this.config,
...newConfig,
headers: {
...this.config.headers,
...newConfig.headers,
},
};
}
public setBaseURL(baseURL: string): void {
this.config.baseURL = baseURL;
}
public setTimeout(timeout: number): void {
this.config.timeout = timeout;
}
public setHeader(key: string, value: string): void {
if (!this.config.headers) {
this.config.headers = {};
}
this.config.headers[key] = value;
}
public removeHeader(key: string): void {
if (this.config.headers) {
const { [key]: removed, ...headers } = this.config.headers;
this.config.headers = headers;
}
}
}

6
src/CellularManagement.WebUI/src/constants/auth.ts

@ -1,5 +1,5 @@
export const DEFAULT_CREDENTIALS = {
username: 'admin',
email: 'admin@example.com',
password: 'admin123'
username: 'zhangsan2024',
email: 'Zhangsan@2024',
password: 'Zhangsan@2024'
};

114
src/CellularManagement.WebUI/src/hooks/useAuth.ts

@ -1,53 +1,83 @@
import { useState, useEffect } from 'react';
import type { Permission } from '@/constants/menuConfig';
interface User {
id: string;
name: string;
email: string;
permissions: Permission[];
}
import { useState, useCallback } from 'react';
import { authService } from '@/services/authService';
import { LoginRequest, LoginResponse } from '@/types/auth';
import { useNavigate } from 'react-router-dom';
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 这里应该从你的认证系统获取用户信息
// 例如从localStorage、API或状态管理库中获取
const fetchUser = async () => {
try {
// 示例:从localStorage获取用户信息
const userData = localStorage.getItem('user');
if (userData) {
setUser(JSON.parse(userData));
}
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
const login = useCallback(async (request: LoginRequest) => {
setIsLoading(true);
setError(null);
try {
const result = await authService.login(request);
if (result.isSuccess && result.data) {
localStorage.setItem('accessToken', result.data.accessToken);
localStorage.setItem('refreshToken', result.data.refreshToken);
navigate('/');
return result.data;
} else {
setError(result.errorMessages?.[0] || '登录失败,请检查用户名和密码');
return null;
}
};
} catch (err) {
setError('登录请求失败,请稍后重试');
return null;
} finally {
setIsLoading(false);
}
}, [navigate]);
fetchUser();
}, []);
const logout = useCallback(async () => {
try {
await authService.logout();
navigate('/login');
} catch (err) {
setError('登出失败,请稍后重试');
}
}, [navigate]);
const login = async (userData: User) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const refreshToken = useCallback(async () => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
return null;
}
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
try {
const result = await authService.refreshToken(refreshToken);
if (result.isSuccess && result.data) {
localStorage.setItem('accessToken', result.data.accessToken);
localStorage.setItem('refreshToken', result.data.refreshToken);
return result.data;
}
return null;
} catch (err) {
return null;
}
}, []);
const getCurrentUser = useCallback(async () => {
try {
const result = await authService.getCurrentUser();
if (result.isSuccess && result.data) {
return result.data.user;
}
return null;
} catch (err) {
return null;
}
}, []);
return {
user,
loading,
userPermissions: user?.permissions || [],
isAuthenticated: !!user,
isLoading,
error,
login,
logout
logout,
refreshToken,
getCurrentUser,
};
}

101
src/CellularManagement.WebUI/src/lib/http-client.ts

@ -0,0 +1,101 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { HttpClientConfigManager } from '@/config/http-client.config';
// 创建HTTP客户端配置接口
export interface HttpClientConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
}
// 创建HTTP客户端类
export class HttpClient {
private instance: AxiosInstance;
private configManager: HttpClientConfigManager;
constructor(config?: Partial<HttpClientConfig>) {
this.configManager = HttpClientConfigManager.getInstance();
if (config) {
this.configManager.updateConfig(config);
}
const currentConfig = this.configManager.getConfig();
this.instance = axios.create(currentConfig);
this.setupInterceptors();
}
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
if (error.response) {
// 处理401错误(未授权)
if (error.response.status === 401) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
return Promise.reject(error.response.data);
}
return Promise.reject(error);
}
);
}
// 更新配置
public updateConfig(config: Partial<HttpClientConfig>): void {
this.configManager.updateConfig(config);
const currentConfig = this.configManager.getConfig();
this.instance.defaults.baseURL = currentConfig.baseURL;
this.instance.defaults.timeout = currentConfig.timeout;
if (currentConfig.headers) {
Object.entries(currentConfig.headers).forEach(([key, value]) => {
this.instance.defaults.headers.common[key] = value;
});
}
}
// 封装GET请求
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.get<T>(url, config);
return response.data;
}
// 封装POST请求
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.post<T>(url, data, config);
return response.data;
}
// 封装PUT请求
public async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.put<T>(url, data, config);
return response.data;
}
// 封装DELETE请求
public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.delete<T>(url, config);
return response.data;
}
}
// 创建默认HTTP客户端实例
export const httpClient = new HttpClient();

57
src/CellularManagement.WebUI/src/services/authService.ts

@ -0,0 +1,57 @@
import { LoginRequest, LoginResponse, OperationResult } from '@/types/auth';
import { httpClient } from '@/lib/http-client';
export const authService = {
async login(request: LoginRequest): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.post<OperationResult<LoginResponse>>('/Auth/Login', request);
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['登录请求失败,请稍后重试'],
data: null,
isSuccess: false,
};
}
},
// 刷新token
async refreshToken(refreshToken: string): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.post<OperationResult<LoginResponse>>('/Auth/RefreshToken', {
refreshToken,
});
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['刷新令牌失败,请重新登录'],
data: null,
isSuccess: false,
};
}
},
// 登出
async logout(): Promise<void> {
try {
await httpClient.post('/Auth/Logout');
} finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
},
// 获取当前用户信息
async getCurrentUser(): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.get<OperationResult<LoginResponse>>('/Auth/CurrentUser');
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['获取用户信息失败'],
data: null,
isSuccess: false,
};
}
},
};

26
src/CellularManagement.WebUI/src/types/auth.ts

@ -0,0 +1,26 @@
export interface User {
id: string;
userName: string;
email: string;
phoneNumber: string;
roles: string[];
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
expiresAt: string;
user: User;
}
export interface OperationResult<T> {
successMessage: string | null;
errorMessages: string[] | null;
data: T | null;
isSuccess: boolean;
}
export interface LoginRequest {
username: string;
password: string;
}

32
src/CellularManagement.WebUI/src/types/env.d.ts

@ -0,0 +1,32 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// API配置
readonly VITE_API_BASE_URL: string;
readonly VITE_API_TIMEOUT: string;
readonly VITE_API_VERSION: string;
// 应用配置
readonly VITE_APP_TITLE: string;
readonly VITE_APP_DESCRIPTION: string;
// 认证配置
readonly VITE_AUTH_TOKEN_KEY: string;
readonly VITE_AUTH_REFRESH_TOKEN_KEY: string;
readonly VITE_AUTH_TOKEN_EXPIRES_KEY: string;
// 其他配置
readonly VITE_ENABLE_MOCK: string;
readonly VITE_MOCK_DELAY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 在代码中使用环境变量
import { envConfig } from '@/config/env';
console.log(envConfig.API_BASE_URL); // 从环境变量获取
console.log(envConfig.API_TIMEOUT); // 自动转换为数字
console.log(envConfig.API_VERSION); // 从环境变量获取
Loading…
Cancel
Save