更新项目结构,添加 demo 页面及相关状态管理;修改路由配置,更新依赖项,重构部分代码

This commit is contained in:
2026-02-09 00:21:44 +08:00
parent e0291d70bf
commit aadf2acebe
15 changed files with 258 additions and 27 deletions

View File

@@ -17,10 +17,6 @@ $:
- vscode
- docker
imports: !reference [.common_env, imports]
# 开发环境启动后会执行的任务
# stages:
# - name: pnpm install
# script: pnpm install
stages: !reference [.dev_template, stages]
.common_sync_to_gitea: &common_sync_to_gitea
@@ -34,10 +30,6 @@ $:
stages: !reference [.common_sync_from_gitea_template, stages]
main:
web_trigger_sync_to_gitea:
- <<: *common_sync_to_gitea
web_trigger_sync_from_gitea:
- <<: *common_sync_from_gitea
api_trigger_sync_to_gitea:
- <<: *common_sync_to_gitea
api_trigger_sync_from_gitea:

View File

@@ -1,11 +0,0 @@
# .cnb/web_trigger.yml
branch:
# 如下按钮在分支名以 release 开头的分支详情页面显示
- reg: "^main"
buttons:
- name: 同步代码到gitea
description: 同步代码到gitea
event: web_trigger_sync_to_gitea
- name: 同步gitea代码到当前仓库
description: 同步gitea代码到当前仓库
event: web_trigger_sync_from_gitea

48
AGENTS.md Normal file
View File

@@ -0,0 +1,48 @@
# AGENTS.md
本指南为在此仓库中工作的 AI 编码代理提供关键信息。
## 项目结构
```
src/
├── components/ui/ # shadcn/ui 组件Base UI 基础组件)
├── lib/ # 工具函数cn() 函数用于 className 合并)
├── modules/ # 应用模块query client、basename
├── pages/ # 页面组件(默认导出)
├── routes/ # TanStack Router 基于文件的路由
├── styles/ # 全局样式、主题 CSS
└── main.tsx # 应用入口
```
## 代码风格指南
### 模块目录结构
每个新模块(如 `page-app`)应遵循以下结构:
```
pages/page-app/
├── components/ # 模块专属组件
├── store/ # 模块状态管理
└── module/ # 模块功能函数
```
### 状态和数据获取
- **Zustand** 用于全局状态管理
- **@kevisual/query** 用于数据获取QueryClient 实例位于 `src/modules/query.ts`
- **React Hook Form** 用于表单管理
## 核心依赖
- **@base-ui/react**: Headless UI 基础组件
- **@tanstack/react-router**: 基于 TanStack Router 插件的文件路由
- **class-variance-authority**: 基于变体的样式系统
- **clsx + tailwind-merge**: 通过 `cn()` 提供 className 工具函数
- **lucide-react**: 图标库
- **react-hook-form**: 表单处理
- **sonner**: Toast 通知
- **zustand**: 状态管理
- **tailwindcss v4**: 使用 @tailwindcss/vite 插件进行样式处理

View File

