- Added a left panel toggle button in the header to show/hide the ViewList component. - Integrated loading state management in the studio store for better user feedback during data fetching. - Updated the ViewList component to utilize a dropdown menu for editing and deleting views. - Improved UI elements with consistent border styles and loading indicators. - Refactored the DataItemForm and ViewFormItem components for better user experience and added clipboard copy functionality. - Introduced a new DropdownMenu component for better dropdown handling across the application.
201 lines
7.3 KiB
TypeScript
201 lines
7.3 KiB
TypeScript
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 border-gray-300 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>
|
||
)
|
||
}
|