feat: Implement view management features with a new UI for editing and listing views

- Added a resizable panel layout in the studio app to display the view list alongside the main application.
- Refactored the studio store to include new methods for fetching and managing route views.
- Introduced a new DataItemForm component for configuring data items in views.
- Created a ViewEditor component for adding and editing views, including data items and queries.
- Enhanced the ViewList component to support searching, adding, editing, and deleting views.
- Updated UI components (Button, Checkbox, Dialog, Input, Label, Table) for better styling and functionality.
- Added environment configuration for API URL.
- Introduced a new workspace configuration for pnpm.
This commit is contained in:
2025-12-31 17:54:11 +08:00
parent 8670fd3bfc
commit 231caa3b9a
22 changed files with 1177 additions and 116 deletions

View File

@@ -0,0 +1,200 @@
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 { nanoid } from "nanoid"
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker;
type RouteViewBase = {
id: string;
title: string;
description: string;
enabled?: boolean;
}
export type RouterViewApi = {
type: 'api',
api: {
url: string,
// 已初始化的query实例不需要编辑配置
query?: Query
}
} & RouteViewBase;
export type RouterViewContext = {
type: 'context',
context: {
key: string,
// 从context中获取router不需要编辑配置
router?: QueryRouterServer
}
} & RouteViewBase;
export type RouterViewWorker = {
type: 'worker',
worker: {
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
url: string,
// 已初始化的worker实例不需要编辑配置
worker?: Worker | SharedWorker | ServiceWorker,
/**
* worker选项
* default: { type: 'module' }
*/
workerOptions?: {
type: 'module' | 'classic'
}
}
} & RouteViewBase;
interface DataItemFormProps {
item: RouterViewItem
onChange: (item: any) => void
onRemove: () => void
}
export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) => {
const handleChange = (field: string, value: any) => {
if (field === 'type') {
const newItem: RouterViewItem = { ...item, type: value }
if (value === 'api' && !('api' in item)) {
(newItem as RouterViewApi).api = { url: '' }
} else if (value === 'context' && !('context' in item)) {
(newItem as RouterViewContext).context = { key: '' }
} else if (value === 'worker' && !('worker' in item)) {
(newItem as RouterViewWorker).worker = { type: 'Worker', url: '', workerOptions: { type: 'module' } }
}
if (!newItem.id) {
newItem.id = nanoid(16)
}
onChange(newItem)
} else {
onChange({ ...item, [field]: value })
}
}
const handleNestedChange = (parent: string, field: string, value: any) => {
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
const newParentValue: Record<string, any> = {
...(parentValue || {}),
[field]: value
}
onChange({ ...item, [parent]: newParentValue })
}
const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => {
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
const nestedValue = parentValue?.[nestedParent] as Record<string, any> | undefined
const newNestedValue: Record<string, any> = {
...(nestedValue || {}),
[field]: value
}
const newParentValue: Record<string, any> = {
...(parentValue || {}),
[nestedParent]: newNestedValue
}
onChange({ ...item, [parent]: newParentValue })
}
return (
<div className="border rounded-lg p-4 mb-4 space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium"></h3>
<button
type="button"
onClick={onRemove}
className="text-sm text-red-500 hover:text-red-700"
>
</button>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={item.title || ''}
onChange={(e) => handleChange('title', e.target.value)}
placeholder="输入标题"
/>
</div>
<div className="space-y-2">
<Label></Label>
<select
value={item.type}
onChange={(e) => handleChange('type', e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="api">API</option>
<option value="context">Context</option>
<option value="worker">Worker</option>
</select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enabled"
checked={item.enabled !== false}
onCheckedChange={(checked) => handleChange('enabled', checked)}
/>
<Label htmlFor="enabled" className="cursor-pointer"></Label>
</div>
{(item.type === 'api') && (
<div className="space-y-2">
<Label>API URL</Label>
<Input
value={item.api?.url || ''}
onChange={(e) => handleNestedChange('api', 'url', e.target.value)}
placeholder="输入 API 地址"
/>
</div>
)}
{item.type === 'context' && (
<div className="space-y-2">
<Label>Context Key</Label>
<Input
value={item.context?.key || ''}
onChange={(e) => handleNestedChange('context', 'key', e.target.value)}
placeholder="输入 Context Key"
/>
</div>
)}
{item.type === 'worker' && (
<div className="space-y-4">
<div className="space-y-2">
<Label>Worker Type</Label>
<select
value={item.worker?.type || 'Worker'}
onChange={(e) => handleNestedChange('worker', 'type', e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="Worker">Worker</option>
<option value="SharedWorker">SharedWorker</option>
<option value="serviceWorker">ServiceWorker</option>
</select>
</div>
<div className="space-y-2">
<Label>Worker URL</Label>
<Input
value={item.worker?.url || ''}
onChange={(e) => handleNestedChange('worker', 'url', e.target.value)}
placeholder="输入 Worker URL"
/>
</div>
<div className="space-y-2">
<Label>Worker Options Type</Label>
<select
value={item.worker?.workerOptions?.type || 'module'}
onChange={(e) => handleNestedDeepChange('worker', 'workerOptions', 'type', e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="module">Module</option>
<option value="classic">Classic</option>
</select>
</div>
</div>
)}
</div>
)
}