This commit is contained in:
2026-02-18 06:44:15 +08:00
parent 0b9d4dfce3
commit 94047cd45f
13 changed files with 460 additions and 119 deletions

32
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^1.2.0
version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@kevisual/router':
specifier: 0.0.72
version: 0.0.72
specifier: 0.0.74
version: 0.0.74
'@tanstack/react-router':
specifier: ^1.160.2
version: 1.160.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -50,9 +50,6 @@ importers:
dayjs:
specifier: ^1.11.19
version: 1.11.19
dotenv:
specifier: ^17.3.1
version: 17.3.1
es-toolkit:
specifier: ^1.44.0
version: 1.44.0
@@ -86,9 +83,6 @@ importers:
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwind-merge:
specifier: ^3.4.1
version: 3.4.1
valtio:
specifier: ^2.3.0
version: 2.3.0(@types/react@19.2.14)(react@19.2.4)
@@ -106,8 +100,8 @@ importers:
specifier: ^0.0.5
version: 0.0.5
'@kevisual/query':
specifier: ^0.0.42
version: 0.0.42
specifier: ^0.0.46
version: 0.0.46
'@kevisual/types':
specifier: ^0.0.12
version: 0.0.12
@@ -132,6 +126,12 @@ importers:
'@vitejs/plugin-react':
specifier: ^5.1.4
version: 5.1.4(vite@8.0.0-beta.14(@types/node@25.2.3)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
dotenv:
specifier: ^17.3.1
version: 17.3.1
tailwind-merge:
specifier: ^3.4.1
version: 3.4.1
tailwindcss:
specifier: ^4.1.18
version: 4.1.18
@@ -516,11 +516,11 @@ packages:
'@kevisual/load@0.0.6':
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
'@kevisual/query@0.0.42':
resolution: {integrity: sha512-FW0DqeAsiAz6ABnjxXcAEzsvMtH59kfvCipuCQilIUvnTeM2tCYR7O7ll7I4KI70WpuxcfNVMFSDqiMrPwTthg==}
'@kevisual/query@0.0.46':
resolution: {integrity: sha512-JwHV16ehk8JWM5wiWW5kz9yTg4HrOmmnci5QvwQYdhXYXDzGpUrOxeoz3wloMs4kX3bkowz97iLLW6uQdgUoTw==}
'@kevisual/router@0.0.72':
resolution: {integrity: sha512-+HL4FINZsjnoRRa8Qs7xoPg+5/TcHR7jZQ7AHWHogo0BJzCAtnQwmidMQzeGL4z0WKNbbgVhXdz1wAYoxHJZTg==}
'@kevisual/router@0.0.74':
resolution: {integrity: sha512-J8qDsvrpf317H0Gq9YkeGwI+GS23RC0q/mYbKOia8wF33ylz+pDhBN8T1KmXx90AVBt/tMGNVJRgEhTVdTgpvA==}
'@kevisual/types@0.0.12':
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
@@ -2877,9 +2877,9 @@ snapshots:
dependencies:
eventemitter3: 5.0.4
'@kevisual/query@0.0.42': {}
'@kevisual/query@0.0.46': {}
'@kevisual/router@0.0.72':
'@kevisual/router@0.0.74':
dependencies:
es-toolkit: 1.44.0

23
web/.gitignore vendored
View File

@@ -1,37 +1,18 @@
# Logs
logs
*.log
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
pack-dist
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo
.turbo
.pnpm-store
.tanstack
.env
.env*
!.env.example

View File

@@ -6,7 +6,7 @@
"name": "@kevisual/router-studio",
"dependencies": {
"@base-ui/react": "^1.2.0",
"@kevisual/router": "0.0.72",
"@kevisual/router": "0.0.75",
"@tanstack/react-router": "^1.160.2",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-md-editor": "^4.0.11",
@@ -33,7 +33,7 @@
"@kevisual/api": "^0.0.51",
"@kevisual/context": "^0.0.6",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/query": "^0.0.42",
"@kevisual/query": "^0.0.47",
"@kevisual/types": "^0.0.12",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-router-devtools": "^1.160.2",
@@ -202,9 +202,9 @@
"@kevisual/load": ["@kevisual/load@0.0.6", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA=="],
"@kevisual/query": ["@kevisual/query@0.0.42", "", {}, "sha512-FW0DqeAsiAz6ABnjxXcAEzsvMtH59kfvCipuCQilIUvnTeM2tCYR7O7ll7I4KI70WpuxcfNVMFSDqiMrPwTthg=="],
"@kevisual/query": ["@kevisual/query@0.0.47", "", {}, "sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g=="],
"@kevisual/router": ["@kevisual/router@0.0.72", "", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-+HL4FINZsjnoRRa8Qs7xoPg+5/TcHR7jZQ7AHWHogo0BJzCAtnQwmidMQzeGL4z0WKNbbgVhXdz1wAYoxHJZTg=="],
"@kevisual/router": ["@kevisual/router@0.0.75", "", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-WBDRKMjNYTP7ymkUUtiQwWYIcqnc+TGo3rFuRze8ovYV2UN5cQxIkIfsDbgWOdV1/v9b57gtiJvJRqWjCBWKRg=="],
"@kevisual/types": ["@kevisual/types@0.0.12", "", {}, "sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q=="],

View File

@@ -14,7 +14,7 @@
],
"dependencies": {
"@base-ui/react": "^1.2.0",
"@kevisual/router": "0.0.72",
"@kevisual/router": "0.0.75",
"@tanstack/react-router": "^1.160.2",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-md-editor": "^4.0.11",
@@ -41,7 +41,7 @@
"@kevisual/api": "^0.0.51",
"@kevisual/context": "^0.0.6",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/query": "^0.0.42",
"@kevisual/query": "^0.0.47",
"@kevisual/types": "^0.0.12",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-router-devtools": "^1.160.2",

View File

@@ -0,0 +1,151 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useQueryViewStore } from '../store';
import { useShallow } from 'zustand/shallow';
import { useStudioStore } from '@/app/studio/store';
import { useState } from 'react';
import { QueryView } from '..';
export const DetailsDialog = () => {
const [activeTab, setActiveTab] = useState('details');
const { showDetailsDialog, setShowDetailsDialog, detailsData } = useQueryViewStore(
useShallow((state) => ({
showDetailsDialog: state.showDetailsDialog,
setShowDetailsDialog: state.setShowDetailsDialog,
detailsData: state.detailsData,
}))
);
const { currentView } = useStudioStore(useShallow((state) => ({
currentView: state.currentView,
})));
if (!detailsData) return null;
console.log('activeTab ', activeTab);
return (
<Dialog open={showDetailsDialog} onOpenChange={setShowDetailsDialog}>
<DialogContent className="max-w-3xl! max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex gap-2 border-b border-gray-200">
<button
onClick={() => setActiveTab('details')}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === 'details'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
</button>
<button
onClick={() => setActiveTab('view')}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === 'view'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
</button>
<button
onClick={() => setActiveTab('response')}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === 'response'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
</button>
</div>
<div className="mt-4 h-[calc(80vh-200px)] overflow-auto scrollbar">
{/* 第一个标签页:详情信息 */}
{activeTab === 'details' && (
<div className="space-y-4">
{/* Type */}
{detailsData.type && (
<div className="border-b border-gray-200 pb-3">
<label className="text-sm font-semibold text-gray-700 block mb-1"></label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md">
{detailsData.type}
</div>
</div>
)}
{/* Title */}
{detailsData.title && (
<div className="border-b border-gray-200 pb-3">
<label className="text-sm font-semibold text-gray-700 block mb-1"></label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md">
{detailsData.title}
</div>
</div>
)}
{/* Description */}
{detailsData.description && (
<div className="border-b border-gray-200 pb-3">
<label className="text-sm font-semibold text-gray-700 block mb-1"></label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md whitespace-pre-wrap">
{detailsData.description}
</div>
</div>
)}
{/* Action */}
{detailsData.action && (
<div className="border-b border-gray-200 pb-3">
<label className="text-sm font-semibold text-gray-700 block mb-1"></label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md">
<pre className="text-xs overflow-auto">
{JSON.stringify(detailsData.action, null, 2)}
</pre>
</div>
</div>
)}
{/* 其他字段 */}
{detailsData.api && (
<div className="border-b border-gray-200 pb-3 w-full scrollbar">
<label className="text-sm font-semibold text-gray-700 block mb-1">API</label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md">
<pre className="text-xs w-full">
{JSON.stringify(detailsData.api, null, 2)}
</pre>
</div>
</div>
)}
</div>
)}
{/* 第二个标签页:当前视图 */}
{activeTab === 'view' && (
<div className="space-y-4">
{currentView ? (
<div className="border-b border-gray-200 pb-3">
<label className="text-sm font-semibold text-gray-700 block mb-1"> ID</label>
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md">
{currentView.viewId}
</div>
</div>
) : (
<div className="text-sm text-gray-500 text-center py-8">
</div>
)}
</div>
)}
{/* 第三个标签页:响应 */}
{activeTab === 'response' && (
<div className="space-y-4">
<QueryView viewData={detailsData} type={'message'} />
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -10,13 +10,16 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu'
import { useStudioStore } from '../studio/store'
import { Message, useStudioStore } from '../studio/store'
import { useQueryViewStore } from './store'
import { DetailsDialog } from './components/DetailsDialog'
import { useShallow } from 'zustand/shallow'
import { cloneDeep } from 'es-toolkit'
import { toast } from 'sonner'
import { Result } from '@kevisual/query'
type Props = {
type: 'component' | 'page',
viewData?: any
type: 'component' | 'page' | 'message',
viewData?: RouterViewItem
}
const queryProxy = new QueryProxy({
@@ -25,7 +28,7 @@ const queryProxy = new QueryProxy({
export const QueryView = (props: Props) => {
const [data, setData] = useState<any[]>([])
const [columns, setColumns] = useState<ColumnDef<any>[]>([])
const [type] = useState<'component' | 'page'>(props.type || 'page')
const [type] = useState<'component' | 'page' | 'message'>(props.type || 'page')
const [viewData, setViewData] = useState<RouterViewItem | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [showMoreMenu, setShowMoreMenu] = useState(false)
@@ -40,11 +43,11 @@ export const QueryView = (props: Props) => {
const studioStore = useStudioStore(useShallow((state) => ({
deleteMessage: state.deleteMessage
})))
const main = async () => {
try {
setIsLoading(true)
const res = await queryProxy.runByRouteView(viewData!)
const response = res.response;
const queryViewStore = useQueryViewStore(useShallow((state) => ({
setShowDetailsDialog: state.setShowDetailsDialog,
setDetailsData: state.setDetailsData,
})))
const handleResponse = (response: Result) => {
console.log('response', response, viewData);
const list = response.data?.list
if (!list) {
@@ -56,7 +59,6 @@ export const QueryView = (props: Props) => {
setIsList(true);
}
setData(response.data.list)
console.log('res', res);
const [_, firstItem] = response.data.list || []
if (firstItem) {
const cols: ColumnDef<any>[] = Object.keys(firstItem).map(key => ({
@@ -66,7 +68,16 @@ export const QueryView = (props: Props) => {
}))
setColumns(cols)
}
}
const main = async () => {
try {
setIsLoading(true)
const res = await queryProxy.runByRouteView(viewData!)
const response = res.response;
handleResponse(response)
console.log('res', res);
toast.success('数据获取成功')
} finally {
setIsLoading(false)
}
@@ -80,18 +91,19 @@ export const QueryView = (props: Props) => {
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(<pre className='max-h-96 overflow-auto'>{str}</pre>, {
// autoClose: 5000,
// closeOnClick: true,
// pauseOnHover: true,
// draggable: true,
// icon: false
// });
const data = cloneDeep(props.viewData) as RouterViewItem
// 删除可能过大的字段,避免在详情弹窗展示
if (data.type === 'api') {
delete data?.api?.query;
}
if (data.type === 'worker') {
delete data?.worker?.worker;
}
if (data.type === 'context') {
delete data?.context?.router;
}
queryViewStore.setDetailsData(data);
queryViewStore.setShowDetailsDialog(true);
}
const handleEdit = () => {
@@ -106,7 +118,7 @@ export const QueryView = (props: Props) => {
console.log('Delete row:', selectedRow)
// 在这里添加删除逻辑
}
studioStore.deleteMessage(props.viewData!)
studioStore.deleteMessage(props.viewData! as Message)
}
const handleExport = () => {
@@ -132,11 +144,19 @@ export const QueryView = (props: Props) => {
// 在这里添加保存并打开逻辑
}
}
useEffect(() => {
if (viewData) {
main()
}
}, [viewData])
// useEffect(() => {
// console.log('执行查询', viewData, props.type)
// if (viewData && props.type !== 'message') {
// main()
// } else if (viewData && props.type === 'message') {
// console.log('viewData ', viewData, props.type)
// if (viewData.response) {
// handleResponse(viewData.response)
// } else {
// //
// }
// }
// }, [viewData, props.type])
useEffect(() => {
props.viewData && setViewData(props.viewData as RouterViewItem)
@@ -186,7 +206,8 @@ export const QueryView = (props: Props) => {
</table>
}
const isPage = type === 'page'
return <div id='route-view' className={`w-full ${type === 'component' ? 'max-h-[600px] overflow-y-auto' : 'h-full overflow-auto'} p-4`}>
return <div id='route-view' className={`w-full ${type === 'component' ? 'max-h-150 overflow-y-auto' : 'h-full overflow-auto'} p-4`}>
<DetailsDialog />
<div className='mb-4'>
<div className='flex items-center justify-between'>
<h2 className={`font-bold ${type === 'component' ? 'text-lg' : 'text-2xl'} truncate`} title={`路由视图 - ${viewData?.title || '未命名'}`}> - {viewData?.title || '未命名'}</h2>
@@ -280,7 +301,7 @@ export const QueryViewMessages = (props: Props) => {
}
// 查询query-view的保存的id赋值后然后执行查询
// @ts-ignore
const DemoRouterView: RouterViewItem = {
const DemoRouterView: Message = {
id: 'getData',
description: '获取数据',
title: '获取数据',
@@ -294,7 +315,7 @@ export const QueryViewMessages = (props: Props) => {
key: 'list'
}
}
studioStore.setMessages([DemoRouterView])
studioStore.setMessages([DemoRouterView as Message])
}
useEffect(() => {
const type = props.type || 'page'

View File

@@ -0,0 +1,22 @@
import { create } from 'zustand';
type QueryViewState = {
showDataDialog: boolean;
setShowDataDialog: (show: boolean) => void;
dataDialogContent: any;
setDataDialogContent: (content: any) => void;
showDetailsDialog: boolean;
setShowDetailsDialog: (show: boolean) => void;
detailsData: any;
setDetailsData: (data: any) => void;
};
export const useQueryViewStore = create<QueryViewState>((set) => ({
showDataDialog: false,
setShowDataDialog: (show) => set({ showDataDialog: show }),
dataDialogContent: null,
setDataDialogContent: (content) => set({ dataDialogContent: content }),
showDetailsDialog: false,
setShowDetailsDialog: (show) => set({ showDetailsDialog: show }),
detailsData: null,
setDetailsData: (data) => set({ detailsData: data }),
}));

View File

@@ -0,0 +1,79 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { useStudioStore } from '../store';
import { useShallow } from 'zustand/shallow';
import { createQueryByRoutes } from '@kevisual/query/api'
import { useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Copy, Check } from 'lucide-react';
import { toast } from 'sonner';
import { useState } from 'react';
import { pick } from 'es-toolkit';
export const ExportDialog = () => {
const { showExportDialog, setShowExportDialog, exportRoutes } = useStudioStore(
useShallow((state) => ({
showExportDialog: state.showExportDialog,
setShowExportDialog: state.setShowExportDialog,
exportRoutes: state.exportRoutes,
}))
);
const [copied, setCopied] = useState(false);
const code = useMemo(() => {
if (!exportRoutes) return '';
let routeInfo = exportRoutes.map(route => pick(route, ['path', 'key', 'id', 'description', 'metadata']));
const query = createQueryByRoutes(routeInfo as any);
return query;
}, [exportRoutes]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
toast.success('代码已复制到剪贴板');
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error('复制失败,请重试');
}
};
return (
<Dialog open={showExportDialog} onOpenChange={setShowExportDialog}>
<DialogContent className="max-w-3xl! max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle>API代码</DialogTitle>
</DialogHeader>
<div className="space-y-4 w-full overflow-hidden">
<div className="p-4 border border-gray-300 rounded-md bg-gray-50">
<pre className="text-xs max-h-[60vh] overflow-auto scrollbar">
{code}
</pre>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowExportDialog(false)}
>
</Button>
<Button
onClick={handleCopy}
className="gap-2"
>
{copied ? (
<>
<Check size={16} />
</>
) : (
<>
<Copy size={16} />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,11 +1,14 @@
import { useStudioStore } from './store.ts';
import { use, useEffect, useState } from 'react';
import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, Filter, FilterX, Search, X } from 'lucide-react';
import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, Filter, FilterX, Search, X, MoreHorizontal, Info, Code, RotateCcw } 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';
import { Button } from '@/components/ui/button.tsx';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu.tsx';
import { ExportDialog } from './components/ExportDialog';
export const AppProvider = () => {
const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
showLeftPanel: state.showLeftPanel,
@@ -89,11 +92,13 @@ export const App = () => {
showFilter: state.showFilter,
currentView: state.currentView,
setShowFilter: state.setShowFilter,
setShowExportDialog: state.setShowExportDialog,
setExportRoutes: state.setExportRoutes,
})));
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set());
const [searchKeyword, setSearchKeyword] = useState<string>('');
const [defaultKeyword, setDefaultKeyword] = useState<string>('');
useEffect(() => {
queryRouteList(true);
}, []);
@@ -113,16 +118,13 @@ export const App = () => {
const viewItem = store.currentView.views.find(v => v.id === viewId);
if (viewItem && viewItem.query) {
setSearchKeyword(viewItem.query);
setDefaultKeyword(viewItem.query);
}
return () => clearTimeout(timer);
}
}, [store.showFilter, store.currentView?.viewId]);
const handleSearch = async (keyword: string) => {
if (keyword.trim()) {
await searchRoutes(keyword);
} else {
await queryRouteList();
}
await searchRoutes(keyword.trim());
};
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -133,7 +135,7 @@ export const App = () => {
const handleClear = async () => {
setSearchKeyword('');
await queryRouteList();
handleSearch('');
};
const toggleDescription = (id: string) => {
@@ -158,23 +160,36 @@ export const App = () => {
};
return (
<div className="max-w-5xl mx-auto p-6 h-full overflow-auto">
<div className="max-w-5xl mx-auto p-6 h-full overflow-hidden flex flex-col relative">
<ExportDialog />
{loading && <div className="text-center text-gray-500 mb-4">...</div>}
{store.showFilter && (
<div className="mb-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-gray-300 bg-white focus-within:ring-2 focus-within:ring-gray-400 focus-within:ring-offset-1 focus-within:border-gray-400">
<Search size={16} className="text-gray-700 flex-shrink-0" strokeWidth={2} />
<Search size={16} className="text-gray-700 shrink-0" strokeWidth={2} />
<Input
placeholder="输入路由关键词进行搜索..."
className="w-full !border-0 !shadow-none !outline-none bg-transparent focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!ring-offset-0 text-sm text-gray-900 placeholder:text-gray-500"
className="w-full border-0! shadow-none! outline-none! bg-transparent focus-visible:outline-none! focus-visible:ring-0! focus-visible:ring-offset-0! text-sm text-gray-900 placeholder:text-gray-500"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={handleKeyDown}
/>
{defaultKeyword && searchKeyword !== defaultKeyword && (
<button
onClick={async () => {
setSearchKeyword(defaultKeyword);
await handleSearch(defaultKeyword);
}}
className="p-1 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-200 active:bg-gray-300 transition-all duration-150 cursor-pointer shrink-0"
title="重置为默认关键词"
>
<RotateCcw size={16} />
</button>
)}
{searchKeyword && (
<button
onClick={handleClear}
className="p-1 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-200 active:bg-gray-300 transition-all duration-150 cursor-pointer flex-shrink-0"
className="p-1 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-200 active:bg-gray-300 transition-all duration-150 cursor-pointer shrink-0"
title="清空搜索"
>
<X size={16} />
@@ -183,7 +198,7 @@ export const App = () => {
</div>
</div>
)}
<div className={`space-y-1 ${loading ? "opacity-50 pointer-events-none" : ""}`}>
<div className={`space-y-1 ${loading ? "opacity-50 pointer-events-none" : ""} flex-1 overflow-auto scrollbar mb-10`}>
{routes.map((route: RouteItem) => {
const isExpanded = expandedIds.has(route.id);
const isIdVisible = visibleIds.has(route.id);
@@ -192,7 +207,7 @@ export const App = () => {
return (
<div
key={route.id}
className="px-4 py-3 border-b border-gray-100 hover:bg-gray-50/50 transition-all duration-200 animate-in fade-in slide-in-from-top-1 duration-400"
className="px-4 py-3 border-b border-gray-100 hover:bg-gray-50/50 transition-all animate-in fade-in slide-in-from-top-1 duration-400"
>
<div className="flex flex-col gap-2.5">
{/* ID and Path/Key in one line */}
@@ -226,6 +241,43 @@ export const App = () => {
onClick={() => run(route, 'custom')}>
<MonitorPlay size={14} strokeWidth={2.5} />
</button>
<DropdownMenu>
<DropdownMenuTrigger>
<button
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
title="更多选项"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal size={14} strokeWidth={2.5} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-gray-300">
<DropdownMenuItem
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
// TODO: 实现显示详情功能
console.log('显示详情', route);
}}
>
<Info size={14} className="mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
store.setExportRoutes([route]);
store.setShowExportDialog(true);
}}
>
<Code size={14} className="mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
@@ -278,6 +330,18 @@ export const App = () => {
);
})}
</div>
<div className='h-12 absolute bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex items-center justify-end px-4'>
<Button
variant="ghost"
onClick={() => {
store.setExportRoutes(routes);
store.setShowExportDialog(true);
}}
className="gap-2"
>
<Code size={16} />
</Button>
</div>
</div>
);
}

View File

@@ -7,10 +7,9 @@ import { use } from '@kevisual/context'
// import { MyCache } from '@kevisual/cache'
import { persist } from 'zustand/middleware';
import { app } from '@/agent/index.ts'
import { cloneDeep, random } from 'es-toolkit'
import { cloneDeep } from 'es-toolkit'
import { nanoid } from 'nanoid';
import { filter } from '@kevisual/js-filter';
import Fuse from 'fuse.js';
import { Result } from '@kevisual/query';
const historyReplace = (url: string) => {
if (window.history.replaceState) {
window.history.replaceState(null, '', url);
@@ -26,6 +25,17 @@ type RouteItem = {
type RouteViewList = Array<RouterViewData>;
type MessageAction = {
path?: string;
key?: string;
[key: string]: any;
}
export type Message = RouterViewItem<{
_id: string;
action: MessageAction;
description?: string;
response?: Result;
}>
interface StudioState {
loading: boolean;
@@ -50,12 +60,16 @@ interface StudioState {
setShowFilter: (show: boolean) => void;
showRightPanel: boolean;
setShowRightPanel: (show: boolean) => void;
messages: any[];
setMessages: (messages: any[]) => void;
addMessage: (message: any) => void;
deleteMessage: (message: any) => void;
messages: Message[];
setMessages: (messages: Message[]) => void;
addMessage: (message: Message) => void;
deleteMessage: (message: Message) => void;
searchKeyword?: string;
setSearchKeyword?: (keyword: string) => void;
showExportDialog: boolean;
setShowExportDialog: (show: boolean) => void;
exportRoutes?: RouteItem[];
setExportRoutes: (routes?: RouteItem[]) => void;
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@@ -192,6 +206,7 @@ export const useStudioStore = create<StudioState>()(
}
}
}
console.log('运行结果 route', route);
if (showRightPanel) {
if (route.metadata && route.metadata?.viewItem) {
const messages = get().messages
@@ -201,7 +216,7 @@ export const useStudioStore = create<StudioState>()(
viewItem.description = route.description || viewItem.description;
// @ts-ignore
viewItem._id = nanoid(16);
set({ messages: [...messages, viewItem] });
set({ messages: [...messages, viewItem as Message] });
}
}
},
@@ -293,7 +308,11 @@ export const useStudioStore = create<StudioState>()(
addMessage: (message: any) => {
const messages = get().messages;
set({ messages: [...messages, message] });
}
},
showExportDialog: false,
setShowExportDialog: (show: boolean) => set({ showExportDialog: show }),
exportRoutes: undefined,
setExportRoutes: (routes?: RouteItem[]) => set({ exportRoutes: routes })
}),
{
name: 'studio-storage',

View File

@@ -117,7 +117,7 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogContent className="max-w-3xl! max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle>{isUpdate ? '编辑视图' : '新增视图'}</DialogTitle>
</DialogHeader>

View File

@@ -197,10 +197,10 @@ export const ViewList = () => {
/>
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
</div>
<Button variant="outline" size="icon" className="h-10 w-10 cursor-pointer border-gray-300" onClick={handleRefresh}>
<Button variant="outline" size="icon" className="h-8 w-8 cursor-pointer border-gray-300" onClick={handleRefresh}>
<RotateCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" className="h-10 w-10 cursor-pointer border-gray-300" onClick={handleAdd}>
<Button variant="outline" size="icon" className="h-8 w-8 cursor-pointer border-gray-300" onClick={handleAdd}>
<Plus className="h-4 w-4" />
</Button>
</div>

View File

@@ -1,9 +1,13 @@
import { QueryClient } from '@kevisual/query';
export const query = new QueryClient({
import { Query } from '@kevisual/query';
import { QueryLoginBrowser } from '@kevisual/api/query-login'
export const query = new Query({
url: '/api/router',
});
export const queryClient = new QueryClient({
export const queryClient = new Query({
url: '/client/router',
});
export const queryLogin = new QueryLoginBrowser({
query: query
});