feat: add mirror settings #29

This commit is contained in:
engigu
2026-03-18 10:53:41 +08:00
parent 7190cca30b
commit a03800f196
5 changed files with 359 additions and 100 deletions
+45
View File
@@ -117,3 +117,48 @@ func (c *MiseController) UnsetGlobal(ctx *gin.Context) {
} }
utils.Success(ctx, nil) utils.Success(ctx, nil)
} }
// Envs 获取全局环境变量
func (c *MiseController) Envs(ctx *gin.Context) {
envs, err := c.service.Envs()
if err != nil {
utils.ServerError(ctx, "获取全局环境变量失败: "+err.Error())
return
}
utils.Success(ctx, envs)
}
// SetEnv 设置全局环境变量
func (c *MiseController) SetEnv(ctx *gin.Context) {
var req struct {
Key string `json:"key"`
Value string `json:"value"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
if req.Key == "" {
utils.BadRequest(ctx, "参数 key 不能为空")
return
}
if err := c.service.SetEnv(req.Key, req.Value); err != nil {
utils.ServerError(ctx, "设置环境变量失败: "+err.Error())
return
}
utils.Success(ctx, nil)
}
// UnsetEnv 取消全局环境变量
func (c *MiseController) UnsetEnv(ctx *gin.Context) {
key := ctx.Query("key")
if key == "" {
utils.BadRequest(ctx, "参数 key 不能为空")
return
}
if err := c.service.UnsetEnv(key); err != nil {
utils.ServerError(ctx, "取消环境变量失败: "+err.Error())
return
}
utils.Success(ctx, nil)
}
+3
View File
@@ -212,6 +212,9 @@ func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) {
mise.GET("/verify-cmd", c.Mise.VerifyCommand) mise.GET("/verify-cmd", c.Mise.VerifyCommand)
mise.POST("/use-global", c.Mise.UseGlobal) mise.POST("/use-global", c.Mise.UseGlobal)
mise.POST("/unset-global", c.Mise.UnsetGlobal) mise.POST("/unset-global", c.Mise.UnsetGlobal)
mise.GET("/envs", c.Mise.Envs)
mise.POST("/envs", c.Mise.SetEnv)
mise.DELETE("/envs", c.Mise.UnsetEnv)
} }
} }
+47
View File
@@ -359,3 +359,50 @@ func (s *MiseService) UnsetGlobal(plugin, version string) error {
} }
return nil return nil
} }
// Envs 获取全局环境变量
func (s *MiseService) Envs() (map[string]string, error) {
cmd := exec.Command("mise", "set")
cmd.Env = os.Environ()
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("mise set failed: %v, output: %s", err, string(output))
}
envs := make(map[string]string)
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
// 期望格式: KEY VALUE SOURCE
// 跳过标题行或不合规行
if len(fields) < 3 || strings.ToLower(fields[0]) == "key" {
continue
}
// 仅显示来自全局配置文件的环境变量
if strings.Contains(strings.ToLower(fields[len(fields)-1]), "config.toml") {
envs[fields[0]] = fields[1]
}
}
return envs, nil
}
// SetEnv 设置全局环境变量
func (s *MiseService) SetEnv(key, value string) error {
cmd := exec.Command("mise", "set", "-g", fmt.Sprintf("%s=%s", key, value))
cmd.Env = os.Environ()
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("mise set -g failed: %v, output: %s", err, string(output))
}
return nil
}
// UnsetEnv 取消全局环境变量
func (s *MiseService) UnsetEnv(key string) error {
cmd := exec.Command("mise", "unset", "-g", key)
cmd.Env = os.Environ()
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("mise unset -g failed: %v, output: %s", err, string(output))
}
return nil
}
+4 -1
View File
@@ -269,7 +269,10 @@ export const api = {
versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`), versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`),
verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`), 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 }) }), 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: { terminal: {
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds') cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
+170 -9
View File
@@ -10,8 +10,9 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { import {
Plus, Globe, Search, RefreshCw, Loader2, Trash2, Plus, Globe, Search, RefreshCw, Loader2, Trash2,
Terminal as TerminalIcon, X, AlertCircle, Terminal as TerminalIcon, X, AlertCircle,
Check, ChevronsUpDown Check, ChevronsUpDown, ArrowRight
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { api, type MiseLanguage } from '@/api' import { api, type MiseLanguage } from '@/api'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import XTerminal from '@/components/XTerminal.vue' import XTerminal from '@/components/XTerminal.vue'
@@ -56,12 +57,67 @@ const filteredPlugins = computed(() => {
return availablePlugins.value.filter(p => p.toLowerCase().includes(s)) return availablePlugins.value.filter(p => p.toLowerCase().includes(s))
}) })
const filteredVersions = computed(() => { const activeTab = ref('runtimes')
const list = availableVersions.value
if (!versionSearch.value) return list // 全局变量相关
const s = versionSearch.value.toLowerCase() const globalEnvs = ref<Record<string, string>>({})
return list.filter(v => v.toLowerCase().includes(s)) 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 showTerminalDialog = ref(false)
const terminalCommand = ref('') 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)) 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() { async function loadLanguages() {
loading.value = true loading.value = true
errorMsg.value = '' errorMsg.value = ''
@@ -260,7 +323,10 @@ async function toggleDefault(lang: MiseLanguage) {
} }
} }
onMounted(loadLanguages) onMounted(() => {
loadLanguages()
loadGlobalEnvs()
})
</script> </script>
<template> <template>
@@ -287,9 +353,17 @@ onMounted(loadLanguages)
<p class="text-sm font-medium">{{ errorMsg }}</p> <p class="text-sm font-medium">{{ errorMsg }}</p>
</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>
<TabsContent value="runtimes" class="space-y-4 outline-none">
<!-- 列表部分 --> <!-- 列表部分 -->
<div class="rounded-lg border bg-card overflow-hidden"> <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="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"> <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" /> <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" /> <Input v-model="searchQuery" placeholder="搜索语言或版本..." class="h-9 pl-8 text-sm" />
@@ -384,6 +458,67 @@ onMounted(loadLanguages)
</template> </template>
</div> </div>
</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"> <Dialog v-model:open="showInstallDialog">
@@ -504,6 +639,32 @@ onMounted(loadLanguages)
</DialogContent> </DialogContent>
</Dialog> </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"> <Dialog v-model:open="showTerminalDialog">
<DialogContent <DialogContent