feat: update routing and API handling, improve query functionality, and enhance UI components
- Changed import path for routes in next-env.d.ts - Updated dependencies in package.json, including @kevisual/api and @kevisual/query - Added delete functionality in QueryView component - Modified run function in studio to accept a custom type - Enhanced searchRoutes function in studio store with Fuse.js for better filtering - Updated DataItemForm to use QueryRouterServer from the browser - Added dynamic URL handling in query module - Improved proxy request handling based on app key - Adjusted TypeScript configuration for stricter checks - Created a new page component for dynamic routing - Introduced a new Toaster component for notifications
This commit is contained in:
1393
pnpm-lock.yaml
generated
1393
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
web/next-env.d.ts
vendored
2
web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./dist/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
"ui": "pnpm dlx shadcn@latest add "
|
"ui": "pnpm dlx shadcn@latest add "
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/api": "^0.0.17",
|
"@kevisual/api": "^0.0.20",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/query": "^0.0.35",
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
|
"@kevisual/query": "^0.0.37",
|
||||||
"@kevisual/router": "^0.0.60",
|
"@kevisual/router": "^0.0.60",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -25,15 +26,18 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
"antd": "^6.2.1",
|
"antd": "^6.2.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"es-toolkit": "^1.44.0",
|
"es-toolkit": "^1.44.0",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-resizable-panels": "^4.4.1",
|
"react-resizable-panels": "^4.4.1",
|
||||||
|
|||||||
14
web/src/app/[root]/v1/[appId]/page.tsx
Normal file
14
web/src/app/[root]/v1/[appId]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { AppProvider } from '../../../studio/index.tsx';
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{
|
||||||
|
root: string
|
||||||
|
appId: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: PageProps) {
|
||||||
|
const { root, appId } = await params;
|
||||||
|
console.log('root', root, 'appId', appId);
|
||||||
|
return <AppProvider />;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { QueryProxy, RouterViewItem } from '@kevisual/api/proxy'
|
|||||||
import { app } from '@/agent/index.ts'
|
import { app } from '@/agent/index.ts'
|
||||||
import { use, useEffect, useState } from 'react'
|
import { use, useEffect, useState } from 'react'
|
||||||
import { flexRender, useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
|
import { flexRender, useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
|
||||||
import { RefreshCw, Info, MoreVertical, Edit, Trash2, Download, Save, ExternalLink, Code } from 'lucide-react'
|
import { RefreshCw, Info, MoreVertical, Edit, Trash2, Download, Save, ExternalLink, Code, Delete } from 'lucide-react'
|
||||||
import { toast, ToastContainer, Slide } from 'react-toastify'
|
import { toast, ToastContainer, Slide } from 'react-toastify'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -192,14 +192,22 @@ export const QueryView = (props: Props) => {
|
|||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<h2 className={`font-bold ${type === 'component' ? 'text-lg' : 'text-2xl'} truncate`} title={`路由视图 - ${viewData?.title || '未命名'}`}>路由视图 - {viewData?.title || '未命名'}</h2>
|
<h2 className={`font-bold ${type === 'component' ? 'text-lg' : 'text-2xl'} truncate`} title={`路由视图 - ${viewData?.title || '未命名'}`}>路由视图 - {viewData?.title || '未命名'}</h2>
|
||||||
<div className='flex items-center gap-2 relative'>
|
<div className='flex items-center gap-2 relative'>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className='p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50'
|
className='p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50'
|
||||||
title='刷新'
|
title='刷新'
|
||||||
>
|
>
|
||||||
<RefreshCw size={20} className={isLoading ? 'animate-spin' : ''} />
|
<RefreshCw size={20} className={isLoading ? 'animate-spin' : 'cursor-pointer'} />
|
||||||
</button>
|
</button>
|
||||||
|
{!isPage && <button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isLoading}
|
||||||
|
className='p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50'
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className='cursor-pointer' />
|
||||||
|
</button>}
|
||||||
|
|
||||||
<DropdownMenu open={showMoreMenu} onOpenChange={setShowMoreMenu}>
|
<DropdownMenu open={showMoreMenu} onOpenChange={setShowMoreMenu}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export const App = () => {
|
|||||||
|
|
||||||
<button className="p-1.5 rounded-md text-gray-20 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
<button className="p-1.5 rounded-md text-gray-20 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
||||||
title="高级运行"
|
title="高级运行"
|
||||||
onClick={() => run(route)}>
|
onClick={() => run(route, 'custom')}>
|
||||||
<MonitorPlay size={14} strokeWidth={2.5} />
|
<MonitorPlay size={14} strokeWidth={2.5} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { QueryProxy, RouterViewData, RouterViewItem, pickRouterViewData } from '@kevisual/api'
|
import { QueryProxy, RouterViewData, RouterViewItem, pickRouterViewData } from '@kevisual/api'
|
||||||
import { query } from '@/modules/query.ts'
|
import { getUrl, query } from '@/modules/query.ts'
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { use } from '@kevisual/context'
|
import { use } from '@kevisual/context'
|
||||||
import { MyCache } from '@kevisual/cache'
|
// import { MyCache } from '@kevisual/cache'
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { app } from '@/agent/index.ts'
|
import { app } from '@/agent/index.ts'
|
||||||
import { cloneDeep, random } from 'es-toolkit'
|
import { cloneDeep, random } from 'es-toolkit'
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { filter } from '@kevisual/js-filter';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
const historyReplace = (url: string) => {
|
const historyReplace = (url: string) => {
|
||||||
if (window.history.replaceState) {
|
if (window.history.replaceState) {
|
||||||
window.history.replaceState(null, '', url);
|
window.history.replaceState(null, '', url);
|
||||||
@@ -30,7 +32,7 @@ interface StudioState {
|
|||||||
setLoading: (loading: boolean) => void;
|
setLoading: (loading: boolean) => void;
|
||||||
routes: Array<RouteItem>;
|
routes: Array<RouteItem>;
|
||||||
searchRoutes: (keyword: string) => Promise<void>;
|
searchRoutes: (keyword: string) => Promise<void>;
|
||||||
run: (route: RouteItem) => Promise<void>;
|
run: (route: RouteItem, type?: 'normal' | 'custom') => Promise<void>;
|
||||||
queryProxy?: QueryProxy;
|
queryProxy?: QueryProxy;
|
||||||
init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>;
|
init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>;
|
||||||
routeViewList: RouteViewList;
|
routeViewList: RouteViewList;
|
||||||
@@ -64,8 +66,20 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
searchRoutes: async (keyword: string) => {
|
searchRoutes: async (keyword: string) => {
|
||||||
const state = await get().init();
|
const state = await get().init();
|
||||||
let queryProxy = state.queryProxy;
|
let queryProxy = state.queryProxy;
|
||||||
const routes: any[] = await queryProxy.listRoutes(() => true, { query: keyword });
|
console.log('搜索路由', keyword);
|
||||||
|
// const routes: any[] = await queryProxy.listRoutes(() => true, { query: keyword });
|
||||||
|
if (keyword.toLocaleUpperCase().startsWith('WHERE')) {
|
||||||
|
const routes = filter(queryProxy.router.routes, keyword);
|
||||||
set({ routes });
|
set({ routes });
|
||||||
|
} else {
|
||||||
|
const fuse = new Fuse(queryProxy.router.routes, {
|
||||||
|
keys: ['path', 'key', 'description'],
|
||||||
|
threshold: 0.4,
|
||||||
|
});
|
||||||
|
const result = fuse.search(keyword);
|
||||||
|
const routes = result.map(r => r.item) as any[];
|
||||||
|
set({ routes });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
currentView: undefined,
|
currentView: undefined,
|
||||||
queryRouteList: async () => {
|
queryRouteList: async () => {
|
||||||
@@ -152,10 +166,11 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
set({ routeViewList: [...newList] });
|
set({ routeViewList: [...newList] });
|
||||||
console.log('删除视图项', id, newList);
|
console.log('删除视图项', id, newList);
|
||||||
},
|
},
|
||||||
run: async (route: RouteItem) => {
|
run: async (route: RouteItem, type?: 'normal' | 'custom') => {
|
||||||
const state = await get().init();
|
const state = await get().init();
|
||||||
|
const isCustom = type === 'custom';
|
||||||
let queryProxy = state.queryProxy;
|
let queryProxy = state.queryProxy;
|
||||||
const showRightPanel = get().showRightPanel;
|
let showRightPanel = get().showRightPanel;
|
||||||
const action = {
|
const action = {
|
||||||
path: route.path,
|
path: route.path,
|
||||||
key: route.key
|
key: route.key
|
||||||
@@ -164,7 +179,14 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
toast.error(`运行失败:${res.message || '未知错误'}`);
|
toast.error(`运行失败:${res.message || '未知错误'}`);
|
||||||
} else if (res.code === 200) {
|
} else if (res.code === 200) {
|
||||||
//
|
if (!showRightPanel) {
|
||||||
|
if (isCustom) {
|
||||||
|
showRightPanel = true;
|
||||||
|
set({ showRightPanel: true });
|
||||||
|
} else {
|
||||||
|
toast.success('运行成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (showRightPanel) {
|
if (showRightPanel) {
|
||||||
if (route.metadata && route.metadata?.viewItem) {
|
if (route.metadata && route.metadata?.viewItem) {
|
||||||
@@ -190,6 +212,14 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
return { queryProxy };
|
return { queryProxy };
|
||||||
}
|
}
|
||||||
let currentView: RouterViewData | undefined = get().currentView;
|
let currentView: RouterViewData | undefined = get().currentView;
|
||||||
|
let url = '/api/router';
|
||||||
|
const _url = new URL(location.href)
|
||||||
|
const pathname = _url.pathname
|
||||||
|
const [user, repo] = pathname.split('/').filter(Boolean)
|
||||||
|
if (repo === 'v1') {
|
||||||
|
url = pathname;
|
||||||
|
}
|
||||||
|
console.log('初始化 QueryProxy,URL:', url);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const routerViewData: RouterViewData = currentView || {
|
const routerViewData: RouterViewData = currentView || {
|
||||||
_id: nanoid(16),
|
_id: nanoid(16),
|
||||||
@@ -206,7 +236,7 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
description: '',
|
description: '',
|
||||||
type: 'api',
|
type: 'api',
|
||||||
api: {
|
api: {
|
||||||
url: '/api/router'
|
url: url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Query } from "@kevisual/query"
|
import { Query } from "@kevisual/query"
|
||||||
import { QueryRouterServer } from "@kevisual/router"
|
import { QueryRouterServer } from "@kevisual/router/browser"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
|
|
||||||
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker;
|
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker;
|
||||||
@@ -72,7 +72,7 @@ export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNestedChange = (parent: string, field: string, value: any) => {
|
const handleNestedChange = (parent: string, field: string, value: any) => {
|
||||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
const parentValue = item[parent as keyof RouterViewItem] as any
|
||||||
const newParentValue: Record<string, any> = {
|
const newParentValue: Record<string, any> = {
|
||||||
...(parentValue || {}),
|
...(parentValue || {}),
|
||||||
[field]: value
|
[field]: value
|
||||||
@@ -81,7 +81,7 @@ export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => {
|
const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => {
|
||||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
const parentValue = item[parent as keyof RouterViewItem] as any
|
||||||
const nestedValue = parentValue?.[nestedParent] as Record<string, any> | undefined
|
const nestedValue = parentValue?.[nestedParent] as Record<string, any> | undefined
|
||||||
const newNestedValue: Record<string, any> = {
|
const newNestedValue: Record<string, any> = {
|
||||||
...(nestedValue || {}),
|
...(nestedValue || {}),
|
||||||
|
|||||||
40
web/src/components/ui/sonner.tsx
Normal file
40
web/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CircleCheckIcon,
|
||||||
|
InfoIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
OctagonXIcon,
|
||||||
|
TriangleAlertIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
|
info: <InfoIcon className="size-4" />,
|
||||||
|
warning: <TriangleAlertIcon className="size-4" />,
|
||||||
|
error: <OctagonXIcon className="size-4" />,
|
||||||
|
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "var(--radius)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { QueryClient } from '@kevisual/query'
|
import { QueryClient } from '@kevisual/query'
|
||||||
|
|
||||||
const getUrl = () => {
|
export const getUrl = () => {
|
||||||
const _url = new URL(window.location.href)
|
if (typeof window === 'undefined') return '/client/router';
|
||||||
|
const _url = new URL(location.href)
|
||||||
const pathname = _url.pathname
|
const pathname = _url.pathname
|
||||||
const [user, repo] = pathname.split('/').filter(Boolean)
|
const [user, repo] = pathname.split('/').filter(Boolean)
|
||||||
if (repo === 'v1') {
|
if (repo === 'v1') {
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ async function proxyRequest(request: NextRequest, targetHost: string): Promise<N
|
|||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
const method = request.method;
|
||||||
|
const [username, appKey] = pathname.split('/').filter(Boolean);
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const path = searchParams.get('path') || '';
|
||||||
// 代理 /api 请求
|
// 代理 /api 请求
|
||||||
if (pathname.startsWith('/api')) {
|
if (pathname.startsWith('/api')) {
|
||||||
return proxyRequest(request, API_URL);
|
return proxyRequest(request, API_URL);
|
||||||
}
|
}
|
||||||
// 代理 /root/home 或 /root/home/ 请求 (第二个路径段后可以为空或以 / 结尾)
|
// 代理 /root/home 或 /root/home/ 请求 (第二个路径段后可以为空或以 / 结尾)
|
||||||
if (pathname.startsWith('/root')) {
|
if (pathname.startsWith('/root') && appKey !== 'v1') {
|
||||||
|
return proxyRequest(request, API_URL);
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/root') && path && appKey === 'v1') {
|
||||||
return proxyRequest(request, API_URL);
|
return proxyRequest(request, API_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "@kevisual/types/json/next.json",
|
"extends": "@kevisual/types/json/next.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types"
|
"node_modules/@types"
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
Reference in New Issue
Block a user