diff --git a/web/.github/prompts/astro.prompt.md b/web/.github/prompts/astro.prompt.md
index 99e86d2..1e83627 100644
--- a/web/.github/prompts/astro.prompt.md
+++ b/web/.github/prompts/astro.prompt.md
@@ -1,5 +1,32 @@
---
agent: agent
+tags: ["astro", "react", "tailwindcss", "shadcn/ui", "typescript"]
+createdAt: 2026-01-03
---
-当前项目使用的是astro和react,使用了tailwindcss,和shadcn/ui组件库。都已经安装好了。
+# 项目技术栈和上下文
+
+## 核心框架和库
+- **Astro** - 静态站点生成框架,用于构建高性能网站
+- **React** - 用于构建交互式 UI 组件
+- **TypeScript** - 项目使用 TypeScript 编写,有 tsconfig.json 配置
+
+## UI 和样式
+- **TailwindCSS** - CSS 框架,已集成
+- **shadcn/ui** - 高质量 React 组件库,已安装
+
+## 项目结构特点
+- 使用 pnpm 工作区管理
+- `src/` 目录包含主要源代码
+ - `apps/` - 应用模块(chat、cv、studio、query-view 等)
+ - `components/` - React组件
+ - `pages/` - Astro 页面
+ - `layouts/` - Astro 布局
+- `slides/` - 演示幻灯片内容
+
+## 开发指南
+- 修改代码时遵循项目现有的代码结构和命名约定
+- React 组件通常使用 `.tsx` 后缀
+- Astro 组件使用 `.astro` 后缀
+- 样式优先使用 TailwindCSS 类
+- 复用已有的 shadcn/ui 组件库中的组件
diff --git a/web/.opencode/skill/git-release/SKILL.md b/web/.opencode/skill/git-release/SKILL.md
new file mode 100644
index 0000000..c2a68a1
--- /dev/null
+++ b/web/.opencode/skill/git-release/SKILL.md
@@ -0,0 +1,19 @@
+---
+title: git-release
+描述: 获取需要diff的代码,总结和提交代码的技能
+license: MIT
+compatibility: opencode
+metadata:
+ audience: maintainers
+ workflow: github
+---
+
+## 我的工作
+
+- 获取代码变更的diff
+- 总结代码变更内容
+- 创建git提交
+
+## 何时使用我
+
+在需要提交代码变更时使用我。
diff --git a/web/package.json b/web/package.json
index 2fc76b6..3625bd6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -32,7 +32,10 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-md-editor": "^4.0.11",
@@ -64,7 +67,7 @@
"access": "public"
},
"devDependencies": {
- "@kevisual/api": "^0.0.16",
+ "@kevisual/api": "^0.0.17",
"@kevisual/types": "^0.0.10",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index a7cc38c..c916b95 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -47,9 +47,18 @@ importers:
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-popover':
+ specifier: ^1.1.15
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.13
+ version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.8
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@6.4.1(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2))
@@ -130,8 +139,8 @@ importers:
version: 5.0.9(@types/react@19.2.7)(react@19.2.3)
devDependencies:
'@kevisual/api':
- specifier: ^0.0.16
- version: 0.0.16
+ specifier: ^0.0.17
+ version: 0.0.17
'@kevisual/types':
specifier: ^0.0.10
version: 0.0.10
@@ -729,8 +738,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
- '@kevisual/api@0.0.16':
- resolution: {integrity: sha512-JInnqWHjUxos1oWHe8dmwxWOMCRgv5nI/7HbSrzvHDQxHE6Egc3xA5iALUcRDdkNOnPz98ErZnLmSgHHJDOwYQ==}
+ '@kevisual/api@0.0.17':
+ resolution: {integrity: sha512-hW3Q182Lm8wggWfHTEKVTKsmp8MWFINB9l82nEbnwTnd1Lh9DPeQo1hMft7aeL8aGe4vjFCTv4MHixXjmQTzGg==}
'@kevisual/cache@0.0.3':
resolution: {integrity: sha512-BWEck69KYL96/ywjYVkML974RHjDJTj2ITQND1zFPR+hlBV1H1p55QZgSYRJCObg3EAV1S9Zic/fR2T4pfe8yg==}
@@ -976,6 +985,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-popover@1.1.15':
+ resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-popper@1.2.8':
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
peerDependencies:
@@ -1072,6 +1094,32 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.8':
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@@ -1144,6 +1192,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
@@ -4421,7 +4482,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@kevisual/api@0.0.16':
+ '@kevisual/api@0.0.17':
dependencies:
'@kevisual/js-filter': 0.0.3
'@kevisual/load': 0.0.6
@@ -4778,6 +4839,29 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3)
+ aria-hidden: 1.2.6
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -4865,6 +4949,42 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.3)':
dependencies:
react: 19.2.3
@@ -4919,6 +5039,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
'@radix-ui/rect@1.1.1': {}
'@rc-component/async-validator@5.0.4':
diff --git a/web/src/apps/chat/index.tsx b/web/src/apps/chat/index.tsx
index c2fc3a9..883b907 100644
--- a/web/src/apps/chat/index.tsx
+++ b/web/src/apps/chat/index.tsx
@@ -3,12 +3,20 @@ import { useStudioStore } from '../studio/store';
import { useShallow } from 'zustand/shallow';
import { useState } from 'react';
import { query } from '@/modules/query.ts'
+import { QueryViewMessages } from '../query-view';
+import { toast } from 'react-toastify';
export const Chat = () => {
const studioStore = useStudioStore(useShallow((state) => ({
routes: state.routes,
+ showRightPanel: state.showRightPanel,
+ setShowRightPanel: state.setShowRightPanel,
+ addMessage: state.addMessage,
})));
const [text, setText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
const onSend = async () => {
+ if (!text.trim() || isLoading) return;
+ setIsLoading(true);
const { routes } = studioStore;
let callPrompts = '';
const toolsList = routes.map((r, index) =>
@@ -55,33 +63,48 @@ ${toolsList}
isJson: true
}
})
+ setText('');
console.log('发送消息', text, res);
if (res.code === 200) {
// 处理返回结果
const payload = res.data?.action;
if (payload) {
const route = routes.find(r => r.id === payload.id);
- console.log('找到工具', route);
const { path, key } = route || {};
const { id, ...otherParams } = payload.payload || {};
+ const action = { path, key, ...otherParams }
+ let response;
if (route) {
- const r = await app.run({ path, key, ...otherParams });
- console.log('工具调用结果', r);
+ response = await app.run(action);
+ // toast.success('工具调用成功');
} else {
console.error('未找到对应工具', payload.id);
+ toast.error('未找到对应工具');
+ return
+ }
+ if (route?.metadata?.viewItem) {
+ // 自动打开右侧面板
+ if (!studioStore.showRightPanel) {
+ studioStore.setShowRightPanel(true);
+ }
+ const viewItem = route.metadata.viewItem;
+ viewItem.response = response;
+ viewItem.action = action;
+ viewItem.description = route.description || viewItem.description;
+ // @ts-ignore
+ viewItem._id = Date.now();
+ studioStore.addMessage(viewItem);
}
}
}
+ setIsLoading(false);
}
return
-
-
- {/* 聊天内容区域 */}
+
+
diff --git a/web/src/apps/cv/index.tsx b/web/src/apps/cv/index.tsx
index d15309a..400cbd0 100644
--- a/web/src/apps/cv/index.tsx
+++ b/web/src/apps/cv/index.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import MDEditor from '@uiw/react-md-editor'
-import { ToastContainer } from 'react-toastify'
+import { ToastContainer, Slide } from 'react-toastify'
import { Save, Download, Printer, Eye, Edit, RotateCcw } from 'lucide-react'
import 'github-markdown-css/github-markdown-light.css'
import './index.css'
@@ -71,7 +71,7 @@ export const AppProvider = () => {
return (
)
}
diff --git a/web/src/apps/query-view/index.tsx b/web/src/apps/query-view/index.tsx
index 771f1c3..7367c6f 100644
--- a/web/src/apps/query-view/index.tsx
+++ b/web/src/apps/query-view/index.tsx
@@ -1,64 +1,159 @@
-import { QueryProxy } from '@kevisual/api/proxy'
+import { QueryProxy, RouterViewItem } from '@kevisual/api/proxy'
import { app } from '@/index.ts'
-import { useEffect, useState } from 'react'
-import { flexRender, useReactTable, getCoreRowModel } from '@tanstack/react-table';
+import { use, useEffect, useState } from 'react'
+import { flexRender, useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
+import { RefreshCw, Info, MoreVertical, Edit, Trash2, Download, Save, ExternalLink, Code } from 'lucide-react'
+import { toast, ToastContainer, Slide } from 'react-toastify'
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+} from '@/components/ui/dropdown-menu'
+import { useStudioStore } from '../studio/store'
+import { useShallow } from 'zustand/shallow'
+import { cloneDeep } from 'es-toolkit'
+
type Props = {
- data: any
- type: 'component' | 'page'
+ type: 'component' | 'page',
+ viewData?: any
}
-export const QueryView = (props: Props) => {
-
- return
API 视图
-}
-
const queryProxy = new QueryProxy({
router: app as any
});
-export const App = () => {
+export const QueryView = (props: Props) => {
const [data, setData] = useState
([])
- const [columns, setColumns] = useState([])
+ const [columns, setColumns] = useState[]>([])
+ const [type] = useState<'component' | 'page'>(props.type || 'page')
+ const [viewData, setViewData] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [showMoreMenu, setShowMoreMenu] = useState(false)
+ const [selectedRow, setSelectedRow] = useState(null)
+ const [isList, setIsList] = useState(true)
+ const [obj, setObj] = useState(null)
const table = useReactTable({
data,
columns: columns,
getCoreRowModel: getCoreRowModel(),
})
+ const studioStore = useStudioStore(useShallow((state) => ({
+ deleteMessage: state.deleteMessage
+ })))
const main = async () => {
- const res = await queryProxy.runByRouteView({
- id: 'getData',
- description: '获取数据',
- title: '获取数据',
- type: 'api',
- api: {
- url: "/api/router",
- },
- action: {
- path: 'router',
- key: 'list'
+ try {
+ setIsLoading(true)
+ const res = await queryProxy.runByRouteView(viewData!)
+ const response = res.response;
+ console.log('response', response, viewData);
+ const list = response.data?.list
+ if (!list) {
+ setIsList(false);
+ setObj(response.data);
+ return;
}
- })
- const response = res.response;
- setData(response.data.list)
- console.log('res', res);
- const [firstItem] = response.data.list || []
- if (firstItem) {
- const cols = Object.keys(firstItem).map(key => ({
- accessorKey: key,
- header: key.toUpperCase(),
- }))
- setColumns(cols)
+ if (isList === false) {
+ setIsList(true);
+ }
+ setData(response.data.list)
+ console.log('res', res);
+ const [_, firstItem] = response.data.list || []
+ if (firstItem) {
+ const cols: ColumnDef[] = Object.keys(firstItem).map(key => ({
+ accessorKey: key,
+ header: key.toUpperCase(),
+ cell: info => info.getValue() + '',
+ }))
+ setColumns(cols)
+ }
+ toast.success('数据获取成功')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleRefresh = () => {
+ if (viewData) {
+ setViewData({ ...viewData, response: undefined }) // 触发刷新
+ }
+ }
+
+ const handleShowDetails = () => {
+ console.log('Show details for row:', props.viewData)
+ const data = cloneDeep(props.viewData)
+ delete data.api?.proxy;
+ delete data.context?.router;
+ delete data.worker?.worker;
+ const str = JSON.stringify(data, null, 2)
+ toast.info({str}, {
+ autoClose: 5000,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ icon: false
+ });
+ }
+
+ const handleEdit = () => {
+ if (selectedRow) {
+ console.log('Edit row:', selectedRow)
+ // 在这里添加编辑逻辑
+ }
+ }
+
+ const handleDelete = () => {
+ if (selectedRow) {
+ console.log('Delete row:', selectedRow)
+ // 在这里添加删除逻辑
+ }
+ studioStore.deleteMessage(props.viewData!)
+ }
+
+ const handleExport = () => {
+ if (!viewData) return
+ console.log('Export viewData:', viewData)
+ }
+ const handleExportCode = () => {
+ if (!viewData) return
+ console.log('Export code for viewData:', viewData)
+ }
+ const handleSave = () => {
+ if (selectedRow) {
+ console.log('Save row:', selectedRow)
+ toast.success('保存成功')
+ // 在这里添加保存逻辑
+ }
+ }
+
+ const handleSaveAndOpen = () => {
+ if (selectedRow) {
+ console.log('Save and open row:', selectedRow)
+ toast.success('保存并打开成功')
+ // 在这里添加保存并打开逻辑
}
}
useEffect(() => {
- main()
+ if (viewData) {
+ main()
+ }
+ }, [viewData])
+
+ useEffect(() => {
+ props.viewData && setViewData(props.viewData as RouterViewItem)
}, [])
- return
-
-
+ const RenderTable = () => {
+ if (!isList) {
+ return
+ {JSON.stringify(obj, null, 2)}
+
+ }
+ return
+
{table.getHeaderGroups().map(headerGroup => (
{headerGroup.headers.map(header => (
- |
@@ -73,14 +168,13 @@ export const App = () => {
|
{table.getRowModel().rows.map((row, idx) => (
-
{row.getVisibleCells().map(cell => (
- |
@@ -91,11 +185,127 @@ export const App = () => {
))}
|
+ }
+ const isPage = type === 'page'
+ return
+
+
+
路由视图 - {viewData?.title || '未命名'}
+
+
+
+
+
+
+
+
+ {!isPage && (
+ <>
+
+
+ 保存
+
+ >
+ )}
+ {!isPage && (
+ <>
+
+
+ 保存并打开
+
+
+ >
+ )}
+ handleShowDetails()}>
+
+ 详情
+
+
+
+ 编辑
+
+
+
+ {!isPage ? '移除' : '删除'}
+
+
+
+
+ 导出
+
+
+
+ 导出代码
+
+
+
+
+
+
+
+
+
}
-
-export const AppProvider = (props: { children?: React.ReactNode }) => {
+export const QueryViewMessages = (props: Props) => {
+ const studioStore = useStudioStore(useShallow((state) => ({
+ messages: state.messages,
+ setMessages: state.setMessages
+ })))
+ const initPage = async () => {
+ const url = new URL(window.location.href)
+ const id = url.searchParams.get('id') || ''
+ if (!id) {
+ toast.error('缺少查询视图ID参数')
+ // return
+ }
+ // 查询query-view的保存的id,赋值后然后执行查询
+ // @ts-ignore
+ const DemoRouterView: RouterViewItem = {
+ id: 'getData',
+ description: '获取数据',
+ title: '获取数据',
+ type: 'api',
+ api: {
+ // url: "/api/router",
+ url: "http://localhost:52000/api/router",
+ },
+ action: {
+ path: 'router',
+ key: 'list'
+ }
+ }
+ studioStore.setMessages([DemoRouterView])
+ }
+ useEffect(() => {
+ const type = props.type || 'page'
+ if (type === 'page') {
+ initPage()
+ }
+ }, [props.type])
+ return
+ {studioStore.messages.map((msg, index) => (
+
+
+
+ ))}
+
+}
+export const AppProvider = () => {
return
-
+
+
}
\ No newline at end of file
diff --git a/web/src/apps/setting/index.tsx b/web/src/apps/setting/index.tsx
index 4135cba..c5eaef7 100644
--- a/web/src/apps/setting/index.tsx
+++ b/web/src/apps/setting/index.tsx
@@ -1,4 +1,4 @@
-import { toast, ToastContainer } from 'react-toastify';
+import { toast, ToastContainer, Slide } from 'react-toastify';
export const AppProvider = () => {
return
@@ -13,7 +13,8 @@ export const AppProvider = () => {
pauseOnFocusLoss
draggable
pauseOnHover
- theme="light" />
+ theme="light"
+ transition={Slide} />
}
diff --git a/web/src/apps/studio/index.tsx b/web/src/apps/studio/index.tsx
index 51a11da..d326265 100644
--- a/web/src/apps/studio/index.tsx
+++ b/web/src/apps/studio/index.tsx
@@ -1,14 +1,16 @@
-import { toast, ToastContainer } from 'react-toastify';
+import { toast, ToastContainer, Slide } from 'react-toastify';
import { useStudioStore } from './store.ts';
-import { useEffect, useState } from 'react';
-import { MonitorPlay, Play, PanelLeft, PanelLeftClose } from 'lucide-react';
+import { use, useEffect, useState } from 'react';
+import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, Filter, FilterX, Search, X } from 'lucide-react';
import { Panel, Group } from 'react-resizable-panels'
import { ViewList } from '../view/list.tsx';
import { useShallow } from 'zustand/shallow';
import { Chat } from '../chat/index.tsx';
+import { Input } from '@/components/ui/input.tsx';
export const AppProvider = () => {
- const { showLeftPanel } = useStudioStore(useShallow((state) => ({
+ const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
showLeftPanel: state.showLeftPanel,
+ showRightPanel: state.showRightPanel,
})));
return
@@ -21,9 +23,9 @@ export const AppProvider = () => {
-
+ {showRightPanel &&
-
+ }
@@ -39,19 +41,43 @@ export const AppProvider = () => {
pauseOnFocusLoss
draggable
pauseOnHover
- theme="light" />
+ theme="light"
+ transition={Slide} />
}
export const WrapperHeader = (props: { children: React.ReactNode }) => {
const showLeftPanel = useStudioStore(state => state.showLeftPanel);
- const setShowLeftPanel = useStudioStore(state => state.setShowLeftPanel);
+ const store = useStudioStore(useShallow((state) => ({
+ showLeftPanel: state.showLeftPanel,
+ setShowLeftPanel: state.setShowLeftPanel,
+ showFilter: state.showFilter,
+ setShowFilter: state.setShowFilter,
+ showRightPanel: state.showRightPanel,
+ setShowRightPanel: state.setShowRightPanel,
+ })));
return
{
- setShowLeftPanel(!showLeftPanel);
+ store.setShowLeftPanel(!store.showLeftPanel);
}}>
{showLeftPanel ?
:
}
+
+
+
+
{props.children}
@@ -67,13 +93,61 @@ interface RouteItem {
}
export const App = () => {
- const { routes, queryRouteList, run, loading } = useStudioStore();
+ const { routes, queryRouteList, run, loading, searchRoutes, ...store } = useStudioStore(useShallow((state) => ({
+ routes: state.routes,
+ searchRoutes: state.searchRoutes,
+ queryRouteList: state.queryRouteList,
+ run: state.run,
+ loading: state.loading,
+ showFilter: state.showFilter,
+ currentView: state.currentView,
+ setShowFilter: state.setShowFilter,
+ })));
const [expandedIds, setExpandedIds] = useState
>(new Set());
const [visibleIds, setVisibleIds] = useState>(new Set());
+ const [searchKeyword, setSearchKeyword] = useState('');
useEffect(() => {
queryRouteList();
}, []);
+ useEffect(() => {
+ if (store.showFilter) {
+ // 当显示过滤器时,自动聚焦输入框
+ const timer = setTimeout(() => {
+ const input = document.querySelector('input[placeholder="过滤路由..."]') as HTMLInputElement;
+ if (input) {
+ input.focus();
+ }
+ }, 100);
+ if (!store.currentView) {
+ return
+ }
+ const viewId = store.currentView.viewId;
+ const viewItem = store.currentView.views.find(v => v.id === viewId);
+ if (viewItem && viewItem.query) {
+ setSearchKeyword(viewItem.query);
+ }
+ return () => clearTimeout(timer);
+ }
+ }, [store.showFilter, store.currentView?.viewId]);
+ const handleSearch = async (keyword: string) => {
+ if (keyword.trim()) {
+ await searchRoutes(keyword);
+ } else {
+ await queryRouteList();
+ }
+ };
+
+ const handleKeyDown = async (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ await handleSearch(searchKeyword);
+ }
+ };
+
+ const handleClear = async () => {
+ setSearchKeyword('');
+ await queryRouteList();
+ };
const toggleDescription = (id: string) => {
const newExpanded = new Set(expandedIds);
@@ -99,6 +173,29 @@ export const App = () => {
return (
{loading &&
加载中...
}
+ {store.showFilter && (
+
+
+
+ setSearchKeyword(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+ {searchKeyword && (
+
+ )}
+
+
+ )}
{routes.map((route: RouteItem) => {
const isExpanded = expandedIds.has(route.id);
diff --git a/web/src/apps/studio/store.ts b/web/src/apps/studio/store.ts
index e646378..ef6aad0 100644
--- a/web/src/apps/studio/store.ts
+++ b/web/src/apps/studio/store.ts
@@ -1,11 +1,13 @@
import { create } from 'zustand';
-import { QueryProxy, RouterViewData } from '@kevisual/api'
+import { QueryProxy, RouterViewData, RouterViewItem, pickRouterViewData } from '@kevisual/api'
import { query } from '@/modules/query.ts'
import { toast } from 'react-toastify';
import { use } from '@kevisual/context'
import { MyCache } from '@kevisual/cache'
import { persist } from 'zustand/middleware';
import { app } from '@/index.ts'
+import { cloneDeep, random } from 'es-toolkit'
+import { nanoid } from 'nanoid';
const historyReplace = (url: string) => {
if (window.history.replaceState) {
window.history.replaceState(null, '', url);
@@ -26,6 +28,7 @@ interface StudioState {
loading: boolean;
setLoading: (loading: boolean) => void;
routes: Array
;
+ searchRoutes: (keyword: string) => Promise;
run: (route: RouteItem) => Promise;
queryProxy?: QueryProxy;
init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>;
@@ -35,12 +38,19 @@ interface StudioState {
getCurrentView: () => Promise;
updateRouteView: (view: RouterViewData) => Promise;
deleteRouteView: (id: string) => Promise;
+ deleteRouteViewItem: (id: string, viewId: string) => void;
currentView?: RouterViewData;
setCurrentView: (view?: RouterViewData) => Promise;
showLeftPanel: boolean;
setShowLeftPanel: (show: boolean) => void;
+ showFilter: boolean;
+ setShowFilter: (show: boolean) => void;
showRightPanel: boolean;
setShowRightPanel: (show: boolean) => void;
+ messages: any[];
+ setMessages: (messages: any[]) => void;
+ addMessage: (message: any) => void;
+ deleteMessage: (message: any) => void;
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@@ -50,6 +60,13 @@ export const useStudioStore = create()(
loading: false,
setLoading: (loading: boolean) => set({ loading }),
routes: [],
+ searchRoutes: async (keyword: string) => {
+ const state = await get().init();
+ let queryProxy = state.queryProxy;
+ const routes: any[] = await queryProxy.listRoutes(() => true, { query: keyword });
+ set({ routes });
+ },
+ currentView: undefined,
queryRouteList: async () => {
await get().getCurrentView();
const state = await get().init();
@@ -116,15 +133,50 @@ export const useStudioStore = create()(
get().getViewList();
toast.success('视图删除成功');
},
+ deleteRouteViewItem: (id: string, viewId: string) => {
+ const routeViewList = get().routeViewList;
+ const viewItem = routeViewList.find(view => view.id === id);
+ if (!viewItem) {
+ toast.error('视图项不存在');
+ return;
+ }
+ viewItem.views = viewItem.views?.filter(v => v.id !== viewId);
+ get().updateRouteView(viewItem);
+ const newList = routeViewList.map(view => {
+ if (view.id === viewItem.id) {
+ return viewItem;
+ }
+ return view;
+ });
+ set({ routeViewList: [...newList] });
+ console.log('删除视图项', id, newList);
+ },
run: async (route: RouteItem) => {
const state = await get().init();
let queryProxy = state.queryProxy;
- const res = await queryProxy.run({ path: route.path, key: route.key });
+ const showRightPanel = get().showRightPanel;
+ const action = {
+ path: route.path,
+ key: route.key
+ }
+ const res = await queryProxy.run(action);
if (res.code !== 200) {
toast.error(`运行失败:${res.message || '未知错误'}`);
} else if (res.code === 200) {
//
}
+ if (showRightPanel) {
+ if (route.metadata && route.metadata?.viewItem) {
+ const messages = get().messages
+ const viewItem = cloneDeep(route.metadata.viewItem) as RouterViewItem;
+ viewItem.response = res;
+ viewItem.action = action
+ viewItem.description = route.description || viewItem.description;
+ // @ts-ignore
+ viewItem._id = nanoid(16);
+ set({ messages: [...messages, viewItem] });
+ }
+ }
},
queryProxy: undefined,
router: undefined,
@@ -137,7 +189,9 @@ export const useStudioStore = create()(
return { queryProxy };
}
let currentView: RouterViewData | undefined = get().currentView;
+ // @ts-ignore
const routerViewData: RouterViewData = currentView || {
+ _id: nanoid(16),
views: [{
id: '',
title: '默认视图',
@@ -163,6 +217,7 @@ export const useStudioStore = create()(
routerViewData,
router: app as any,
});
+ console.log('初始化 QueryProxy 完成', queryProxy.token);
set({ loading: true });
await sleep(1000); // 保证 loading 状态更新
await queryProxy.init();
@@ -172,12 +227,34 @@ export const useStudioStore = create()(
},
showLeftPanel: false,
setShowLeftPanel: (show: boolean) => set({ showLeftPanel: show }),
- showRightPanel: false,
+ showRightPanel: true,
setShowRightPanel: (show: boolean) => set({ showRightPanel: show }),
+ showFilter: false,
+ setShowFilter: (show: boolean) => set({ showFilter: show }),
+ messages: [],
+ setMessages: (messages: any[]) => set({ messages }),
+ deleteMessage: (message: any) => {
+ const messages = get().messages;
+ const index = messages.findIndex(m => {
+ return m._id === message._id || m.id === message.id;
+ });
+ if (index !== -1) {
+ messages.splice(index, 1);
+ set({ messages: [...messages] });
+ }
+ },
+ addMessage: (message: any) => {
+ const messages = get().messages;
+ set({ messages: [...messages, message] });
+ }
}),
{
name: 'studio-storage',
- partialize: (state) => ({ showLeftPanel: state.showLeftPanel, showRightPanel: state.showRightPanel }),
+ partialize: (state) => ({
+ showLeftPanel: state.showLeftPanel,
+ showRightPanel: state.showRightPanel,
+ showFilter: state.showFilter,
+ }),
}
)
);
diff --git a/web/src/apps/view/components/ViewEditor.tsx b/web/src/apps/view/components/ViewEditor.tsx
index c0bd9ec..e45bebf 100644
--- a/web/src/apps/view/components/ViewEditor.tsx
+++ b/web/src/apps/view/components/ViewEditor.tsx
@@ -1,8 +1,9 @@
-import { useState, useEffect } from "react"
+import { useState, useEffect, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { DataItemForm } from "@/apps/view/components/DataItemForm"
import { ViewFormItem } from "@/apps/view/components/ViewFormItem"
import { nanoid } from "nanoid"
@@ -23,6 +24,8 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps
const [title, setTitle] = useState('')
const [dataItems, setDataItems] = useState([])
const [views, setViews] = useState([])
+ const dataItemsScrollRef = useRef(null)
+ const viewsScrollRef = useRef(null)
const isUpdate = !!data?.id
@@ -36,6 +39,15 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps
const handleAddDataItem = () => {
setDataItems([...dataItems, { type: 'api', api: { url: '' } }])
+ // 异步滚动到底部,使用 smooth 平滑滚动
+ setTimeout(() => {
+ if (dataItemsScrollRef.current) {
+ dataItemsScrollRef.current.scrollTo({
+ top: dataItemsScrollRef.current.scrollHeight,
+ behavior: 'smooth'
+ })
+ }
+ }, 0)
}
const handleUpdateDataItem = (index: number, item: any) => {
@@ -50,6 +62,15 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps
const handleAddView = () => {
setViews([...views, { id: nanoid(16), title: '', query: '' }])
+ // 异步滚动到底部,使用 smooth 平滑滚动
+ setTimeout(() => {
+ if (viewsScrollRef.current) {
+ viewsScrollRef.current.scrollTo({
+ top: viewsScrollRef.current.scrollHeight,
+ behavior: 'smooth'
+ })
+ }
+ }, 0)
}
const handleUpdateView = (index: number, view: any) => {
@@ -96,12 +117,13 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps
return (