Initial commit: TaskPool React panel
- React frontend with route-level code splitting - Backend rebranded from Baihu to TaskPool - DB brand migration script and local compatibility
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
Boxes,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Package,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Terminal as TerminalIcon,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { api, type MiseLanguage } from '@/api'
|
||||
import { useMiseList } from '@/api/hooks'
|
||||
import { Button, Card, Input, Modal, PageHeader, Select, Tabs } from '@/components/ui'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type DisplayLang = MiseLanguage & { sourceText: string }
|
||||
|
||||
function sourceText(source: MiseLanguage['source']) {
|
||||
if (!source) return '-'
|
||||
if (typeof source === 'string') return source
|
||||
return source.path || source.type || '-'
|
||||
}
|
||||
|
||||
export default function Languages() {
|
||||
const navigate = useNavigate()
|
||||
const [tab, setTab] = useState<'runtimes' | 'envs'>('runtimes')
|
||||
const [installOpen, setInstallOpen] = useState(false)
|
||||
const [plugins, setPlugins] = useState<string[]>([])
|
||||
const [versions, setVersions] = useState<string[]>([])
|
||||
const [plugin, setPlugin] = useState('python')
|
||||
const [version, setVersion] = useState('')
|
||||
const [cmdOpen, setCmdOpen] = useState(false)
|
||||
const [command, setCommand] = useState('')
|
||||
const [envs, setEnvs] = useState<Record<string, string>>({})
|
||||
const [envKey, setEnvKey] = useState('')
|
||||
const [envValue, setEnvValue] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [showTip, setShowTip] = useState(() => localStorage.getItem('taskpool_show_mise_extension_tip') !== '0')
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useMiseList()
|
||||
const list: DisplayLang[] = useMemo(
|
||||
() =>
|
||||
(Array.isArray(data) ? data : []).map((item) => ({
|
||||
...item,
|
||||
sourceText: sourceText(item.source),
|
||||
})),
|
||||
[data],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'envs') return
|
||||
api.mise
|
||||
.getEnvs()
|
||||
.then((res) => setEnvs(res || {}))
|
||||
.catch(() => setEnvs({}))
|
||||
}, [tab])
|
||||
|
||||
useEffect(() => {
|
||||
if (!installOpen) return
|
||||
api.mise
|
||||
.plugins()
|
||||
.then((res) => {
|
||||
const arr = Array.isArray(res) ? res : []
|
||||
setPlugins(arr)
|
||||
if (arr.length && !arr.includes(plugin)) setPlugin(arr[0]!)
|
||||
})
|
||||
.catch(() => setPlugins(['python', 'node', 'go', 'php', 'ruby']))
|
||||
}, [installOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!installOpen || !plugin) return
|
||||
setVersions([])
|
||||
setVersion('')
|
||||
api.mise
|
||||
.versions(plugin)
|
||||
.then((res) => {
|
||||
const arr = Array.isArray(res) ? res : []
|
||||
setVersions(arr)
|
||||
if (arr[0]) setVersion(arr[0])
|
||||
})
|
||||
.catch(() => setVersions([]))
|
||||
}, [installOpen, plugin])
|
||||
|
||||
async function sync() {
|
||||
setBusy(true)
|
||||
try {
|
||||
await api.mise.sync()
|
||||
setMessage('本地环境同步成功')
|
||||
toast.success('本地环境同步成功')
|
||||
refetch()
|
||||
} catch (e: any) {
|
||||
setMessage(e?.message || '同步失败')
|
||||
toast.error(e?.message || '同步失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function openInstallCmd() {
|
||||
if (!plugin || !version) return
|
||||
setCommand(`mise install ${plugin}@${version}`)
|
||||
setInstallOpen(false)
|
||||
setCmdOpen(true)
|
||||
}
|
||||
|
||||
function uninstallCmd(lang: DisplayLang) {
|
||||
setCommand(`mise uninstall ${lang.plugin}@${lang.version}`)
|
||||
setCmdOpen(true)
|
||||
}
|
||||
|
||||
async function verify(lang: DisplayLang) {
|
||||
try {
|
||||
const res = await api.mise.verifyCommand(lang.plugin, lang.version)
|
||||
setCommand(res.command)
|
||||
setCmdOpen(true)
|
||||
} catch (e: any) {
|
||||
setMessage(e?.message || '获取验证命令失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDefault(lang: DisplayLang) {
|
||||
setBusy(true)
|
||||
try {
|
||||
if (lang.is_global) {
|
||||
await api.mise.unsetGlobal(lang.plugin, lang.version)
|
||||
setMessage(`已取消 ${lang.plugin} 的全局默认`)
|
||||
} else {
|
||||
await api.mise.useGlobal(lang.plugin, lang.version)
|
||||
setMessage(`已将 ${lang.plugin} ${lang.version} 设为全局默认`)
|
||||
}
|
||||
refetch()
|
||||
} catch (e: any) {
|
||||
setMessage(e?.message || '操作失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEnv() {
|
||||
if (!envKey.trim()) return
|
||||
setBusy(true)
|
||||
try {
|
||||
await api.mise.setEnv(envKey.trim(), envValue)
|
||||
setEnvKey('')
|
||||
setEnvValue('')
|
||||
const res = await api.mise.getEnvs()
|
||||
setEnvs(res || {})
|
||||
setMessage('环境变量已保存')
|
||||
} catch (e: any) {
|
||||
setMessage(e?.message || '保存失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEnv(key: string) {
|
||||
setBusy(true)
|
||||
try {
|
||||
await api.mise.unsetEnv(key)
|
||||
const res = await api.mise.getEnvs()
|
||||
setEnvs(res || {})
|
||||
} catch (e: any) {
|
||||
setMessage(e?.message || '删除失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function closeTip() {
|
||||
setShowTip(false)
|
||||
localStorage.setItem('taskpool_show_mise_extension_tip', '0')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="语言依赖"
|
||||
description="管理编程语言及相关包依赖 (Mise)"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => refetch()} disabled={isFetching}>
|
||||
<RefreshCw size={14} className={isFetching ? 'animate-spin' : ''} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={sync} disabled={busy}>
|
||||
同步
|
||||
</Button>
|
||||
<Button onClick={() => setInstallOpen(true)}>
|
||||
<Plus size={14} />
|
||||
新增语言
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{message ? (
|
||||
<div className="mb-3 rounded-md border border-[var(--border)] bg-[var(--bg-tertiary)] px-3 py-2 text-sm">
|
||||
{message}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-sm text-amber-600 dark:text-amber-400">
|
||||
<Info size={16} className="mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<b>设为默认</b>:将选定版本设为系统全局默认,未指定环境的任务将调用此版本。
|
||||
</span>
|
||||
</div>
|
||||
{showTip ? (
|
||||
<div className="flex items-start justify-between gap-2 rounded-lg border border-blue-500/20 bg-blue-500/10 px-3 py-2 text-sm text-blue-600 dark:text-blue-400">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info size={16} className="mt-0.5 shrink-0" />
|
||||
<span>Mise 也可管理其它工具插件,可在终端安装后通过同步展示。</span>
|
||||
</div>
|
||||
<button className="text-xs opacity-70 hover:opacity-100" onClick={closeTip}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={(v) => setTab(v as 'runtimes' | 'envs')}
|
||||
items={[
|
||||
{ value: 'runtimes', label: '运行时' },
|
||||
{ value: 'envs', label: '加速/环境' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tab === 'runtimes' ? (
|
||||
<Card className="overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-6 text-[var(--text-muted)]">加载中...</div>
|
||||
) : list.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-[var(--text-muted)]">
|
||||
<Boxes className="mx-auto mb-2 opacity-50" size={28} />
|
||||
暂无语言运行时,点击「新增语言」安装
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--border)]">
|
||||
{list.map((lang, idx) => (
|
||||
<div
|
||||
key={`${lang.plugin}-${lang.version}-${idx}`}
|
||||
className="flex flex-col gap-3 p-4 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{lang.plugin} {lang.version}
|
||||
</span>
|
||||
{lang.is_global ? (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-green-500/15 px-1.5 py-0.5 text-xs text-green-500">
|
||||
<CheckCircle2 size={12} />
|
||||
默认
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[var(--text-muted)]">
|
||||
来源: {lang.sourceText} · {lang.install_path || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/dependencies',
|
||||
search: { language: lang.plugin, version: lang.version },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Package size={14} />
|
||||
依赖
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => toggleDefault(lang)} disabled={busy}>
|
||||
{lang.is_global ? '取消默认' : '设为默认'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => verify(lang)}>
|
||||
<TerminalIcon size={14} />
|
||||
验证
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => uninstallCmd(lang)}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Card className="space-y-3 p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Zap size={16} />
|
||||
Mise 环境变量(镜像加速等)
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-[1fr_1fr_auto]">
|
||||
<Input placeholder="KEY" value={envKey} onChange={(e) => setEnvKey(e.target.value)} />
|
||||
<Input placeholder="VALUE" value={envValue} onChange={(e) => setEnvValue(e.target.value)} />
|
||||
<Button onClick={saveEnv} disabled={busy || !envKey.trim()}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="overflow-hidden">
|
||||
{Object.keys(envs).length === 0 ? (
|
||||
<div className="p-6 text-sm text-[var(--text-muted)]">暂无环境变量</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--border)]">
|
||||
{Object.entries(envs).map(([k, v]) => (
|
||||
<div key={k} className="flex items-center justify-between gap-3 p-4 text-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono font-medium">{k}</div>
|
||||
<div className="truncate text-xs text-[var(--text-muted)]">{v}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="danger" onClick={() => removeEnv(k)} disabled={busy}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={installOpen}
|
||||
title="安装语言运行时"
|
||||
onClose={() => setInstallOpen(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => setInstallOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={openInstallCmd} disabled={!plugin || !version}>
|
||||
生成安装命令
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Select value={plugin} onChange={(e) => setPlugin(e.target.value)}>
|
||||
{(plugins.length ? plugins : ['python', 'node', 'go', 'php', 'ruby']).map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select value={version} onChange={(e) => setVersion(e.target.value)}>
|
||||
{versions.length === 0 ? <option value="">加载版本中...</option> : null}
|
||||
{versions.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={cmdOpen}
|
||||
title="终端命令"
|
||||
onClose={() => setCmdOpen(false)}
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command)
|
||||
setMessage('命令已复制')
|
||||
} catch {
|
||||
setMessage('复制失败')
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setCmdOpen(false)
|
||||
await sync()
|
||||
}}
|
||||
>
|
||||
关闭并同步
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<pre className={cn('overflow-auto rounded-md bg-[var(--bg-primary)] p-3 font-mono text-xs')}>{command}</pre>
|
||||
<p className="mt-2 text-xs text-[var(--text-muted)]">请在「终端命令」页面或系统终端执行上述命令。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user