feat(interconnect): sync environment variables with original structure and IDs, and translate UI badges
This commit is contained in:
@@ -2,6 +2,8 @@ package controllers
|
||||
|
||||
import (
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/models/vo"
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
"github.com/engigu/baihu-panel/internal/services/relation"
|
||||
@@ -300,3 +302,88 @@ func (ec *EnvController) GetTags(c *gin.Context) {
|
||||
}
|
||||
utils.Success(c, tags)
|
||||
}
|
||||
|
||||
// BulkSaveEnv 批量保存环境变量
|
||||
func (ec *EnvController) BulkSaveEnv(c *gin.Context) {
|
||||
var reqs []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Value string `json:"value" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
Type string `json:"type"`
|
||||
Hidden *bool `json:"hidden"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&reqs); err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
for _, req := range reqs {
|
||||
if req.Type == "secret" {
|
||||
continue // 二次严苛拦截,机密变量不应下发/保存
|
||||
}
|
||||
|
||||
hidden := true
|
||||
if req.Hidden != nil {
|
||||
hidden = *req.Hidden
|
||||
}
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
|
||||
var existingEnv *models.EnvironmentVariable
|
||||
// 优先按 ID 匹配
|
||||
if req.ID != "" {
|
||||
var e models.EnvironmentVariable
|
||||
if err := database.DB.Where("id = ?", req.ID).First(&e).Error; err == nil {
|
||||
existingEnv = &e
|
||||
}
|
||||
}
|
||||
// 如果 ID 没找到,按 Name 匹配
|
||||
if existingEnv == nil {
|
||||
var e models.EnvironmentVariable
|
||||
if err := database.DB.Where("name = ?", req.Name).First(&e).Error; err == nil {
|
||||
existingEnv = &e
|
||||
}
|
||||
}
|
||||
|
||||
if existingEnv != nil {
|
||||
existingEnv.Name = req.Name
|
||||
existingEnv.Value = models.BigText(req.Value)
|
||||
existingEnv.Remark = req.Remark
|
||||
existingEnv.Type = req.Type
|
||||
existingEnv.Hidden = &hidden
|
||||
existingEnv.Enabled = &enabled
|
||||
database.DB.Save(existingEnv)
|
||||
|
||||
if req.ID != "" && existingEnv.ID != req.ID {
|
||||
database.DB.Model(existingEnv).Update("id", req.ID)
|
||||
}
|
||||
} else {
|
||||
envVar := &models.EnvironmentVariable{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Value: models.BigText(req.Value),
|
||||
Remark: req.Remark,
|
||||
Type: req.Type,
|
||||
Hidden: &hidden,
|
||||
Enabled: &enabled,
|
||||
UserID: userID,
|
||||
CreatedAt: models.Now(),
|
||||
UpdatedAt: models.Now(),
|
||||
}
|
||||
if envVar.ID == "" {
|
||||
envVar.ID = utils.GenerateID()
|
||||
}
|
||||
database.DB.Create(envVar)
|
||||
}
|
||||
}
|
||||
|
||||
services.GetAgentWSManager().BroadcastTasksToAll()
|
||||
utils.Success(c, nil)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/models/vo"
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
@@ -271,9 +272,13 @@ func (ic *InterconnectController) SyncEnv(c *gin.Context) {
|
||||
var req struct {
|
||||
NodeIDs []string `json:"node_ids" binding:"required"`
|
||||
Envs []struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Remark string `json:"remark"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Remark string `json:"remark"`
|
||||
Type string `json:"type"`
|
||||
Hidden *bool `json:"hidden"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
} `json:"envs" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -282,6 +287,27 @@ func (ic *InterconnectController) SyncEnv(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out secret variables securely using the database
|
||||
var safeEnvs []interface{}
|
||||
for _, envReq := range req.Envs {
|
||||
var env models.EnvironmentVariable
|
||||
query := database.DB
|
||||
if envReq.ID != "" {
|
||||
query = query.Where("id = ? OR name = ?", envReq.ID, envReq.Name)
|
||||
} else {
|
||||
query = query.Where("name = ?", envReq.Name)
|
||||
}
|
||||
if err := query.First(&env).Error; err == nil {
|
||||
if env.Type == "secret" {
|
||||
continue // 坚决阻断下发机密数据
|
||||
}
|
||||
}
|
||||
if envReq.Type == "secret" {
|
||||
continue // 坚决阻断下发机密数据
|
||||
}
|
||||
safeEnvs = append(safeEnvs, envReq)
|
||||
}
|
||||
|
||||
results := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, nodeID := range req.NodeIDs {
|
||||
@@ -291,44 +317,31 @@ func (ic *InterconnectController) SyncEnv(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
client, apiURL, err := ic.getClientAndURL(node, "/api/v1/env")
|
||||
client, apiURL, err := ic.getClientAndURL(node, "/api/v1/env/bulk_save")
|
||||
if err != nil {
|
||||
results = append(results, map[string]interface{}{"node_id": nodeID, "success": false, "msg": "反向隧道未连接"})
|
||||
continue
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, env := range req.Envs {
|
||||
payload := map[string]interface{}{
|
||||
"name": env.Name,
|
||||
"value": env.Value,
|
||||
"remark": env.Remark,
|
||||
"type": "normal",
|
||||
}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
payloadBytes, _ := json.Marshal(safeEnvs)
|
||||
|
||||
httpReq, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+node.Token)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
successCount++
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
httpReq, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
results = append(results, map[string]interface{}{"node_id": nodeID, "success": false, "msg": "构建请求失败"})
|
||||
continue
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+node.Token)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
results = append(results, map[string]interface{}{
|
||||
"node_id": nodeID,
|
||||
"success": true,
|
||||
"msg": "同步完成",
|
||||
"count": successCount,
|
||||
})
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
results = append(results, map[string]interface{}{"node_id": nodeID, "success": false, "msg": "同步请求失败或超时"})
|
||||
} else {
|
||||
results = append(results, map[string]interface{}{"node_id": nodeID, "success": true, "msg": "同步成功"})
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
utils.Success(c, results)
|
||||
|
||||
@@ -116,6 +116,7 @@ func registerEnvRoutes(g *gin.RouterGroup, c *Controllers) {
|
||||
env.GET("/secret-status", c.Env.GetSecretStatus)
|
||||
env.GET("/tags", c.Env.GetTags)
|
||||
env.POST("", c.Env.CreateEnvVar)
|
||||
env.POST("/bulk_save", c.Env.BulkSaveEnv)
|
||||
env.GET("", c.Env.GetEnvVars)
|
||||
env.GET("/all", c.Env.GetAllEnvVars)
|
||||
env.GET("/:id", c.Env.GetEnvVar)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user