feat: add mirror settings #29
This commit is contained in:
@@ -117,3 +117,48 @@ func (c *MiseController) UnsetGlobal(ctx *gin.Context) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -212,6 +212,9 @@ func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) {
|
||||
mise.GET("/verify-cmd", c.Mise.VerifyCommand)
|
||||
mise.POST("/use-global", c.Mise.UseGlobal)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -359,3 +359,50 @@ func (s *MiseService) UnsetGlobal(plugin, version string) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,9 +353,17 @@ onMounted(loadLanguages)
|
||||
<p class="text-sm font-medium">{{ errorMsg }}</p>
|
||||
</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="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">
|
||||
<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" />
|
||||
@@ -384,6 +458,67 @@ onMounted(loadLanguages)
|
||||
</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