diff --git a/AGENTS.md b/AGENTS.md index c878589..6b64f68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,19 +25,36 @@ src/ ``` pages/page-app/ ├── components/ # 模块专属组件 -├── store/ # 模块状态管理 -└── module/ # 模块功能函数 +├── hooks/ # 模块 React Query hooks(API 查询封装) +├── modules/ # 模块功能函数(UI 组件、工具函数等) +└── store/ # 模块状态管理(Zustand) ``` +### hooks/ 文件夹说明 + +每个模块的 `hooks/` 文件夹用于封装与该模块相关的 React Query hooks: + +- **use-api-query.ts**: 使用 `@tanstack/react-query` 的 `useQuery` 封装 API 调用 + - 定义 `queryKeys` 常量用于缓存标识 + - 封装 `useQuery` hooks 用于数据获取(GET 请求) + - 封装 `useMutation` hooks 用于数据修改(POST/PUT/DELETE 请求) + - 支持预取(prefetch)和无限滚动(infinite query) +- **index.ts**: 导出模块所有 hooks,便于统一导入使用 + ### 状态和数据获取 +- **@tanstack/react-query** 用于数据获取、缓存和状态管理 + - 在模块的 `hooks/` 文件夹中封装 API 调用 + - QueryClient 实例位于 `src/modules/query.ts` + - 在 `src/routes/__root.tsx` 中通过 `QueryClientProvider` 提供 - **Zustand** 用于全局状态管理 -- **@kevisual/query** 用于数据获取(QueryClient 实例位于 `src/modules/query.ts`) +- **@kevisual/query** 用于底层 API 请求封装 - **React Hook Form** 用于表单管理 ## 核心依赖 - **@base-ui/react**: Headless UI 基础组件 +- **@tanstack/react-query**: 数据获取、缓存和状态管理(配合 hooks/ 使用) - **@tanstack/react-router**: 基于 TanStack Router 插件的文件路由 - **class-variance-authority**: 基于变体的样式系统 - **clsx + tailwind-merge**: 通过 `cn()` 提供 className 工具函数 diff --git a/package.json b/package.json index 9ecbd15..64e9588 100644 --- a/package.json +++ b/package.json @@ -13,53 +13,55 @@ "dist" ], "dependencies": { - "@base-ui/react": "^1.2.0", - "@kevisual/router": "0.1.1", - "@tanstack/react-router": "^1.166.7", + "@base-ui/react": "^1.3.0", + "@kevisual/router": "0.1.6", + "@tanstack/react-query": "^5.91.0", + "@tanstack/react-router": "^1.167.4", "@tanstack/react-table": "^8.21.3", "@uiw/react-codemirror": "^4.25.8", "@uiw/react-md-editor": "^4.0.11", - "antd": "^6.3.2", + "antd": "^6.3.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "eruda": "^3.4.3", "es-toolkit": "^1.45.1", "fuse.js": "^7.1.0", "idb-keyval": "^6.2.2", "lucide-react": "^0.577.0", - "nanoid": "^5.1.6", + "nanoid": "^5.1.7", "next-themes": "^0.4.6", "react": "19.2.4", "react-dom": "19.2.4", "react-hook-form": "^7.71.2", - "react-resizable-panels": "^4.7.2", + "react-resizable-panels": "^4.7.3", "sonner": "^2.0.7", "valtio": "^2.3.1", "zod": "^4.3.6", - "zustand": "^5.0.11" + "zustand": "^5.0.12" }, "devDependencies": { "@kevisual/ai": "^0.0.28", - "@kevisual/api": "^0.0.62", + "@kevisual/api": "^0.0.64", "@kevisual/context": "^0.0.8", "@kevisual/js-filter": "^0.0.6", - "@kevisual/kv-login": "^0.1.17", - "@kevisual/query": "^0.0.53", + "@kevisual/kv-login": "^0.1.18", + "@kevisual/query": "^0.0.54", "@kevisual/types": "^0.0.12", - "@tailwindcss/vite": "^4.2.1", - "@tanstack/react-router-devtools": "^1.166.7", - "@tanstack/router-plugin": "^1.166.7", - "@types/node": "^25.4.0", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-router-devtools": "^1.166.9", + "@tanstack/router-plugin": "^1.166.13", + "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react": "^6.0.1", "dotenv": "^17.3.1", "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.1", + "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vite": "v8.0.0-beta.16" + "vite": "v8.0.0", + "vite-plugin-pwa": "^1.2.0" } } diff --git a/public/auth.json b/public/auth.json new file mode 100644 index 0000000..acf0250 --- /dev/null +++ b/public/auth.json @@ -0,0 +1,44 @@ +{ + "metadata": { + "name": "kevisual", + "share": "public" + }, + "registry": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template", + "clone": { + ".": { + "enabled": true + } + }, + "syncd": [ + { + "files": [ + "**/*" + ], + "registry": "" + } + ], + "scripts": { + "auth": "ev sync clone -l -i https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/public/auth.json" + }, + "sync": { + "AGENTS.md": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/AGENTS.md", + "vite.config.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/vite.config.ts", + "src/main.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/main.tsx", + "public/auth.json": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/public/auth.json", + "src/agents/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/agents/index.ts", + "src/modules/basename.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/modules/basename.ts", + "src/modules/query.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/modules/query.ts", + "src/routes/demo.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/demo.tsx", + "src/routes/index.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/index.tsx", + "src/routes/login.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/login.tsx", + "src/styles/theme.css": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/styles/theme.css", + "src/pages/auth/index.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/index.tsx", + "src/pages/auth/page.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/page.tsx", + "src/pages/auth/store.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/store.ts", + "src/pages/demo/page.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/demo/page.tsx", + "src/pages/auth/hooks/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/hooks/index.ts", + "src/pages/auth/hooks/use-api-query.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/hooks/use-api-query.ts", + "src/pages/auth/modules/BaseHeader.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/modules/BaseHeader.tsx", + "src/pages/demo/store/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/demo/store/index.ts" + } +} \ No newline at end of file diff --git a/src/modules/basename.ts b/src/modules/basename.ts index 0516610..4792606 100644 --- a/src/modules/basename.ts +++ b/src/modules/basename.ts @@ -19,4 +19,14 @@ export const getDynamicBasename = (): string => { } // 默认使用构建时的 basename return basename +} + +export const openLink = (path: string, target: string = '_self') => { + if (path.startsWith('http://') || path.startsWith('https://')) { + window.open(path, target); + return; + } + const url = new URL(path, window.location.origin); + url.pathname = wrapBasename(url.pathname); + window.open(url.toString(), target); } \ No newline at end of file diff --git a/src/modules/query.ts b/src/modules/query.ts index bc14351..b45efe0 100644 --- a/src/modules/query.ts +++ b/src/modules/query.ts @@ -1,20 +1,18 @@ -import { Query } from '@kevisual/query'; +import { Query, DataOpts } from '@kevisual/query'; import { QueryLoginBrowser } from '@kevisual/api/query-login' import { useContextKey } from '@kevisual/context'; -export const query = useContextKey('query', () => { - return new Query({ - url: '/api/router', - }); -}); +import { QueryClient } from '@tanstack/react-query'; -export const queryClient = useContextKey('queryClient', () => { - return new Query({ - url: '/client/router', - }); -}); +export const query = useContextKey('query', new Query({ + url: '/api/router', +})); -export const queryLogin = useContextKey('queryLogin', () => { - return new QueryLoginBrowser({ - query: query - }); -}); \ No newline at end of file +export const queryClient = useContextKey('queryClient', new Query({ + url: '/client/router', +})); + +export const queryLogin = useContextKey('queryLogin', new QueryLoginBrowser({ + query: query +})); + +export const stackQueryClient = useContextKey('stackQueryClient', new QueryClient()); \ No newline at end of file diff --git a/src/pages/auth/hooks/index.ts b/src/pages/auth/hooks/index.ts new file mode 100644 index 0000000..660cd77 --- /dev/null +++ b/src/pages/auth/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-api-query'; diff --git a/src/pages/auth/hooks/use-api-query.ts b/src/pages/auth/hooks/use-api-query.ts new file mode 100644 index 0000000..44302c2 --- /dev/null +++ b/src/pages/auth/hooks/use-api-query.ts @@ -0,0 +1,55 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { queryLogin } from '@/modules/query'; +import { toast } from 'sonner'; +import type { UserInfo } from '../store'; + +export const authQueryKeys = { + me: ['auth', 'me'] as const, + token: ['auth', 'token'] as const, +} as const; + +export const useMe = () => { + return useQuery({ + queryKey: authQueryKeys.me, + queryFn: async () => { + const res = await queryLogin.getMe(); + if (res.code === 200) { + return res.data; + } + throw new Error(res.message || 'Failed to fetch user info'); + }, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; + +export const useSwitchOrg = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (username?: string) => { + const res = await queryLogin.switchUser(username || ''); + if (res.code === 200) { + return res.data; + } + throw new Error(res.message || 'Switch failed'); + }, + onSuccess: () => { + toast.success('切换成功'); + queryClient.invalidateQueries({ queryKey: authQueryKeys.me }); + setTimeout(() => { + window.location.reload(); + }, 1000); + }, + onError: (error) => { + toast.error(error.message || '请求失败'); + }, + }); +}; + +export const useGetToken = () => { + return useQuery({ + queryKey: authQueryKeys.token, + queryFn: () => queryLogin.getToken(), + staleTime: Infinity, + }); +}; diff --git a/src/pages/auth/index.tsx b/src/pages/auth/index.tsx index ac827c7..abc6ff5 100644 --- a/src/pages/auth/index.tsx +++ b/src/pages/auth/index.tsx @@ -6,7 +6,6 @@ export { BaseHeader } from './modules/BaseHeader' import { useMemo } from 'react'; import { useLocation, useNavigate } from '@tanstack/react-router'; - type Props = { children?: React.ReactNode, mustLogin?: boolean, diff --git a/src/pages/auth/modules/BaseHeader.tsx b/src/pages/auth/modules/BaseHeader.tsx index 551ad39..3136210 100644 --- a/src/pages/auth/modules/BaseHeader.tsx +++ b/src/pages/auth/modules/BaseHeader.tsx @@ -83,7 +83,6 @@ export const BaseHeader = (props: { main?: React.ComponentType | null }) => { {meInfo} -