feat: add mirror settings #29
This commit is contained in:
@@ -269,7 +269,10 @@ export const api = {
|
||||
versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`),
|
||||
verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`),
|
||||
useGlobal: (plugin: string, version: string) => request<void>('/mise/use-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }),
|
||||
unsetGlobal: (plugin: string, version: string) => request<void>('/mise/unset-global', { method: 'POST', body: JSON.stringify({ plugin, version }) })
|
||||
unsetGlobal: (plugin: string, version: string) => request<void>('/mise/unset-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }),
|
||||
getEnvs: () => request<Record<string, string>>('/mise/envs'),
|
||||
setEnv: (key: string, value: string) => request<void>('/mise/envs', { method: 'POST', body: JSON.stringify({ key, value }) }),
|
||||
unsetEnv: (key: string) => request<void>(`/mise/envs?key=${key}`, { method: 'DELETE' })
|
||||
},
|
||||
terminal: {
|
||||
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
|
||||
|
||||
@@ -10,8 +10,9 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
Plus, Globe, Search, RefreshCw, Loader2, Trash2,
|
||||
Terminal as TerminalIcon, X, AlertCircle,
|
||||
Check, ChevronsUpDown
|
||||
Check, ChevronsUpDown, ArrowRight
|
||||
} from 'lucide-vue-next'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { api, type MiseLanguage } from '@/api'
|
||||
import { toast } from 'vue-sonner'
|
||||
import XTerminal from '@/components/XTerminal.vue'
|
||||
@@ -56,12 +57,67 @@ const filteredPlugins = computed(() => {
|
||||
return availablePlugins.value.filter(p => p.toLowerCase().includes(s))
|
||||
})
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = availableVersions.value
|
||||
if (!versionSearch.value) return list
|
||||
const s = versionSearch.value.toLowerCase()
|
||||
return list.filter(v => v.toLowerCase().includes(s))
|
||||
})
|
||||
const activeTab = ref('runtimes')
|
||||
|
||||
// 全局变量相关
|
||||
const globalEnvs = ref<Record<string, string>>({})
|
||||
const loadingEnvs = ref(false)
|
||||
const showAddEnvDialog = ref(false)
|
||||
const newEnvKey = ref('')
|
||||
const newEnvValue = ref('')
|
||||
|
||||
const ENV_PRESETS = [
|
||||
{ label: 'npm (淘宝镜像)', key: 'NPM_CONFIG_REGISTRY', value: 'https://registry.npmmirror.com' },
|
||||
{ label: 'Python (清华镜像)', key: 'PIP_INDEX_URL', value: 'https://pypi.tuna.tsinghua.edu.cn/simple' },
|
||||
{ label: 'Go (goproxy.cn)', key: 'GOPROXY', value: 'https://goproxy.cn,direct' },
|
||||
{ label: 'Rust (Sparse protocol)', key: 'CARGO_REGISTRY_CRATES_IO_PROTOCOL', value: 'sparse' },
|
||||
{ label: 'Rust (中科大镜像)', key: 'CARGO_REGISTRIES_CRATES_IO_INDEX', value: 'https://mirrors.ustc.edu.cn/crates.io-index' },
|
||||
]
|
||||
|
||||
async function loadGlobalEnvs() {
|
||||
loadingEnvs.value = true
|
||||
try {
|
||||
globalEnvs.value = await api.mise.getEnvs()
|
||||
} catch (e) {
|
||||
toast.error('获取全局环境变量失败: ' + e)
|
||||
} finally {
|
||||
loadingEnvs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetEnv(key: string, value: string) {
|
||||
try {
|
||||
await api.mise.setEnv(key, value)
|
||||
toast.success(`环境变量 ${key} 设置成功`)
|
||||
loadGlobalEnvs()
|
||||
} catch (e) {
|
||||
toast.error('设置失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnsetEnv(key: string) {
|
||||
try {
|
||||
await api.mise.unsetEnv(key)
|
||||
toast.success(`环境变量 ${key} 已移除`)
|
||||
loadGlobalEnvs()
|
||||
} catch (e) {
|
||||
toast.error('移除失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreset(preset: { key: string, value: string }) {
|
||||
newEnvKey.value = preset.key
|
||||
newEnvValue.value = preset.value
|
||||
showAddEnvDialog.value = true
|
||||
}
|
||||
|
||||
function startAddEnv() {
|
||||
if (!newEnvKey.value.trim()) return
|
||||
handleSetEnv(newEnvKey.value.trim(), newEnvValue.value.trim())
|
||||
showAddEnvDialog.value = false
|
||||
newEnvKey.value = ''
|
||||
newEnvValue.value = ''
|
||||
}
|
||||
|
||||
const showTerminalDialog = ref(false)
|
||||
const terminalCommand = ref('')
|
||||
@@ -73,6 +129,13 @@ const filteredLanguages = computed(() => {
|
||||
return languages.value.filter(l => l.plugin.toLowerCase().includes(q) || l.version.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = availableVersions.value
|
||||
if (!versionSearch.value) return list
|
||||
const s = versionSearch.value.toLowerCase()
|
||||
return list.filter(v => v.toLowerCase().includes(s))
|
||||
})
|
||||
|
||||
async function loadLanguages() {
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
@@ -260,7 +323,10 @@ async function toggleDefault(lang: MiseLanguage) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadLanguages)
|
||||
onMounted(() => {
|
||||
loadLanguages()
|
||||
loadGlobalEnvs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -287,103 +353,172 @@ onMounted(loadLanguages)
|
||||
<p class="text-sm font-medium">{{ errorMsg }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 列表部分 -->
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between px-4 py-3 border-b bg-muted/30 gap-3">
|
||||
<div class="relative w-full sm:w-64">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="searchQuery" placeholder="搜索语言或版本..." class="h-9 pl-8 text-sm" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
<Button variant="outline" class="h-9 px-3 text-sm flex-1 sm:flex-none"
|
||||
@click="showSyncConfirm = true" :disabled="syncing || loading">
|
||||
<RefreshCw class="h-4 w-4 sm:mr-2" :class="{ 'animate-spin': syncing }" />
|
||||
<span class="hidden sm:inline">更新本地环境</span>
|
||||
<span class="sm:hidden">同步</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLanguages"
|
||||
:disabled="loading">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full sm:w-72 grid-cols-2 mb-4">
|
||||
<TabsTrigger value="runtimes">安装列表</TabsTrigger>
|
||||
<TabsTrigger value="envs">镜像加速</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div class="divide-y max-h-[600px] overflow-y-auto min-h-[200px]">
|
||||
<div v-if="loading && languages.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin mx-auto mb-2 opacity-20" />
|
||||
正在扫描运行环境...
|
||||
</div>
|
||||
<div v-else-if="filteredLanguages.length === 0 && !loading"
|
||||
class="text-center py-12 text-muted-foreground">
|
||||
<Globe class="h-12 w-12 mx-auto mb-2 opacity-10" />
|
||||
{{ searchQuery ? '未找到匹配的语言' : '未发现已安装的语言' }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-for="lang in filteredLanguages" :key="lang.plugin + lang.version"
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between px-4 py-4 hover:bg-muted/50 transition-colors gap-4">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-primary/10 flex items-center justify-center font-bold text-primary uppercase overflow-hidden shrink-0">
|
||||
<template v-if="getLangIcon(lang.plugin)">
|
||||
<div class="w-full h-full bg-white/80 p-2 flex items-center justify-center">
|
||||
<img :src="getLangIcon(lang.plugin)" :alt="lang.plugin"
|
||||
class="w-full h-full object-contain" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ lang.plugin.length > 2 ? lang.plugin.substring(0, 2) : lang.plugin }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold capitalize truncate">{{ lang.plugin }}</span>
|
||||
<Badge variant="outline" class="font-mono whitespace-nowrap">{{ lang.version }}
|
||||
</Badge>
|
||||
<Badge v-if="lang.isGlobal" variant="secondary"
|
||||
class="bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20 text-[10px] h-5 px-1.5 font-normal">
|
||||
默认
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1 space-y-0.5">
|
||||
<div class="font-mono opacity-60 truncate" :title="lang.source">来源: {{ lang.source
|
||||
}}
|
||||
</div>
|
||||
<div v-if="lang.installed_at" class="opacity-50">
|
||||
添加日期: {{ lang.installed_at }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="runtimes" class="space-y-4 outline-none">
|
||||
<!-- 列表部分 -->
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between px-4 py-3 border-b bg-muted/30 gap-3">
|
||||
<div class="relative w-full sm:w-64">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="searchQuery" placeholder="搜索语言或版本..." class="h-9 pl-8 text-sm" />
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 sm:ml-auto w-full sm:w-auto overflow-x-auto pb-1 sm:pb-0 hide-scrollbar">
|
||||
<Button v-if="SUPPORTED_DEPS_LANGS.includes(lang.plugin)" variant="outline" size="sm"
|
||||
class="whitespace-nowrap flex-1 sm:flex-none"
|
||||
@click="$router.push(`/dependencies?language=${lang.plugin}&version=${lang.version}`)">
|
||||
依赖管理
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
<Button variant="outline" class="h-9 px-3 text-sm flex-1 sm:flex-none"
|
||||
@click="showSyncConfirm = true" :disabled="syncing || loading">
|
||||
<RefreshCw class="h-4 w-4 sm:mr-2" :class="{ 'animate-spin': syncing }" />
|
||||
<span class="hidden sm:inline">更新本地环境</span>
|
||||
<span class="sm:hidden">同步</span>
|
||||
</Button>
|
||||
<Badge v-else variant="secondary"
|
||||
class="h-8 opacity-60 flex-1 sm:flex-none justify-center whitespace-nowrap">
|
||||
不支持管理
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" class="whitespace-nowrap flex-1 sm:flex-none"
|
||||
@click="handleVerify(lang)">
|
||||
环境验证
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"
|
||||
:class="cn('whitespace-nowrap flex-1 sm:flex-none', lang.isGlobal && 'text-amber-600 border-amber-500/50 bg-amber-500/5 hover:bg-amber-500/10')"
|
||||
@click="toggleDefault(lang)">
|
||||
{{ lang.isGlobal ? '取消默认' : '设为默认' }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon"
|
||||
class="text-destructive h-8 w-8 shrink-0 ml-auto sm:ml-0" @click="confirmDelete(lang)"
|
||||
title="卸载">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLanguages"
|
||||
:disabled="loading">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y max-h-[600px] overflow-y-auto min-h-[200px]">
|
||||
<div v-if="loading && languages.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin mx-auto mb-2 opacity-20" />
|
||||
正在扫描运行环境...
|
||||
</div>
|
||||
<div v-else-if="filteredLanguages.length === 0 && !loading"
|
||||
class="text-center py-12 text-muted-foreground">
|
||||
<Globe class="h-12 w-12 mx-auto mb-2 opacity-10" />
|
||||
{{ searchQuery ? '未找到匹配的语言' : '未发现已安装的语言' }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-for="lang in filteredLanguages" :key="lang.plugin + lang.version"
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between px-4 py-4 hover:bg-muted/50 transition-colors gap-4">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-primary/10 flex items-center justify-center font-bold text-primary uppercase overflow-hidden shrink-0">
|
||||
<template v-if="getLangIcon(lang.plugin)">
|
||||
<div class="w-full h-full bg-white/80 p-2 flex items-center justify-center">
|
||||
<img :src="getLangIcon(lang.plugin)" :alt="lang.plugin"
|
||||
class="w-full h-full object-contain" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ lang.plugin.length > 2 ? lang.plugin.substring(0, 2) : lang.plugin }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold capitalize truncate">{{ lang.plugin }}</span>
|
||||
<Badge variant="outline" class="font-mono whitespace-nowrap">{{ lang.version }}
|
||||
</Badge>
|
||||
<Badge v-if="lang.isGlobal" variant="secondary"
|
||||
class="bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20 text-[10px] h-5 px-1.5 font-normal">
|
||||
默认
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1 space-y-0.5">
|
||||
<div class="font-mono opacity-60 truncate" :title="lang.source">来源: {{ lang.source
|
||||
}}
|
||||
</div>
|
||||
<div v-if="lang.installed_at" class="opacity-50">
|
||||
添加日期: {{ lang.installed_at }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 sm:ml-auto w-full sm:w-auto overflow-x-auto pb-1 sm:pb-0 hide-scrollbar">
|
||||
<Button v-if="SUPPORTED_DEPS_LANGS.includes(lang.plugin)" variant="outline" size="sm"
|
||||
class="whitespace-nowrap flex-1 sm:flex-none"
|
||||
@click="$router.push(`/dependencies?language=${lang.plugin}&version=${lang.version}`)">
|
||||
依赖管理
|
||||
</Button>
|
||||
<Badge v-else variant="secondary"
|
||||
class="h-8 opacity-60 flex-1 sm:flex-none justify-center whitespace-nowrap">
|
||||
不支持管理
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" class="whitespace-nowrap flex-1 sm:flex-none"
|
||||
@click="handleVerify(lang)">
|
||||
环境验证
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"
|
||||
:class="cn('whitespace-nowrap flex-1 sm:flex-none', lang.isGlobal && 'text-amber-600 border-amber-500/50 bg-amber-500/5 hover:bg-amber-500/10')"
|
||||
@click="toggleDefault(lang)">
|
||||
{{ lang.isGlobal ? '取消默认' : '设为默认' }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon"
|
||||
class="text-destructive h-8 w-8 shrink-0 ml-auto sm:ml-0" @click="confirmDelete(lang)"
|
||||
title="卸载">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="envs" class="space-y-4 outline-none">
|
||||
<div class="rounded-lg border bg-card p-6">
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold flex items-center gap-2">
|
||||
<Globe class="h-5 w-5 text-primary" />
|
||||
镜像源加速 (Mirror Acceleration)
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-1 max-w-2xl">
|
||||
通过配置全局环境变量,加速编程语言环境及依赖包(如 pip, npm, go get)的安装速度。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常用预设网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div v-for="preset in ENV_PRESETS" :key="preset.label"
|
||||
:class="cn(
|
||||
'relative p-4 rounded-xl border transition-all group overflow-hidden',
|
||||
globalEnvs[preset.key] ? 'bg-primary/5 border-primary shadow-sm' : 'bg-muted/30 hover:border-primary/50 cursor-pointer'
|
||||
)" @click="applyPreset({ key: preset.key, value: globalEnvs[preset.key] || preset.value })">
|
||||
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="font-semibold text-sm">{{ preset.label }}</div>
|
||||
<div v-if="globalEnvs[preset.key]"
|
||||
class="h-5 w-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center">
|
||||
<Check class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[10px] font-mono mb-3">
|
||||
<div class="text-muted-foreground truncate">{{ preset.key }}</div>
|
||||
<div v-if="globalEnvs[preset.key]"
|
||||
class="mt-1 text-primary/80 font-medium truncate bg-primary/5 px-1.5 py-0.5 rounded-sm border border-primary/10"
|
||||
:title="globalEnvs[preset.key] as string">
|
||||
{{ globalEnvs[preset.key] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-auto">
|
||||
<span v-if="globalEnvs[preset.key]" class="text-[10px] font-medium text-primary">
|
||||
已启用 (点击修改)
|
||||
</span>
|
||||
<span v-else class="text-[10px] font-medium text-muted-foreground group-hover:text-primary transition-colors flex items-center">
|
||||
立即启用 <ArrowRight class="h-3 w-3 ml-1 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
|
||||
<Button v-if="globalEnvs[preset.key]"
|
||||
variant="ghost" size="icon"
|
||||
class="h-6 w-6 text-destructive hover:bg-destructive/10"
|
||||
@click.stop="handleUnsetEnv(preset.key)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 安装对话框 (带搜索下拉) -->
|
||||
<Dialog v-model:open="showInstallDialog">
|
||||
@@ -504,6 +639,32 @@ onMounted(loadLanguages)
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 启用/编辑镜像加速对话框 -->
|
||||
<Dialog v-model:open="showAddEnvDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>设置镜像加速</DialogTitle>
|
||||
<DialogDescription>
|
||||
正在配置 <span class="font-bold text-primary">{{ newEnvKey }}</span>。你可以根据需要调整镜像地址。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>环境变量名 (Readonly)</Label>
|
||||
<Input v-model="newEnvKey" readonly class="bg-muted cursor-default" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>镜像源地址 (Value)</Label>
|
||||
<Input v-model="newEnvValue" placeholder="请输入镜像地址" @keydown.enter="startAddEnv" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showAddEnvDialog = false">取消</Button>
|
||||
<Button @click="startAddEnv">确认启用</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 终端对话框 -->
|
||||
<Dialog v-model:open="showTerminalDialog">
|
||||
<DialogContent
|
||||
|
||||
Reference in New Issue
Block a user