feat: 项目接口联调
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export * from './request'
|
||||
export * from './tool'
|
||||
|
||||
15
src/lib/query.ts
Normal file
15
src/lib/query.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
10
src/lib/queryKeys.ts
Normal file
10
src/lib/queryKeys.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type {goodsType} from '@/types/business.type.ts'
|
||||
|
||||
export const queryKeys = {
|
||||
authBootstrap: ['auth-bootstrap'] as const,
|
||||
goodsCatalog: (types?: readonly goodsType[]) => ['goods-catalog', ...(types ?? ['all'])] as const,
|
||||
assets: (sessionId: string) => ['assets', sessionId] as const,
|
||||
addressList: (sessionId: string) => ['address-list', sessionId] as const,
|
||||
orders: (sessionId: string) => ['orders', sessionId] as const,
|
||||
pointsLogs: (sessionId: string) => ['points-logs', sessionId] as const,
|
||||
}
|
||||
183
src/lib/request.ts
Normal file
183
src/lib/request.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import ky, {HTTPError, type Input, type KyInstance, type Options} from 'ky'
|
||||
|
||||
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
||||
import {useUserStore} from '@/store/user.ts'
|
||||
|
||||
type RequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
||||
type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'response'
|
||||
type TokenFormatter = (token: string) => string
|
||||
|
||||
export type RequestResult<T, R extends ResponseType = 'json'> =
|
||||
R extends 'text' ? string
|
||||
: R extends 'blob' ? Blob
|
||||
: R extends 'arrayBuffer' ? ArrayBuffer
|
||||
: R extends 'formData' ? FormData
|
||||
: R extends 'response' ? Response
|
||||
: T
|
||||
|
||||
export type RequestConfig<R extends ResponseType = 'json'> = Omit<Options, 'hooks' | 'method'> & {
|
||||
responseType?: R
|
||||
}
|
||||
|
||||
type ApiEnvelope = {
|
||||
code: number
|
||||
msg?: string
|
||||
}
|
||||
|
||||
export class RequestError<T = unknown> extends Error {
|
||||
status?: number
|
||||
data?: T
|
||||
|
||||
constructor(message: string, options?: {status?: number; data?: T; cause?: unknown}) {
|
||||
super(message, {cause: options?.cause})
|
||||
this.name = 'RequestError'
|
||||
this.status = options?.status
|
||||
this.data = options?.data
|
||||
}
|
||||
}
|
||||
|
||||
const isApiEnvelope = (value: unknown): value is ApiEnvelope =>
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'code' in value &&
|
||||
typeof (value as {code?: unknown}).code === 'number'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api/'
|
||||
const REQUEST_TIMEOUT = 10_000
|
||||
|
||||
let accessTokenFormatter: TokenFormatter = (token) => `Bearer ${token}`
|
||||
|
||||
export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
|
||||
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
||||
}
|
||||
|
||||
const requestClient = ky.create({
|
||||
baseUrl: API_BASE_URL,
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
retry: 0,
|
||||
headers: {
|
||||
lang: 'zh',
|
||||
},
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
({request}) => {
|
||||
const token = useUserStore.getState().userInfo?.token
|
||||
if (!token) {
|
||||
return
|
||||
}
|
||||
|
||||
request.headers.set('Authorization', accessTokenFormatter(token))
|
||||
},
|
||||
],
|
||||
beforeError: [
|
||||
async ({error}) => {
|
||||
if (error instanceof HTTPError) {
|
||||
const message = resolveToastMessage(
|
||||
error.data,
|
||||
typeof error.data === 'string'
|
||||
? error.data
|
||||
: error.response.statusText || 'Request failed',
|
||||
)
|
||||
|
||||
notifyError(message)
|
||||
|
||||
return new RequestError(message, {
|
||||
status: error.response.status,
|
||||
data: error.data,
|
||||
cause: error,
|
||||
})
|
||||
}
|
||||
|
||||
const message = error.message || 'Network request failed'
|
||||
notifyError(message)
|
||||
return new RequestError(message, {
|
||||
cause: error,
|
||||
})
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const isEmptyResponse = (response: Response) =>
|
||||
response.status === 204 ||
|
||||
response.status === 205 ||
|
||||
response.headers.get('content-length') === '0'
|
||||
|
||||
async function parseResponse<T, R extends ResponseType>(
|
||||
response: Response,
|
||||
responseType: R,
|
||||
): Promise<RequestResult<T, R>> {
|
||||
if (responseType === 'response') {
|
||||
return response as RequestResult<T, R>
|
||||
}
|
||||
|
||||
if (isEmptyResponse(response)) {
|
||||
return undefined as RequestResult<T, R>
|
||||
}
|
||||
|
||||
switch (responseType) {
|
||||
case 'text':
|
||||
return (await response.text()) as RequestResult<T, R>
|
||||
case 'blob':
|
||||
return (await response.blob()) as RequestResult<T, R>
|
||||
case 'arrayBuffer':
|
||||
return (await response.arrayBuffer()) as RequestResult<T, R>
|
||||
case 'formData':
|
||||
return (await response.formData()) as RequestResult<T, R>
|
||||
case 'json':
|
||||
default:
|
||||
{
|
||||
const payload = await response.json()
|
||||
|
||||
if (isApiEnvelope(payload) && payload.code !== 1 && payload.code !== 200) {
|
||||
const message = payload.msg?.trim() || 'Request failed'
|
||||
notifyError(message)
|
||||
throw new RequestError(message, {
|
||||
status: response.status,
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
return payload as RequestResult<T, R>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T = unknown, R extends ResponseType = 'json'>(
|
||||
method: RequestMethod,
|
||||
input: Input,
|
||||
config?: RequestConfig<R>,
|
||||
): Promise<RequestResult<T, R>> {
|
||||
const responseType = (config?.responseType ?? 'json') as R
|
||||
const options = config ? {...config} : {}
|
||||
delete options.responseType
|
||||
const response = await requestClient(input, {
|
||||
...options,
|
||||
method,
|
||||
})
|
||||
|
||||
return parseResponse<T, R>(response, responseType)
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||
request<T, R>('get', input, config),
|
||||
post: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||
request<T, R>('post', input, config),
|
||||
put: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||
request<T, R>('put', input, config),
|
||||
patch: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||
request<T, R>('patch', input, config),
|
||||
delete: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||
request<T, R>('delete', input, config),
|
||||
request: <T = unknown, R extends ResponseType = 'json'>(
|
||||
method: RequestMethod,
|
||||
input: Input,
|
||||
config?: RequestConfig<R>,
|
||||
): Promise<RequestResult<T, R>> => request<T, R>(method, input, config),
|
||||
create: (options?: Options): KyInstance => requestClient.extend(options ?? {}),
|
||||
}
|
||||
|
||||
export {requestClient}
|
||||
|
||||
export default http
|
||||
28
src/lib/tool.ts
Normal file
28
src/lib/tool.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
type FormDataValue = string | number | boolean | Blob
|
||||
|
||||
type FormDataRecordValue =
|
||||
| FormDataValue
|
||||
| null
|
||||
| undefined
|
||||
| FormDataValue[]
|
||||
|
||||
export function objectToFormData(data: Record<string, FormDataRecordValue>): FormData {
|
||||
const formData = new FormData()
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
formData.append(key, item instanceof Blob ? item : String(item))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
formData.append(key, value instanceof Blob ? value : String(value))
|
||||
})
|
||||
|
||||
return formData
|
||||
}
|
||||
Reference in New Issue
Block a user