mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
perf: login module
This commit is contained in:
@@ -181,3 +181,5 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.DS_Store
|
||||
@@ -1,22 +1,24 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from apps.system.schemas.auth import LocalLoginSchema
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from common.core.deps import SessionDep
|
||||
from ..crud.user import authenticate
|
||||
from common.core.security import create_access_token
|
||||
from datetime import timedelta
|
||||
from common.core.config import settings
|
||||
from common.core.schemas import Token
|
||||
router = APIRouter(tags=["login"], prefix="/login")
|
||||
|
||||
router = APIRouter(tags=["login"])
|
||||
|
||||
@router.post("/localLogin")
|
||||
@router.post("/access-token")
|
||||
def local_login(
|
||||
session: SessionDep,
|
||||
schema: LocalLoginSchema,
|
||||
) -> str:
|
||||
user = authenticate(session=session, account=schema.account, password=schema.password)
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||
) -> Token:
|
||||
user = authenticate(session=session, account=form_data.username, password=form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect account or password")
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return create_access_token(
|
||||
user.id, expires_delta=access_token_expires
|
||||
)
|
||||
user_dict = user.to_dict()
|
||||
return Token(access_token=create_access_token(
|
||||
user_dict, expires_delta=access_token_expires
|
||||
))
|
||||
@@ -5,7 +5,5 @@ router = APIRouter(tags=["user"], prefix="/user")
|
||||
|
||||
|
||||
@router.get("/info")
|
||||
async def user_info():
|
||||
return {
|
||||
"name": "admin"
|
||||
}
|
||||
async def user_info(current_user: CurrentUser):
|
||||
return current_user.to_dict()
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from common.core.config import settings
|
||||
from common.core.deps import get_current_user
|
||||
|
||||
|
||||
|
||||
|
||||
async def verify_token(request: Request, call_next):
|
||||
print(request.url.path)
|
||||
if request.url.path == "/login/localLogin":
|
||||
return await call_next(request)
|
||||
try:
|
||||
key = settings.TOKEN_KEY
|
||||
token = request.headers.get(key)
|
||||
get_current_user(token)
|
||||
except (Exception):
|
||||
return HTTPException(status_code=401, detail="Invalid token")
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@@ -6,3 +6,10 @@ class sys_user(SQLModel, table=True):
|
||||
account: str = Field(max_length=255, unique=True)
|
||||
password: str = Field(max_length=255)
|
||||
oid: int = Field(default=1)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"account": self.account,
|
||||
"oid": self.oid
|
||||
}
|
||||
+12
-41
@@ -2,48 +2,26 @@ from typing import Annotated
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
# from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from pydantic import ValidationError
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlmodel import Session
|
||||
|
||||
from common.core.schemas import TokenPayload, XOAuth2PasswordBearer
|
||||
from common.core import security
|
||||
from common.core.config import settings
|
||||
from common.core.db import get_session
|
||||
from apps.system.models.user import sys_user
|
||||
import threading
|
||||
""" reusable_oauth2 = OAuth2PasswordBearer(
|
||||
reusable_oauth2 = XOAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
|
||||
)
|
||||
"""
|
||||
|
||||
thread_local = threading.local()
|
||||
|
||||
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
# TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||
TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||
|
||||
def get_current_user(session: SessionDep, token: str) -> sys_user:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||
)
|
||||
# token_data = TokenPayload(**payload)
|
||||
except (InvalidTokenError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = session.get(sys_user, payload.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
""" if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user") """
|
||||
thread_local.current_user = user
|
||||
return user
|
||||
def get_current_user_from_thread():
|
||||
return getattr(thread_local, "current_user", None)
|
||||
CurrentUser = Annotated[sys_user, Depends(get_current_user_from_thread)]
|
||||
""" def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
def get_current_user(session: SessionDep, token: TokenDep) -> sys_user:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||
@@ -54,20 +32,13 @@ CurrentUser = Annotated[sys_user, Depends(get_current_user_from_thread)]
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = session.get(User, token_data.sub)
|
||||
user = session.get(sys_user, token_data.id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
""" if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user") """
|
||||
return user
|
||||
CurrentUser = Annotated[sys_user, Depends(get_current_user)]
|
||||
|
||||
|
||||
CurrentUser = Annotated[User, Depends(get_current_user)]
|
||||
|
||||
|
||||
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
return current_user """
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import SQLModel
|
||||
from starlette.status import HTTP_403_FORBIDDEN, HTTP_401_UNAUTHORIZED
|
||||
from common.core.config import settings
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
class TokenPayload(BaseModel):
|
||||
account: str | None = None
|
||||
id: int | None = None
|
||||
oid: int | None = None
|
||||
|
||||
class Token(SQLModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
class XOAuth2PasswordBearer(OAuth2PasswordBearer):
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get(settings.TOKEN_KEY)
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
@@ -12,9 +12,11 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def create_access_token(subject: str | Any, expires_delta: timedelta) -> str:
|
||||
def create_access_token(data: dict | Any, expires_delta: timedelta) -> str:
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode = {"exp": expire, "account": str(subject)}
|
||||
to_encode.update({"exp": expire})
|
||||
# to_encode = {"exp": expire, "account": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import sentry_sdk
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from apps.api import api_router
|
||||
from common.core.config import settings
|
||||
|
||||
from apps.system.middleware.token import verify_token
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
return f"{route.tags[0]}-{route.name}"
|
||||
@@ -32,7 +30,6 @@ if settings.all_cors_origins:
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
# app.add_middleware(BaseHTTPMiddleware, dispatch=verify_token)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_API_BASE_URL=http://localhost:8000/api/v1
|
||||
VITE_APP_TITLE=SQLBot (Development)
|
||||
@@ -1,2 +1,2 @@
|
||||
VITE_API_BASE_URL=https://api.SQLBot.com
|
||||
VITE_API_BASE_URL=./
|
||||
VITE_APP_TITLE=SQLBot
|
||||
Vendored
+1
-1
@@ -6,5 +6,5 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.5.0",
|
||||
"web-storage-cache": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
export const AuthApi = {
|
||||
login: (credentials: { username: string; password: string }) => request.post<{
|
||||
data: any; token: string
|
||||
}>('/login/access-token', credentials, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
requestOptions: {
|
||||
rawResponse: true,
|
||||
}
|
||||
}),
|
||||
logout: () => request.post('/auth/logout')
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import chat from '@/views/chat/index.vue'
|
||||
import ds from '@/views/ds/index.vue'
|
||||
import dashboard from '@/views/dashboard/index.vue'
|
||||
import setting from '@/views/setting/index.vue'
|
||||
import { watchRouter } from './watch'
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
@@ -68,5 +69,5 @@ const router = createRouter({
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watchRouter(router)
|
||||
export default router
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
// 不需要验证token的白名单路由
|
||||
const whiteList = ['/login']
|
||||
export const watchRouter = (router: any) => {
|
||||
router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
// 获取token
|
||||
const token = wsCache.get('user.token')
|
||||
// 如果没有token
|
||||
if (!token) {
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
ElMessage.error('请先登录')
|
||||
next('/login')
|
||||
}
|
||||
return
|
||||
}
|
||||
// 处理根路径重定向
|
||||
if (to.path === '/') {
|
||||
next('/chat')
|
||||
return
|
||||
}
|
||||
// 已有token和用户信息,允许访问
|
||||
if (to.path === '/login') {
|
||||
next('/chat')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { AuthApi } from '@/api/login'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref('')
|
||||
const username = ref('')
|
||||
|
||||
const login = async (formData: { username: string; password: string }) => {
|
||||
token.value = 'mock-token'
|
||||
username.value = formData.username
|
||||
return Promise.resolve()
|
||||
const res = await AuthApi.login(formData)
|
||||
token.value = res.data.access_token
|
||||
wsCache.set('user.token', token.value)
|
||||
username.value = formData.username
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
token.value = ''
|
||||
username.value = ''
|
||||
wsCache.delete('user.token')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
// src/services/request.ts
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
type InternalAxiosRequestConfig,
|
||||
type CancelTokenSource
|
||||
} from 'axios'
|
||||
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
// Response data structure
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
success: boolean
|
||||
[key: string]: any // Allow additional fields
|
||||
}
|
||||
|
||||
// Extended request options
|
||||
export interface RequestOptions {
|
||||
silent?: boolean // Silent mode (no error alerts)
|
||||
rawResponse?: boolean // Return raw Axios response
|
||||
customError?: boolean // Custom error handling
|
||||
retryCount?: number // Number of retry attempts
|
||||
}
|
||||
|
||||
// Merged request configuration
|
||||
export interface FullRequestConfig extends AxiosRequestConfig {
|
||||
requestOptions?: RequestOptions
|
||||
}
|
||||
|
||||
// Custom error type
|
||||
export interface RequestError<T = any> extends Error {
|
||||
config: FullRequestConfig
|
||||
code?: string
|
||||
request?: any
|
||||
response?: AxiosResponse<T>
|
||||
isAxiosError: boolean
|
||||
}
|
||||
|
||||
class HttpService {
|
||||
private instance: AxiosInstance
|
||||
private cancelTokenSource: CancelTokenSource
|
||||
|
||||
constructor(config?: AxiosRequestConfig) {
|
||||
this.cancelTokenSource = axios.CancelToken.source()
|
||||
this.instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config?.headers
|
||||
},
|
||||
...config
|
||||
})
|
||||
|
||||
this.setupInterceptors()
|
||||
}
|
||||
|
||||
private setupInterceptors() {
|
||||
// Request interceptor
|
||||
this.instance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// Add auth token
|
||||
const token = wsCache.get('user.token')
|
||||
if (token && config.headers) {
|
||||
config.headers['X-SQLBOT_TOKEN'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// Request logging
|
||||
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`)
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor
|
||||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
console.log(`[Response] ${response.config.url}`, response.data)
|
||||
|
||||
// Return raw response if configured
|
||||
if ((response.config as FullRequestConfig).requestOptions?.rawResponse) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Handle business logic
|
||||
if (response.data?.success !== true) {
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
|
||||
return response.data.data
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const config = error.config as FullRequestConfig & { __retryCount?: number }
|
||||
const requestOptions = config?.requestOptions || {}
|
||||
|
||||
// Retry logic for specific status codes
|
||||
const shouldRetry =
|
||||
error.response?.status === 502 &&
|
||||
(config.__retryCount || 0) < (requestOptions.retryCount || 3)
|
||||
|
||||
if (shouldRetry) {
|
||||
config.__retryCount = (config.__retryCount || 0) + 1
|
||||
|
||||
// Exponential backoff
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, 1000 * (config.__retryCount || 1))
|
||||
)
|
||||
|
||||
return this.instance.request(config)
|
||||
}
|
||||
|
||||
// Unified error handling
|
||||
if (!requestOptions.customError && !requestOptions.silent) {
|
||||
this.handleError(error)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private handleError(error: AxiosError) {
|
||||
let errorMessage = 'Request error'
|
||||
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
errorMessage = 'Invalid request parameters'
|
||||
break
|
||||
case 401:
|
||||
errorMessage = 'Unauthorized, please login again'
|
||||
// Redirect to login page if needed
|
||||
break
|
||||
case 403:
|
||||
errorMessage = 'Access denied'
|
||||
break
|
||||
case 404:
|
||||
errorMessage = 'Resource not found'
|
||||
break
|
||||
case 500:
|
||||
errorMessage = 'Server error'
|
||||
break
|
||||
default:
|
||||
errorMessage = `Server responded with error: ${error.response.status}`
|
||||
}
|
||||
} else if (error.request) {
|
||||
errorMessage = 'No response from server'
|
||||
} else if (axios.isCancel(error)) {
|
||||
errorMessage = 'Request canceled'
|
||||
return // Skip showing cancel messages
|
||||
} else {
|
||||
errorMessage = error['message'] || 'Unknown error'
|
||||
}
|
||||
|
||||
// Show error using UI library (e.g., Element Plus, Ant Design)
|
||||
console.error(errorMessage)
|
||||
// ElMessage.error(errorMessage)
|
||||
ElMessage({
|
||||
message: errorMessage,
|
||||
type: 'error',
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
|
||||
// Cancel all pending requests
|
||||
public cancelRequests(message?: string) {
|
||||
this.cancelTokenSource.cancel(message)
|
||||
// Create new token source for future requests
|
||||
this.cancelTokenSource = axios.CancelToken.source()
|
||||
}
|
||||
|
||||
// Base request method
|
||||
public request<T = any>(config: FullRequestConfig): Promise<T> {
|
||||
return this.instance.request({
|
||||
cancelToken: this.cancelTokenSource.token,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// GET request
|
||||
public get<T = any>(url: string, config?: FullRequestConfig): Promise<T> {
|
||||
return this.request({ ...config, method: 'GET', url })
|
||||
}
|
||||
|
||||
// POST request
|
||||
public post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: FullRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request({ ...config, method: 'POST', url, data })
|
||||
}
|
||||
|
||||
// PUT request
|
||||
public put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: FullRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request({ ...config, method: 'PUT', url, data })
|
||||
}
|
||||
|
||||
// DELETE request
|
||||
public delete<T = any>(url: string, config?: FullRequestConfig): Promise<T> {
|
||||
return this.request({ ...config, method: 'DELETE', url })
|
||||
}
|
||||
|
||||
// PATCH request
|
||||
public patch<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: FullRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request({ ...config, method: 'PATCH', url, data })
|
||||
}
|
||||
|
||||
// File upload
|
||||
public upload<T = any>(
|
||||
url: string,
|
||||
file: File,
|
||||
fieldName = 'file',
|
||||
config?: FullRequestConfig
|
||||
): Promise<T> {
|
||||
const formData = new FormData()
|
||||
formData.append(fieldName, file)
|
||||
|
||||
return this.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// Download file
|
||||
public download(
|
||||
url: string,
|
||||
config?: FullRequestConfig
|
||||
): Promise<Blob> {
|
||||
return this.request<Blob>({
|
||||
...config,
|
||||
method: 'GET',
|
||||
url,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const request = new HttpService({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 配置浏览器本地存储的方式,可直接存储对象数组。
|
||||
*/
|
||||
|
||||
import WebStorageCache from 'web-storage-cache'
|
||||
|
||||
type CacheType = 'sessionStorage' | 'localStorage'
|
||||
|
||||
export const useCache = (type: CacheType = 'localStorage') => {
|
||||
const wsCache: WebStorageCache = new WebStorageCache({
|
||||
storage: type
|
||||
})
|
||||
|
||||
return {
|
||||
wsCache
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user