码桶
发现社区成员的开源项目
PluginConfigPanel.tsx8 KB
import { useEffect, useState } from 'react'
import { api } from '@/lib/api'
import { toast } from 'sonner'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'
import { PushConfig } from '@/components/PushConfig'
import { RssConfig } from '@/components/RssConfig'
export interface PluginSettingsField {
key: string
label?: string
type?: string
placeholder?: string
help?: string
default?: string
options?: { label?: string; value: string }[]
}
export interface PluginForSettings {
id: string
name: string
settingsSchema?: PluginSettingsField[] | null
/** 可能是对象(通用 key-value)或字符串标识符(专用 UI:'push' / 'rss_feeds') */
customConfig?: Record<string, any> | string | null
configurable?: boolean
}
/** 字符串型 customConfig 对应的专用组件 */
function renderCustomUi(kind: string, botId: number) {
if (kind === 'push') return <PushConfig botId={botId} />
if (kind === 'rss_feeds') return <RssConfig botId={botId} />
return null
}
/** 单个插件的自定义设置编辑器,可复用于控制台与市场页 */
export function PluginSettingsEditor({ plugin, botId }: { plugin: PluginForSettings; botId: number }) {
const [values, setValues] = useState<Record<string, any>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const fields: PluginSettingsField[] = Array.isArray(plugin.settingsSchema) ? plugin.settingsSchema : []
const customConfigObj = plugin.customConfig && typeof plugin.customConfig === 'object' ? plugin.customConfig : null
const customConfigKind = typeof plugin.customConfig === 'string' && plugin.customConfig ? plugin.customConfig : null
const customUi = customConfigKind ? renderCustomUi(customConfigKind, botId) : null
const isConfigurable = fields.length > 0 || !!customConfigObj || !!customUi
useEffect(() => {
let active = true
setLoading(true)
api.post('/api/market/plugin_settings', { plugin_id: plugin.id, bot_id: botId })
.then(r => {
if (!active) return
if (r.ok) setValues(r.settings || {})
})
.catch(e => toast.error('读取配置失败:' + e.message))
.finally(() => { if (active) setLoading(false) })
return () => { active = false }
}, [plugin.id, botId])
if (loading) {
return <div className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />读取配置中…</div>
}
// 字符串型 customConfig:优先渲染专用 UI
if (customUi) {
return customUi
}
if (!isConfigurable) {
return <p className="text-xs text-muted-foreground">该插件没有可配置项。</p>
}
const setField = (k: string, v: any) => setValues(prev => ({ ...prev, [k]: v }))
const renderField = (f: PluginSettingsField) => {
const type = (f.type || 'text').toLowerCase()
const label = f.label || f.key
const val = values[f.key]
if (type === 'switch') {
const on = val === '1' || val === 1 || val === true
return (
<div key={f.key} className="flex items-center justify-between gap-3 rounded-md border p-2.5">
<div className="min-w-0">
<Label className="text-xs">{label}</Label>
{f.help && <p className="text-[11px] text-muted-foreground mt-0.5">{f.help}</p>}
</div>
<Switch checked={!!on} onCheckedChange={c => setField(f.key, c ? '1' : '0')} />
</div>
)
}
if (type === 'textarea') {
return (
<div key={f.key} className="space-y-1.5">
<Label className="text-xs">{label}</Label>
<Textarea value={val ?? ''} placeholder={f.placeholder || ''} onChange={e => setField(f.key, e.target.value)} />
{f.help && <p className="text-[11px] text-muted-foreground">{f.help}</p>}
</div>
)
}
if (type === 'select') {
return (
<div key={f.key} className="space-y-1.5">
<Label className="text-xs">{label}</Label>
<Select value={val ?? ''} onValueChange={v => setField(f.key, v)}>
<SelectTrigger><SelectValue placeholder={label} /></SelectTrigger>
<SelectContent>
{(f.options || []).map(o => <SelectItem key={o.value} value={o.value}>{o.label || o.value}</SelectItem>)}
</SelectContent>
</Select>
{f.help && <p className="text-[11px] text-muted-foreground">{f.help}</p>}
</div>
)
}
return (
<div key={f.key} className="space-y-1.5">
<Label className="text-xs">{label}</Label>
<Input
type={type === 'number' ? 'number' : type === 'password' ? 'password' : 'text'}
value={val ?? ''}
placeholder={f.placeholder || ''}
onChange={e => setField(f.key, e.target.value)}
/>
{f.help && <p className="text-[11px] text-muted-foreground">{f.help}</p>}
</div>
)
}
return (
<div className="space-y-3">
{fields.map(renderField)}
{customConfigObj && (
<div className="space-y-2 rounded-md border p-2">
<p className="text-xs font-medium">高级自定义配置</p>
{Object.entries(customConfigObj).map(([k, v]) => (
<div key={k} className="space-y-1">
<Label className="text-xs">{k}</Label>
<Input value={values[k] ?? String(v)} onChange={e => setField(k, e.target.value)} />
</div>
))}
</div>
)}
<Button size="sm" disabled={saving} onClick={async () => {
setSaving(true)
try {
const r = await api.post('/api/market/plugin_settings_save', { plugin_id: plugin.id, bot_id: botId, data: values })
if (r.ok) toast.success('已保存配置')
else toast.error(r.msg || '保存失败')
} catch (e: any) { toast.error('保存失败:' + e.message) } finally { setSaving(false) }
}}>
{saving && <Loader2 className="h-4 w-4 animate-spin" />} 保存配置
</Button>
</div>
)
}
/** 控制台侧栏:选择已安装可配置插件并进行设置 */
export function PluginConfigPanel({ botId }: { botId: number }) {
const [plugins, setPlugins] = useState<PluginForSettings[]>([])
const [selId, setSelId] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
let active = true
api.post('/api/market/list', { bot_id: botId, pageSize: 50 })
.then(r => {
if (!active) return
if (r.ok) {
const ps = (r.plugins || []).filter((p: any) => p.installed && p.configurable)
setPlugins(ps)
if (ps[0]) setSelId(ps[0].id)
}
})
.catch(e => toast.error(e.message))
.finally(() => { if (active) setLoading(false) })
return () => { active = false }
}, [])
if (loading) return <div className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />加载中…</div>
if (!plugins.length) return <p className="text-xs text-muted-foreground">当前没有已安装的可配置插件。</p>
const sel = plugins.find(p => p.id === selId)
return (
<div className="space-y-3">
<Select value={selId} onValueChange={setSelId}>
<SelectTrigger><SelectValue placeholder="选择插件" /></SelectTrigger>
<SelectContent>
{plugins.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
</SelectContent>
</Select>
{sel && <PluginSettingsEditor plugin={sel} botId={botId} />}
</div>
)
}