@@ -1,12 +1,11 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.png" />
<link rel="icon" type="image/jpg" href="https://kevisual.xiongxiao.me/root/center/panda.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<link rel="stylesheet" href="/src/index.css" />
<title>Light Code</title>
<style>
html,
body {

View File

@@ -18,6 +18,8 @@
"license": "MIT",
"dependencies": {
"@base-ui/react": "^1.1.0",
"@kevisual/api": "^0.0.47",
"@kevisual/context": "^0.0.4",
"@kevisual/router": "0.0.70",
"@tanstack/react-router": "^1.158.4",
"class-variance-authority": "^0.7.1",

58
pnpm-lock.yaml generated
View File

@@ -11,6 +11,12 @@ importers:
'@base-ui/react':
specifier: ^1.1.0
version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@kevisual/api':
specifier: ^0.0.47
version: 0.0.47
'@kevisual/context':
specifier: ^0.0.4
version: 0.0.4
'@kevisual/router':
specifier: 0.0.70
version: 0.0.70
@@ -583,9 +589,21 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@kevisual/api@0.0.47':
resolution: {integrity: sha512-Yhb5NQR+FqQB6huAPqO4uCoEdWiWwXGI0m0lCj6yk0/eIM+X/CzTRtS2mMcxDH3r/BacDJ+OlGQMCqnQcM896g==}
'@kevisual/cache@0.0.2':
resolution: {integrity: sha512-2Cl5KF2Gi27uLfhO6CdTMFnRzx9vYnqevAo7d9ab3rOaqTgF8tLeAXglXyRbaWW3WUbHU2XaOb4r98uUsqIQQw==}
'@kevisual/context@0.0.4':
resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==}
'@kevisual/js-filter@0.0.5':
resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==}
'@kevisual/load@0.0.6':
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
'@kevisual/query-login@0.0.5':
resolution: {integrity: sha512-389cMMWAisjQoafxX+cUEa2z41S5koDjiyHkucfCkhRoP4M6g0iqbBMavLKmLOWSKx3R8e3ZmXT6RfsYGBb8Ww==}
peerDependencies:
@@ -1504,6 +1522,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
fdir@6.4.4:
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
peerDependencies:
@@ -1536,6 +1557,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
fuse.js@7.1.0:
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
engines: {node: '>=10'}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -1863,6 +1888,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
path-browserify-esm@1.0.6:
resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -2092,6 +2120,9 @@ packages:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
spark-md5@3.0.2:
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
@@ -2729,6 +2760,17 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@kevisual/api@0.0.47':
dependencies:
'@kevisual/js-filter': 0.0.5
'@kevisual/load': 0.0.6
es-toolkit: 1.44.0
eventemitter3: 5.0.4
fuse.js: 7.1.0
nanoid: 5.1.6
path-browserify-esm: 1.0.6
spark-md5: 3.0.2
'@kevisual/cache@0.0.2(rollup@4.40.1)(typescript@5.8.3)':
dependencies:
'@rollup/plugin-commonjs': 28.0.3(rollup@4.40.1)
@@ -2741,6 +2783,14 @@ snapshots:
- tslib
- typescript
'@kevisual/context@0.0.4': {}
'@kevisual/js-filter@0.0.5': {}
'@kevisual/load@0.0.6':
dependencies:
eventemitter3: 5.0.4
'@kevisual/query-login@0.0.5(@kevisual/query@0.0.28(ws@8.18.1))(rollup@4.40.1)(typescript@5.8.3)':
dependencies:
'@kevisual/cache': 0.0.2(rollup@4.40.1)(typescript@5.8.3)
@@ -3572,6 +3622,8 @@ snapshots:
estree-walker@2.0.2: {}
eventemitter3@5.0.4: {}
fdir@6.4.4(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
@@ -3591,6 +3643,8 @@ snapshots:
function-bind@1.1.2: {}
fuse.js@7.1.0: {}
gensync@1.0.0-beta.2: {}
get-nonce@1.0.1: {}
@@ -3827,6 +3881,8 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
path-browserify-esm@1.0.6: {}
path-parse@1.0.7: {}
path-type@4.0.0: {}
@@ -4047,6 +4103,8 @@ snapshots:
source-map@0.7.6: {}
spark-md5@3.0.2: {}
stylis@4.2.0: {}
supports-preserve-symlinks-flag@1.0.0: {}

3
src/agents/app.ts Normal file
View File

@@ -0,0 +1,3 @@
import { QueryRouterServer } from "@kevisual/router/browser"
import { useContextKey } from '@kevisual/context'
export const app = useContextKey('router', new QueryRouterServer())

1
src/agents/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './app.ts'

View File

View File

@@ -1,2 +1,11 @@
// @ts-ignore
export const basename = BASE_NAME;
export const wrapBasename = (path: string) => {
const hasEnd = path.endsWith('/')
if (basename) {
return `${basename}${path}` + (hasEnd ? '' : '/');
} else {
return path;
}
}

View File

@@ -1,3 +1,3 @@
import { QueryClient } from '@kevisual/query';
export const query = new QueryClient();
export const query = new QueryClient({});

8
src/pages/demo/page.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { useDemoStore } from './store/index'
export const App = () => {
const demoStore = useDemoStore()
console.log('demo', demoStore.formData)
return <div>App</div>
}
export default App;

View File

@@ -0,0 +1,95 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { toast } from 'sonner';
interface Data {
id: string;
[key: string]: any;
}
type State = {
formData: Record<string, any>;
setFormData: (data: Record<string, any>) => void;
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;
loading: boolean;
setLoading: (loading: boolean) => void;
list: Data[];
getItem: (id: string) => Promise<any>;
getList: () => Promise<any>;
updateData: (data: Data) => Promise<void>;
deleteData: (id: string) => Promise<void>;
}
export const useDemoStore = create<State>((set, get) => {
return {
formData: {},
setFormData: (data) => set({ formData: data }),
showEdit: false,
setShowEdit: (showEdit) => set({ showEdit }),
loading: false,
setLoading: (loading) => set({ loading }),
list: [],
getItem: async (id) => {
const { setLoading } = get();
setLoading(true);
try {
const res = await query.post({
path: 'demo',
key: 'item',
data: { id }
})
if (res.code === 200) {
return res;
} else {
toast.error(res.message || '请求失败');
}
} finally {
setLoading(false);
}
},
getList: async () => {
const { setLoading } = get();
setLoading(true);
try {
const res = await query.post({
path: 'demo',
key: 'list'
});
if (res.code === 200) {
const list = res.data?.list || []
set({ list });
} else {
toast.error(res.message || '请求失败');
}
return res;
} finally {
setLoading(false);
}
},
updateData: async (data) => {
const res = await query.post({
path: 'demo',
key: 'update',
data
})
if (res.code === 200) {
get().getList()
} else {
toast.error(res.message || '请求失败');
}
},
deleteData: async (id) => {
const res = await query.post({
path: 'demo',
key: 'delete',
data: { id }
})
if (res.code === 200) {
get().getList()
} else {
toast.error(res.message || '请求失败');
}
}
}
})

View File

@@ -9,8 +9,14 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as DemoRouteImport } from './routes/demo'
import { Route as IndexRouteImport } from './routes/index'
const DemoRoute = DemoRouteImport.update({
id: '/demo',
path: '/demo',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fullPaths: '/' | '/demo'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
to: '/' | '/demo'
id: '__root__' | '/' | '/demo'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DemoRoute: typeof DemoRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/demo': {
id: '/demo'
path: '/demo'
fullPath: '/demo'
preLoaderRoute: typeof DemoRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DemoRoute: DemoRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

9
src/routes/demo.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import App from '@/pages/demo/page'
export const Route = createFileRoute('/demo')({
component: RouteComponent,
})
function RouteComponent() {
return <App />
}