temp
This commit is contained in:
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -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
23
web/.gitignore
vendored
@@ -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
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
151
web/src/app/query-view/components/DetailsDialog.tsx
Normal file
151
web/src/app/query-view/components/DetailsDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,33 +43,41 @@ export const QueryView = (props: Props) => {
|
||||
const studioStore = useStudioStore(useShallow((state) => ({
|
||||
deleteMessage: state.deleteMessage
|
||||
})))
|
||||
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) {
|
||||
setIsList(false);
|
||||
setObj(response.data);
|
||||
return;
|
||||
}
|
||||
if (isList === false) {
|
||||
setIsList(true);
|
||||
}
|
||||
setData(response.data.list)
|
||||
const [_, firstItem] = response.data.list || []
|
||||
if (firstItem) {
|
||||
const cols: ColumnDef<any>[] = Object.keys(firstItem).map(key => ({
|
||||
accessorKey: key,
|
||||
header: key.toUpperCase(),
|
||||
cell: info => info.getValue() + '',
|
||||
}))
|
||||
setColumns(cols)
|
||||
}
|
||||
}
|
||||
const main = async () => {
|
||||
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;
|
||||
}
|
||||
if (isList === false) {
|
||||
setIsList(true);
|
||||
}
|
||||
setData(response.data.list)
|
||||
handleResponse(response)
|
||||
console.log('res', res);
|
||||
const [_, firstItem] = response.data.list || []
|
||||
if (firstItem) {
|
||||
const cols: ColumnDef<any>[] = Object.keys(firstItem).map(key => ({
|
||||
accessorKey: key,
|
||||
header: key.toUpperCase(),
|
||||
cell: info => info.getValue() + '',
|
||||
}))
|
||||
setColumns(cols)
|
||||
}
|
||||
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'
|
||||
|
||||
22
web/src/app/query-view/store/index.ts
Normal file
22
web/src/app/query-view/store/index.ts
Normal 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 }),
|
||||
}));
|
||||
79
web/src/app/studio/components/ExportDialog.tsx
Normal file
79
web/src/app/studio/components/ExportDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
@@ -66,7 +69,7 @@ export const WrapperHeader = (props: { children: React.ReactNode }) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100% - 3rem)' }} className="overflow-auto">
|
||||
<div style={{ height: 'calc(100% - 3rem)' }} className="overflow-auto ">
|
||||
{props.children}
|
||||
</div>
|
||||
</div >
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
Reference in New Issue
Block a user