feat: api client, auth store, login, authed shell, data-table + CRUD kit

This commit is contained in:
2026-07-07 22:15:08 +08:00
parent 95a3dee29f
commit eb617ce436
23 changed files with 1002 additions and 352 deletions
+24
View File
@@ -0,0 +1,24 @@
import { toast } from 'sonner'
import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'
import { ApiError } from '@/api/client'
/**
* Standard error handling: 422 field errors land on the form (when given),
* everything else surfaces as a toast with the backend's localized message.
*/
export function handleApiError<T extends FieldValues>(
error: unknown,
setError?: UseFormSetError<T>,
): void {
if (error instanceof ApiError) {
if (error.status === 422 && error.errors && setError) {
for (const [field, messages] of Object.entries(error.errors)) {
setError(field as Path<T>, { type: 'server', message: messages[0] })
}
return
}
toast.error(error.message || `请求失败(${error.code}`)
return
}
toast.error('网络异常,请稍后重试')
}
+41
View File
@@ -0,0 +1,41 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import type { ListParams, PaginatedResponse } from '@/api/types'
/**
* Standard list-page state: page / page_size wired to the backend's
* Pagination envelope; changing filters snaps back to page 1.
*/
export function useListPage<T>(
key: string,
fetcher: (params: ListParams) => Promise<PaginatedResponse<T>>,
filters: ListParams = {},
) {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const filterKey = JSON.stringify(filters)
useEffect(() => {
setPage(1)
}, [filterKey])
const query = useQuery({
queryKey: [key, filterKey, page, pageSize],
queryFn: () => fetcher({ ...filters, page, page_size: pageSize }),
placeholderData: keepPreviousData,
})
return {
query,
rows: query.data?.data ?? [],
pagination: query.data?.pagination,
loading: query.isPending,
page,
setPage,
setPageSize: (size: number) => {
setPageSize(size)
setPage(1)
},
}
}