feat: 项目接口联调

This commit is contained in:
JiaJun
2026-04-10 09:27:11 +08:00
parent 906fa63870
commit af3ed15ba2
62 changed files with 4307 additions and 982 deletions

View File

@@ -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
View 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
View 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
View 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
View 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
}