Files
TaskPool/web/src/views/languages/Languages.vue
T

278 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Plus, Loader2, Terminal as TerminalIcon, X, AlertCircle, Boxes, Zap, Info } from 'lucide-vue-next'
import { api, type MiseLanguage } from '@/api'
import { toast } from 'vue-sonner'
import XTerminal from '@/components/XTerminal.vue'
import { useStorage } from '@vueuse/core'
// Import components
import RuntimeListTab from './components/RuntimeListTab.vue'
import EnvMirrorTab from './components/EnvMirrorTab.vue'
import InstallLanguageDialog from './components/InstallLanguageDialog.vue'
import LanguageDetailDialog from './components/LanguageDetailDialog.vue'
interface DisplayLanguage extends Omit<MiseLanguage, 'source'> {
source: string
isGlobal: boolean
}
const activeTab = ref('runtimes')
const showExtensionTip = useStorage('baihu_show_mise_extension_tip', true)
// Shared State
const languages = ref<DisplayLanguage[]>([])
const loading = ref(false)
const errorMsg = ref('')
const syncing = ref(false)
const showSyncConfirm = ref(false)
// Dialog Refs and States
const installDialogRef = ref<InstanceType<typeof InstallLanguageDialog> | null>(null)
const showDetailDialog = ref(false)
const selectedLang = ref<DisplayLanguage | null>(null)
// Terminal State
const showTerminalDialog = ref(false)
const terminalCommand = ref('')
const isInstallSuccess = ref(false)
async function loadLanguages() {
loading.value = true
errorMsg.value = ''
try {
const data = await api.mise.list()
if (!data || !Array.isArray(data)) {
languages.value = []
return
}
languages.value = data.map(item => ({
...item,
source: typeof item.source === 'object' ? (item.source.path || item.source.type || '-') : (item.source || '-'),
isGlobal: !!item.is_global
}))
} catch (e) {
toast.error('获取语言列表失败')
errorMsg.value = String(e)
} finally {
loading.value = false
}
}
async function handleSync() {
syncing.value = true
try {
await api.mise.sync()
toast.success('本地环境同步成功')
await loadLanguages()
} catch (e) {
toast.error('同步失败: ' + e)
} finally {
syncing.value = false
showSyncConfirm.value = false
}
}
function openInstallDialog() {
installDialogRef.value?.openDialog()
}
function runInTerminal(command: string) {
terminalCommand.value = command
isInstallSuccess.value = false
showTerminalDialog.value = true
}
function handleInstall(plugin: string, version: string) {
const cmd = `mise install ${plugin}@${version}`
runInTerminal(cmd)
}
function confirmDelete(lang: MiseLanguage) {
const cmd = `mise uninstall ${lang.plugin}@${lang.version}`
runInTerminal(cmd)
}
async function handleVerify(lang: MiseLanguage) {
try {
const { command } = await api.mise.verifyCommand(lang.plugin, lang.version)
runInTerminal(command)
} catch (e) {
toast.error('获取验证命令失败')
}
}
async function toggleDefault(lang: MiseLanguage) {
try {
if (lang.is_global) {
await api.mise.unsetGlobal(lang.plugin, lang.version)
toast.success(`已取消 ${lang.plugin} 的全局默认设置`)
} else {
await api.mise.useGlobal(lang.plugin, lang.version)
toast.success(`已将 ${lang.plugin} ${lang.version} 设为全局默认版本`)
}
await loadLanguages()
} catch (e) {
toast.error('操作失败: ' + e)
}
}
async function handleTerminalClose() {
showTerminalDialog.value = false
syncing.value = true
try {
await api.mise.sync()
toast.success('环境同步完成')
} catch (e) {
toast.error('环境同步失败: ' + e)
} finally {
syncing.value = false
}
await loadLanguages()
}
function viewDetail(lang: DisplayLanguage) {
selectedLang.value = lang
showDetailDialog.value = true
}
onMounted(() => {
loadLanguages()
})
</script>
<template>
<div class="space-y-4">
<!-- 顶栏标题与操作 -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">语言依赖</h2>
<p class="text-muted-foreground text-sm mt-0.5">管理编程语言及相关包依赖(Mise)</p>
</div>
<div class="flex flex-row items-center justify-start sm:justify-end gap-2 shrink-0 w-full md:w-auto overflow-x-auto pb-1 sm:pb-0">
<Button @click="openInstallDialog" class="flex-1 sm:flex-none h-9 text-sm px-2 sm:px-4">
<Plus class="h-3.5 w-3.5 sm:mr-2" /> <span class="whitespace-nowrap">新增语言</span>
</Button>
<Tabs v-model="activeTab" class="flex-1 sm:flex-none">
<TabsList class="h-9 p-0.5 bg-muted/20 border border-border/40 rounded-lg w-full sm:w-auto flex">
<TabsTrigger value="runtimes" class="px-3 h-8 text-xs gap-1.5 font-medium transition-all flex-1 sm:flex-none">
<Boxes class="w-3.5 h-3.5 opacity-70" />
<span>运行时</span>
</TabsTrigger>
<TabsTrigger value="envs" class="px-3 h-8 text-xs gap-1.5 font-medium transition-all flex-1 sm:flex-none">
<Zap class="w-3.5 h-3.5 opacity-70" />
<span>加速</span>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
<div class="flex flex-col gap-2.5">
<div
class="flex items-center gap-2.5 text-[13px] text-amber-600 dark:text-amber-500 bg-amber-500/10 px-4 py-2.5 rounded-lg border border-amber-500/20 leading-relaxed shadow-sm select-none">
<AlertCircle class="h-4 w-4 shrink-0" />
<span>
<b class="font-bold">设为默认</b>将选定版本设为系统全局默认 (mise use -g)生效后所有未通过高级配置指定特定环境的任务将默认调用此环境
</span>
</div>
<div v-if="showExtensionTip"
class="flex items-start sm:items-center justify-between gap-2.5 text-[13px] text-blue-600 dark:text-blue-500 bg-blue-500/10 px-4 py-2.5 rounded-lg border border-blue-500/20 leading-relaxed shadow-sm select-none">
<div class="flex items-start sm:items-center gap-2.5">
<Info class="h-4 w-4 shrink-0 mt-0.5 sm:mt-0" />
<span>
<b class="font-bold">扩展支持</b>虽然本界面主要专注于编程语言环境 Mise 本质上也可以管理各类系统包和工具插件若有需要您可以直接在终端中执行命令安装其他依赖包此处将会自动同步并展示您的最新环境
</span>
</div>
<button @click="showExtensionTip = false" class="text-blue-500/60 hover:text-blue-500 transition-colors shrink-0 outline-none p-0.5 mt-0.5 sm:mt-0">
<X class="h-4 w-4" />
</button>
</div>
</div>
<Tabs v-model="activeTab" class="w-full">
<div v-if="errorMsg"
class="bg-destructive/10 border border-destructive/20 rounded-lg p-4 flex items-center gap-3 text-destructive mb-4">
<AlertCircle class="h-5 w-5 shrink-0" />
<p class="text-sm font-medium">{{ errorMsg }}</p>
</div>
<TabsContent value="runtimes" class="space-y-4 outline-none">
<RuntimeListTab
:languages="languages"
:loading="loading"
:syncing="syncing"
@sync="showSyncConfirm = true"
@refresh="loadLanguages"
@uninstall="confirmDelete"
@verify="handleVerify"
@toggle-default="toggleDefault"
@view-detail="viewDetail"
/>
</TabsContent>
<TabsContent value="envs" class="space-y-4 outline-none">
<EnvMirrorTab />
</TabsContent>
</Tabs>
<!-- 对话框组件 -->
<InstallLanguageDialog ref="installDialogRef" @install="handleInstall" />
<LanguageDetailDialog v-model:open="showDetailDialog" :lang="selectedLang" />
<!-- 终端对话框 -->
<Dialog v-model:open="showTerminalDialog">
<DialogContent
class="w-[calc(100%-2rem)] sm:max-w-[90vw] lg:max-w-4xl xl:max-w-5xl h-[60vh] sm:h-[70vh] flex flex-col p-0 overflow-hidden bg-[#1e1e1e] border-none shadow-2xl"
:show-close-button="false" @interact-outside="(e) => e.preventDefault()"
@escape-key-down="(e) => e.preventDefault()">
<DialogHeader class="sr-only">
<DialogTitle>终端执行</DialogTitle>
<DialogDescription>正在执行 mise 相关指令</DialogDescription>
</DialogHeader>
<div class="flex flex-col h-full">
<div class="flex items-center justify-between px-4 py-2 bg-[#252526] border-b border-[#3c3c3c]">
<div class="flex items-center gap-2">
<TerminalIcon class="h-4 w-4 text-white" />
<span class="text-xs font-medium text-gray-300">正在安装 / 执行: {{ terminalCommand }}</span>
</div>
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white"
@click="handleTerminalClose">
<X class="h-4 w-4" />
</Button>
</div>
<div class="flex-1">
<XTerminal v-if="showTerminalDialog" :font-size="13" :initial-command="terminalCommand"
@success="isInstallSuccess = true" @failed="isInstallSuccess = false" />
</div>
</div>
</DialogContent>
</Dialog>
<!-- 同步确认对话框 -->
<Dialog v-model:open="showSyncConfirm">
<DialogContent class="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>同步本地环境</DialogTitle>
<DialogDescription>
将实时扫描系统中已安装的所有 Mise 运行时并更新到数据库表中。
<p class="mt-2 text-destructive font-medium italic text-xs">注意:这可能会覆盖或更新表中的记录。</p>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" @click="showSyncConfirm = false" :disabled="syncing">取消</Button>
<Button @click="handleSync" :disabled="syncing">
<Loader2 v-if="syncing" class="mr-2 h-4 w-4 animate-spin" />
立即同步
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>