feat: complete TanStack dashboard acceptance
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
// 401 = 未认证:抹去凭证并引导到登录页。开发代理下 401 由后端直接返回。
|
||||
// 401 = 未认证:抹去凭证并通过 TanStack Router 监听的 history 事件回到登录页。
|
||||
const unauthorized = () => {
|
||||
localStorage.removeItem("creatorhub.auth");
|
||||
if (!location.hash.startsWith("#/login")) location.hash = "#/login";
|
||||
if (typeof window !== "undefined" && window.location.pathname !== "/login") {
|
||||
window.history.replaceState({}, "", "/login");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}
|
||||
return Object.assign(new Error("认证已过期,请重新登录"), { status: 401 });
|
||||
};
|
||||
|
||||
@@ -74,11 +77,11 @@ const withID = (record, fallback) => ({
|
||||
});
|
||||
|
||||
export const dataProvider = {
|
||||
// Refine v5 契约:所有方法收单个参数对象(getList 收 {resource, pagination, filters, meta})。
|
||||
// 页面数据层契约:所有方法收单个参数对象(getList 收 {resource, pagination, filters, meta})。
|
||||
async getList({ resource, pagination = {}, filters = [], meta = {} }) {
|
||||
const path = resourcePaths[resource];
|
||||
if (!path) return unsupported(resource, "getList");
|
||||
// filters 为 [{field, value, operator}];meta 是 Refine 透传的查询上下文。
|
||||
// filters 为 [{field, value, operator}];meta 是页面层透传的查询上下文。
|
||||
const valueOf = (field) => {
|
||||
const direct = meta?.[field];
|
||||
if (direct !== undefined) return direct;
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("dataProvider", () => {
|
||||
dataProvider.getList({ resource: "browsers" }),
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
expect(localStorage.getItem("creatorhub.auth")).toBeNull();
|
||||
expect(location.hash).toBe("#/login");
|
||||
expect(location.pathname).toBe("/login");
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createContext, createElement, useContext } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { dataProvider as defaultDataProvider } from '../api/dataProvider'
|
||||
|
||||
export const DataProviderContext = createContext(defaultDataProvider)
|
||||
|
||||
export function DataProviderProvider({ dataProvider, children }) {
|
||||
return createElement(DataProviderContext.Provider, { value: dataProvider }, children)
|
||||
}
|
||||
|
||||
const emptyList = { data: [], total: 0 }
|
||||
|
||||
function listKey({ resource, pagination, filters, meta }) {
|
||||
return ['creatorhub', 'list', resource, { pagination, filters, meta }]
|
||||
}
|
||||
|
||||
export function useList({ resource, pagination, filters, meta, queryOptions = {} }) {
|
||||
const provider = useContext(DataProviderContext)
|
||||
const query = useQuery({
|
||||
queryKey: listKey({ resource, pagination, filters, meta }),
|
||||
queryFn: () => provider.getList({ resource, pagination, filters, meta }),
|
||||
...queryOptions,
|
||||
})
|
||||
return {
|
||||
result: query.data ?? emptyList,
|
||||
query,
|
||||
error: query.error,
|
||||
isPending: query.isPending,
|
||||
}
|
||||
}
|
||||
|
||||
export function useOne({ resource, id, queryOptions = {} }) {
|
||||
const provider = useContext(DataProviderContext)
|
||||
const query = useQuery({
|
||||
queryKey: ['creatorhub', 'one', resource, id],
|
||||
queryFn: () => provider.getOne({ resource, id }),
|
||||
enabled: Boolean(id) && queryOptions.enabled !== false,
|
||||
...queryOptions,
|
||||
})
|
||||
return {
|
||||
result: query.data?.data,
|
||||
query,
|
||||
error: query.error,
|
||||
isPending: query.isPending,
|
||||
}
|
||||
}
|
||||
|
||||
export function useDataProvider() {
|
||||
const provider = useContext(DataProviderContext)
|
||||
return () => provider
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import {
|
||||
Link as TanStackLink,
|
||||
Outlet as TanStackOutlet,
|
||||
useLocation as useTanStackLocation,
|
||||
useNavigate as useTanStackNavigate,
|
||||
useParams as useTanStackParams,
|
||||
} from '@tanstack/react-router'
|
||||
|
||||
export const TestRouterContext = createContext(null)
|
||||
|
||||
export function Link({ to, ...props }) {
|
||||
const testRouter = useContext(TestRouterContext)
|
||||
if (testRouter) {
|
||||
return <a href={typeof to === 'string' ? to : '#'} data-router-link="true" {...props} onClick={(event) => {
|
||||
props.onClick?.(event)
|
||||
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||
event.preventDefault()
|
||||
testRouter.navigate({ to })
|
||||
}} />
|
||||
}
|
||||
return <TanStackLink to={to} data-router-link="true" {...props} />
|
||||
}
|
||||
|
||||
export function Outlet() {
|
||||
return <TanStackOutlet />
|
||||
}
|
||||
|
||||
export function useNavigate() {
|
||||
const testRouter = useContext(TestRouterContext)
|
||||
if (testRouter) return (to, options = {}) => testRouter.navigate(typeof to === 'string' ? { to, ...options } : to)
|
||||
const navigate = useTanStackNavigate()
|
||||
return (to, options = {}) => {
|
||||
if (typeof to === 'string') return navigate({ to, ...options })
|
||||
return navigate(to)
|
||||
}
|
||||
}
|
||||
|
||||
export function useLocation() {
|
||||
const testRouter = useContext(TestRouterContext)
|
||||
if (testRouter) return testRouter.location
|
||||
const location = useTanStackLocation()
|
||||
return {
|
||||
...location,
|
||||
pathname: location.pathname,
|
||||
search: location.searchStr,
|
||||
}
|
||||
}
|
||||
|
||||
export const RouterParamsContext = createContext(null)
|
||||
|
||||
export function useParams() {
|
||||
return useContext(RouterParamsContext) ?? useTanStackParams({ strict: false })
|
||||
}
|
||||
|
||||
export function useSearchParams() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const searchParams = new URLSearchParams(location.searchStr || '')
|
||||
const setSearchParams = (next, options = {}) => {
|
||||
const values = next instanceof URLSearchParams ? Object.fromEntries(next) : next
|
||||
return navigate({
|
||||
to: location.pathname,
|
||||
search: values,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
return [searchParams, setSearchParams]
|
||||
}
|
||||
Reference in New Issue
Block a user