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/image-types/global" />
|
||||
import "./dist/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
"ui": "pnpm dlx shadcn@latest add "
|
||||
},
|
||||
"dependencies": {
|
||||
"@kevisual/api": "^0.0.17",
|
||||
"@kevisual/api": "^0.0.20",
|
||||
"@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",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -25,15 +26,18 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@uiw/react-md-editor": "^4.0.11",
|
||||
"antd": "^6.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"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 { 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 { RefreshCw, Info, MoreVertical, Edit, Trash2, Download, Save, ExternalLink, Code, Delete } from 'lucide-react'
|
||||
import { toast, ToastContainer, Slide } from 'react-toastify'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -192,14 +192,22 @@ export const QueryView = (props: Props) => {
|
||||
<div className='flex items-center justify-between'>
|
||||
<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'>
|
||||
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className='p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50'
|
||||
title='刷新'
|
||||
>
|
||||
<RefreshCw size={20} className={isLoading ? 'animate-spin' : ''} />
|
||||
<RefreshCw size={20} className={isLoading ? 'animate-spin' : 'cursor-pointer'} />
|
||||
</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}>
|
||||
<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"
|
||||
title="高级运行"
|
||||
onClick={() => run(route)}>
|
||||
onClick={() => run(route, 'custom')}>
|
||||
<MonitorPlay size={14} strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
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 { use } from '@kevisual/context'
|
||||
import { MyCache } from '@kevisual/cache'
|
||||
// import { MyCache } from '@kevisual/cache'
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { app } from '@/agent/index.ts'
|
||||
import { cloneDeep, random } from 'es-toolkit'
|
||||
import { nanoid } from 'nanoid';
|
||||
import { filter } from '@kevisual/js-filter';
|
||||
import Fuse from 'fuse.js';
|
||||
const historyReplace = (url: string) => {
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, '', url);
|
||||
@@ -30,7 +32,7 @@ interface StudioState {
|
||||
setLoading: (loading: boolean) => void;
|
||||
routes: Array<RouteItem>;
|
||||
searchRoutes: (keyword: string) => Promise<void>;
|
||||
run: (route: RouteItem) => Promise<void>;
|
||||
run: (route: RouteItem, type?: 'normal' | 'custom') => Promise<void>;
|
||||
queryProxy?: QueryProxy;
|
||||
init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>;
|
||||
routeViewList: RouteViewList;
|
||||
@@ -64,8 +66,20 @@ export const useStudioStore = create<StudioState>()(
|
||||
searchRoutes: async (keyword: string) => {
|
||||
const state = await get().init();
|
||||
let queryProxy = state.queryProxy;
|
||||
const routes: any[] = await queryProxy.listRoutes(() => true, { query: keyword });
|
||||
set({ routes });
|
||||
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 });
|
||||
} 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,
|
||||
queryRouteList: async () => {
|
||||
@@ -152,10 +166,11 @@ export const useStudioStore = create<StudioState>()(
|
||||
set({ routeViewList: [...newList] });
|
||||
console.log('删除视图项', id, newList);
|
||||
},
|
||||
run: async (route: RouteItem) => {
|
||||
run: async (route: RouteItem, type?: 'normal' | 'custom') => {
|
||||
const state = await get().init();
|
||||
const isCustom = type === 'custom';
|
||||
let queryProxy = state.queryProxy;
|
||||
const showRightPanel = get().showRightPanel;
|
||||
let showRightPanel = get().showRightPanel;
|
||||
const action = {
|
||||
path: route.path,
|
||||
key: route.key
|
||||
@@ -164,7 +179,14 @@ export const useStudioStore = create<StudioState>()(
|
||||
if (res.code !== 200) {
|
||||
toast.error(`运行失败:${res.message || '未知错误'}`);
|
||||
} else if (res.code === 200) {
|
||||
//
|
||||
if (!showRightPanel) {
|
||||
if (isCustom) {
|
||||
showRightPanel = true;
|
||||
set({ showRightPanel: true });
|
||||
} else {
|
||||
toast.success('运行成功');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showRightPanel) {
|
||||
if (route.metadata && route.metadata?.viewItem) {
|
||||
@@ -190,6 +212,14 @@ export const useStudioStore = create<StudioState>()(
|
||||
return { queryProxy };
|
||||
}
|
||||
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
|
||||
const routerViewData: RouterViewData = currentView || {
|
||||
_id: nanoid(16),
|
||||
@@ -206,7 +236,7 @@ export const useStudioStore = create<StudioState>()(
|
||||
description: '',
|
||||
type: 'api',
|
||||
api: {
|
||||
url: '/api/router'
|
||||
url: url
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Query } from "@kevisual/query"
|
||||
import { QueryRouterServer } from "@kevisual/router"
|
||||
import { QueryRouterServer } from "@kevisual/router/browser"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
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 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> = {
|
||||
...(parentValue || {}),
|
||||
[field]: value
|
||||
@@ -81,7 +81,7 @@ export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) =>
|
||||
}
|
||||
|
||||
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 newNestedValue: Record<string, any> = {
|
||||
...(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';
|
||||
import { QueryClient } from '@kevisual/query'
|
||||
|
||||
const getUrl = () => {
|
||||
const _url = new URL(window.location.href)
|
||||
export const getUrl = () => {
|
||||
if (typeof window === 'undefined') return '/client/router';
|
||||
const _url = new URL(location.href)
|
||||
const pathname = _url.pathname
|
||||
const [user, repo] = pathname.split('/').filter(Boolean)
|
||||
if (repo === 'v1') {
|
||||
|
||||
@@ -46,12 +46,19 @@ async function proxyRequest(request: NextRequest, targetHost: string): Promise<N
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
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 请求
|
||||
if (pathname.startsWith('/api')) {
|
||||
return proxyRequest(request, API_URL);
|
||||
}
|
||||
// 代理 /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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/next.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
@@ -9,7 +11,7 @@
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
|
||||
Reference in New Issue
Block a user