feat(interconnect): sync environment variables with original structure and IDs, and translate UI badges

This commit is contained in:
duorameng
2026-06-26 09:23:20 +08:00
parent 898cb64085
commit bdeda51b22
4 changed files with 205 additions and 66 deletions
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { toast } from 'vue-sonner'
import * as interconnectApi from '@/api/interconnect'
import { api, type EnvVar, type Task } from '@/api'
import { Network, Search, HardDrive } from 'lucide-vue-next'
import { Network, Search, HardDrive, Check } from 'lucide-vue-next'
const props = defineProps<{
nodes: interconnectApi.InterconnectNode[]
@@ -62,12 +61,13 @@ const filteredEnvs = computed(() => {
return localEnvs.value.filter((e: EnvVar) => e.name.toLowerCase().includes(kw) || e.value.toLowerCase().includes(kw))
})
function toggleNode(nodeId: string) {
const index = selectedNodes.value.indexOf(nodeId)
function toggleNode(nodeId: any) {
const idStr = String(nodeId)
const index = selectedNodes.value.indexOf(idStr)
if (index > -1) {
selectedNodes.value.splice(index, 1)
} else {
selectedNodes.value.push(nodeId)
selectedNodes.value.push(idStr)
}
}
@@ -75,16 +75,17 @@ function toggleAllNodes() {
if (selectedNodes.value.length === props.nodes.length && props.nodes.length > 0) {
selectedNodes.value = []
} else {
selectedNodes.value = props.nodes.map(n => n.id)
selectedNodes.value = props.nodes.map(n => String(n.id))
}
}
function toggleTask(id: string) {
const index = selectedTasks.value.indexOf(id)
function toggleTask(id: any) {
const idStr = String(id)
const index = selectedTasks.value.indexOf(idStr)
if (index > -1) {
selectedTasks.value.splice(index, 1)
} else {
selectedTasks.value.push(id)
selectedTasks.value.push(idStr)
}
}
@@ -92,24 +93,25 @@ function toggleAllTasks() {
if (selectedTasks.value.length === filteredTasks.value.length && filteredTasks.value.length > 0) {
selectedTasks.value = []
} else {
selectedTasks.value = filteredTasks.value.map((t: Task) => t.id)
selectedTasks.value = filteredTasks.value.map((t: Task) => String(t.id))
}
}
function toggleEnv(id: string) {
const index = selectedEnvsIds.value.indexOf(id)
function toggleEnv(id: any) {
const idStr = String(id)
const index = selectedEnvsIds.value.indexOf(idStr)
if (index > -1) {
selectedEnvsIds.value.splice(index, 1)
} else {
selectedEnvsIds.value.push(id)
selectedEnvsIds.value.push(idStr)
}
}
function toggleAllEnvs() {
if (selectedEnvsIds.value.length === filteredEnvs.value.length && filteredEnvs.value.length > 0) {
if (selectedEnvsIds.value.length === filteredEnvs.value.filter((e: EnvVar) => e.type !== 'secret').length && filteredEnvs.value.length > 0) {
selectedEnvsIds.value = []
} else {
selectedEnvsIds.value = filteredEnvs.value.map((e: EnvVar) => e.id)
selectedEnvsIds.value = filteredEnvs.value.filter((e: EnvVar) => e.type !== 'secret').map((e: EnvVar) => String(e.id))
}
}
@@ -136,7 +138,19 @@ async function handleSync() {
toast.warning('请先选择要同步的变量')
return
}
const envsToSync = localEnvs.value.filter((e: EnvVar) => selectedEnvsIds.value.includes(e.id))
const allSelectedEnvs = localEnvs.value.filter((e: EnvVar) => selectedEnvsIds.value.includes(e.id))
const envsToSync = allSelectedEnvs.filter((e: EnvVar) => e.type !== 'secret')
if (allSelectedEnvs.length > envsToSync.length) {
toast.info('已自动过滤机密变量,因下发无意义')
}
if (envsToSync.length === 0) {
syncing.value = false
return
}
const res = await interconnectApi.syncEnv(targetNodeIds, envsToSync)
showSyncResult(res)
}
@@ -161,7 +175,7 @@ function showSyncResult(res: any[]) {
<template>
<div class="flex flex-col lg:flex-row gap-6 h-[calc(100vh-16rem)] min-h-[500px]">
<!-- 左侧节点选择 -->
<div class="w-full lg:w-1/3 flex flex-col border rounded-lg bg-card overflow-hidden">
<div class="w-full lg:w-1/3 flex flex-col border rounded-lg bg-card overflow-hidden h-[180px] lg:h-full shrink-0">
<div class="p-3 border-b bg-muted/20 flex items-center justify-between">
<h3 class="font-medium text-sm flex items-center gap-2">
<HardDrive class="w-4 h-4 text-muted-foreground" />
@@ -176,8 +190,13 @@ function showSyncResult(res: any[]) {
暂无子节点
</div>
<div v-else class="space-y-1">
<div v-for="node in nodes" :key="node.id" class="flex items-center gap-3 p-2 hover:bg-muted/50 rounded-md cursor-pointer" @click="toggleNode(node.id)">
<Checkbox :checked="selectedNodes.includes(node.id)" @update:checked="toggleNode(node.id)" @click.stop />
<div v-for="node in nodes" :key="node.id" class="flex items-center gap-3 p-2 hover:bg-muted/50 rounded-md cursor-pointer select-none group/node" @click="toggleNode(node.id)">
<div
class="size-4 rounded-[4px] border flex items-center justify-center shrink-0 transition-all duration-200 shadow-xs"
:class="selectedNodes.includes(String(node.id)) ? 'bg-primary border-primary text-primary-foreground scale-100' : 'bg-transparent border-input group-hover/node:border-muted-foreground/50 scale-95'"
>
<Check v-if="selectedNodes.includes(String(node.id))" class="size-3 stroke-[3.5] animate-in zoom-in-75 fade-in duration-150" />
</div>
<div class="flex flex-col flex-1 min-w-0">
<span class="text-sm font-medium truncate">{{ node.name }}</span>
<span class="text-xs text-muted-foreground truncate">{{ node.url }}</span>
@@ -186,9 +205,9 @@ function showSyncResult(res: any[]) {
</div>
</div>
</div>
<!-- 右侧数据选择 -->
<div class="w-full lg:w-2/3 flex flex-col border rounded-lg bg-card overflow-hidden">
<div class="w-full lg:w-2/3 flex flex-col border rounded-lg bg-card overflow-hidden flex-1 lg:h-full min-h-[250px] lg:min-h-0">
<div class="p-3 border-b bg-muted/20 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex bg-muted p-1 rounded-md">
<button
@@ -207,22 +226,22 @@ function showSyncResult(res: any[]) {
</button>
</div>
<div class="flex items-center gap-2">
<div class="relative w-48 hidden sm:block">
<div class="flex items-center gap-2 w-full justify-end sm:w-auto">
<div class="relative flex-grow sm:flex-grow-0 sm:w-48">
<Search class="w-3.5 h-3.5 absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
<Input v-model="taskSearch" v-if="activeSyncType === 'task'" placeholder="搜索任务..." class="h-8 pl-7 text-xs" />
<Input v-model="envSearch" v-if="activeSyncType === 'env'" placeholder="搜索变量..." class="h-8 pl-7 text-xs" />
</div>
<Button class="h-8 gap-1.5" size="sm" @click="handleSync" :disabled="syncing">
<Network class="w-3.5 h-3.5" :class="{'animate-pulse': syncing}" />
{{ syncing ? '正在下发...' : '执行下发' }}
{{ syncing ? '正在下发...' : '下发' }}
</Button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-2">
<!-- 任务列表 -->
<div v-if="activeSyncType === 'task'" class="space-y-1">
<div v-if="activeSyncType === 'task'" key="task-list" class="space-y-1">
<div class="flex items-center justify-between px-2 py-1 mb-2">
<span class="text-xs text-muted-foreground">已选 {{ selectedTasks.length }} </span>
<Button variant="ghost" size="sm" class="h-6 text-xs px-2" @click="toggleAllTasks">
@@ -231,12 +250,17 @@ function showSyncResult(res: any[]) {
</div>
<div v-if="loading" class="text-center py-8 text-sm text-muted-foreground">加载中...</div>
<div v-else-if="filteredTasks.length === 0" class="text-center py-8 text-sm text-muted-foreground">暂无任务</div>
<div v-for="task in filteredTasks" :key="task.id" class="flex items-start gap-3 p-2 hover:bg-muted/50 rounded-md cursor-pointer" @click="toggleTask(task.id)">
<Checkbox class="mt-1" :checked="selectedTasks.includes(task.id)" @update:checked="toggleTask(task.id)" @click.stop />
<div v-for="task in filteredTasks" :key="task.id" class="flex items-start gap-3 p-2 hover:bg-muted/50 rounded-md cursor-pointer select-none group/task" @click="toggleTask(task.id)">
<div
class="mt-1 size-4 rounded-[4px] border flex items-center justify-center shrink-0 transition-all duration-200 shadow-xs"
:class="selectedTasks.includes(String(task.id)) ? 'bg-primary border-primary text-primary-foreground scale-100' : 'bg-transparent border-input group-hover/task:border-muted-foreground/50 scale-95'"
>
<Check v-if="selectedTasks.includes(String(task.id))" class="size-3 stroke-[3.5] animate-in zoom-in-75 fade-in duration-150" />
</div>
<div class="flex flex-col flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-medium truncate">{{ task.name }}</span>
<span class="text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{{ task.type }}</span>
<span class="text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{{ task.type === 'task' ? '任务' : (task.type === 'repo' ? '仓库' : task.type) }}</span>
</div>
<span class="text-xs text-muted-foreground truncate font-mono mt-0.5">{{ task.command }}</span>
</div>
@@ -244,7 +268,7 @@ function showSyncResult(res: any[]) {
</div>
<!-- 变量列表 -->
<div v-if="activeSyncType === 'env'" class="space-y-1">
<div v-if="activeSyncType === 'env'" key="env-list" class="space-y-1">
<div class="flex items-center justify-between px-2 py-1 mb-2">
<span class="text-xs text-muted-foreground">已选 {{ selectedEnvsIds.length }} </span>
<Button variant="ghost" size="sm" class="h-6 text-xs px-2" @click="toggleAllEnvs">
@@ -253,11 +277,25 @@ function showSyncResult(res: any[]) {
</div>
<div v-if="loading" class="text-center py-8 text-sm text-muted-foreground">加载中...</div>
<div v-else-if="filteredEnvs.length === 0" class="text-center py-8 text-sm text-muted-foreground">暂无变量</div>
<div v-for="env in filteredEnvs" :key="env.id" class="flex items-start gap-3 p-2 hover:bg-muted/50 rounded-md cursor-pointer" @click="toggleEnv(env.id)">
<Checkbox class="mt-1" :checked="selectedEnvsIds.includes(env.id)" @update:checked="toggleEnv(env.id)" @click.stop />
<div v-for="env in filteredEnvs" :key="env.id"
class="flex items-start gap-3 p-2 rounded-md select-none group/env"
:class="env.type === 'secret' ? 'opacity-50 cursor-not-allowed bg-muted/20' : 'hover:bg-muted/50 cursor-pointer'"
@click="env.type === 'secret' ? null : toggleEnv(env.id)">
<div
class="mt-1 size-4 rounded-[4px] border flex items-center justify-center shrink-0 transition-all duration-200 shadow-xs"
:class="[
selectedEnvsIds.includes(String(env.id)) ? 'bg-primary border-primary text-primary-foreground scale-100' : 'bg-transparent border-input group-hover/env:border-muted-foreground/50 scale-95',
env.type === 'secret' ? 'opacity-50 bg-muted border-muted-foreground/30' : ''
]"
>
<Check v-if="selectedEnvsIds.includes(String(env.id))" class="size-3 stroke-[3.5] animate-in zoom-in-75 fade-in duration-150" />
</div>
<div class="flex flex-col flex-1 min-w-0">
<span class="text-sm font-medium truncate font-mono">{{ env.name }}</span>
<span class="text-xs text-muted-foreground truncate font-mono mt-0.5">{{ env.value }}</span>
<div class="flex items-center gap-2">
<span class="text-sm font-medium truncate">{{ env.name }}</span>
<span v-if="env.type" class="text-[10px] bg-muted px-1.5 py-0.5 rounded" :class="env.type === 'secret' ? 'text-amber-500 bg-amber-500/10' : 'text-muted-foreground'">{{ env.type === 'secret' ? '机密' : (env.type === 'normal' ? '常量' : env.type) }}</span>
</div>
<span class="text-xs text-muted-foreground truncate font-mono mt-0.5">{{ env.type === 'secret' ? '********' : env.value }}</span>
</div>
</div>
</div>