chore: update page style mobile #20

This commit is contained in:
engigu
2026-04-08 21:47:30 +08:00
parent ba522c4f36
commit d897195832
11 changed files with 535 additions and 442 deletions
+10 -13
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
const props = withDefaults(
defineProps<{
@@ -22,19 +22,16 @@ function handleClick() {
</script>
<template>
<span v-bind="$attrs" class="truncate block cursor-pointer hover:text-primary" :title="text || '-'"
@click="handleClick">
<span v-bind="$attrs" class="truncate block cursor-pointer hover:text-primary transition-colors" :title="text || '-'"
@click.stop="handleClick">
{{ text || '-' }}
</span>
<Dialog v-model:open="showDialog">
<DialogContent class="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{{ title }}</DialogTitle>
</DialogHeader>
<div class="max-h-[400px] overflow-y-auto">
<pre class="text-sm whitespace-pre-wrap break-all font-mono bg-muted p-3 rounded-lg">{{ text }}</pre>
</div>
</DialogContent>
</Dialog>
<BaihuDialog v-model:open="showDialog" :title="title">
<div class="max-h-[60vh] overflow-y-auto custom-scrollbar">
<p class="text-[15px] leading-relaxed text-foreground/80 break-all whitespace-pre-wrap font-sans">
{{ text }}
</p>
</div>
</BaihuDialog>
</template>
+130
View File
@@ -0,0 +1,130 @@
<script setup lang="ts">
import { X } from 'lucide-vue-next'
import * as Icons from 'lucide-vue-next'
import { computed } from 'vue'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose
} from '@/components/ui/dialog'
interface Props {
open: boolean
title?: string
description?: string
icon?: string
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
showClose?: boolean
}
const props = withDefaults(defineProps<Props>(), {
title: '',
description: '',
size: 'md',
showClose: true,
icon: ''
})
const iconComponent = computed(() => {
if (!props.icon) return null
return (Icons as any)[props.icon]
})
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
const sizeClasses = {
sm: 'sm:max-w-[425px]',
md: 'sm:max-w-[600px]',
lg: 'sm:max-w-[800px]',
xl: 'sm:max-w-[1000px]',
full: 'sm:max-w-[95vw] h-[90vh]'
}
</script>
<template>
<Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent
:show-close-button="false"
:class="[
'overflow-hidden border-none p-0 bg-background/90 backdrop-blur-xl shadow-2xl ring-1 ring-black/5 dark:ring-white/10 rounded-xl',
sizeClasses[size],
'transition-all duration-300'
]"
>
<!-- 统一的 Header 风格 (参考日志详情) -->
<div class="flex items-center justify-between px-4 h-11 border-b bg-muted/20 shrink-0">
<DialogHeader class="space-y-1 text-left flex-1 min-w-0">
<div class="flex items-center gap-2">
<component :is="iconComponent" v-if="iconComponent" class="h-4 w-4 text-primary shrink-0" />
<DialogTitle class="text-[13px] font-normal text-muted-foreground tracking-wide truncate">
{{ title }}
</DialogTitle>
</div>
<DialogDescription v-if="description" class="text-xs text-muted-foreground/60 leading-relaxed truncate">
{{ description }}
</DialogDescription>
<DialogDescription v-else class="sr-only">
{{ title || '提示对话框' }}
</DialogDescription>
</DialogHeader>
<DialogClose
v-if="showClose"
class="h-7 w-7 rounded-md opacity-70 transition-all hover:opacity-100 hover:bg-muted flex items-center justify-center -mr-1"
>
<X class="h-4 w-4 text-muted-foreground" />
<span class="sr-only">关闭</span>
</DialogClose>
</div>
<!-- 内容区域 (保持整洁简洁) -->
<div class="p-6">
<div class="animate-in fade-in slide-in-from-bottom-1 duration-400">
<slot />
</div>
<!-- 底部插槽 -->
<div v-if="$slots.footer" class="mt-8 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2">
<slot name="footer" />
</div>
</div>
</DialogContent>
</Dialog>
</template>
<style scoped>
:deep([data-state='open']) {
animation: baihu-dialog-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
:deep([data-state='closed']) {
animation: baihu-dialog-out 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes baihu-dialog-in {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes baihu-dialog-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
}
</style>
+29 -27
View File
@@ -258,26 +258,26 @@ onMounted(async () => {
<div class="mt-4">
<div class="rounded-lg border bg-card overflow-x-auto">
<!-- 工具栏 -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 px-4 py-3 border-b bg-muted/10">
<div class="flex items-center gap-2">
<Badge variant="secondary">{{ filteredDeps.length }} 个包</Badge>
<div class="flex items-center justify-between gap-3 px-3 sm:px-4 py-2 sm:py-3 border-b bg-muted/10">
<div class="flex items-center gap-2 shrink-0">
<Badge variant="secondary" class="h-6 sm:h-7 px-2 text-[11px] sm:text-xs bg-primary/10 text-primary border-primary/20">{{ filteredDeps.length }} 个包</Badge>
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="relative flex-1 sm:flex-none">
<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 w-full sm:w-40 md:w-48 text-sm" />
<div class="flex items-center gap-2 flex-1 justify-end">
<div class="relative flex-1 max-w-[200px]">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<Input v-model="searchQuery" placeholder="搜索包名..." class="h-8 sm:h-9 pl-8 w-full text-[11px] sm:text-sm bg-background/50 focus:bg-background transition-all" />
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadDeps" :disabled="loading">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
<div class="flex items-center gap-1.5 sm:gap-2 shrink-0">
<Button variant="outline" size="icon" class="h-8 w-8 sm:h-9 sm:w-9 shrink-0 shadow-sm" @click="loadDeps" :disabled="loading">
<RefreshCw class="h-3.5 w-3.5 sm:h-4 sm:w-4" :class="{ 'animate-spin': loading }" />
</Button>
<Button variant="outline" size="sm" class="h-9 shrink-0" @click="reinstallAll"
<Button variant="outline" size="sm" class="h-8 sm:h-9 px-2 sm:px-3 text-[11px] sm:text-xs shrink-0 shadow-sm" @click="reinstallAll"
:disabled="reinstallingAll || filteredDeps.length === 0">
<RotateCw class="h-4 w-4 sm:mr-1.5" :class="{ 'animate-spin': reinstallingAll }" /> <span
<RotateCw class="h-3.5 w-3.5 sm:h-4 sm:w-4 sm:mr-1.5" :class="{ 'animate-spin': reinstallingAll }" /> <span
class="hidden sm:inline">全部重装</span>
</Button>
<Button size="sm" class="h-9 shrink-0" @click="openInstallDialog">
<Download class="h-4 w-4 sm:mr-1.5" /> <span class="hidden sm:inline">安装</span>
<Button size="sm" class="h-8 sm:h-9 px-2 sm:px-3 text-[11px] sm:text-xs shrink-0 shadow-sm" @click="openInstallDialog">
<Download class="h-3.5 w-3.5 sm:h-4 sm:w-4 sm:mr-1.5" /> <span class="hidden sm:inline">安装</span>
</Button>
</div>
</div>
@@ -285,11 +285,12 @@ onMounted(async () => {
<!-- 表头 -->
<div
class="flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium min-w-[400px]">
class="flex items-center gap-4 px-4 h-10 border-b bg-muted/20 text-xs text-muted-foreground font-bold uppercase tracking-wider min-w-[400px]">
<span class="w-12 shrink-0 pl-1">序号</span>
<span class="flex-1">包名</span>
<span class="w-32">版本</span>
<span class="w-48 hidden md:block">备注</span>
<span class="w-32 text-center">操作</span>
<span class="w-24 shrink-0 px-2">版本</span>
<span class="w-48 hidden md:block shrink-0 px-2 font-medium">备注说明</span>
<span class="w-32 text-center shrink-0">操作</span>
</div>
<!-- 列表 -->
@@ -302,29 +303,30 @@ onMounted(async () => {
<Package class="h-8 w-8 mx-auto mb-2 opacity-50" />
{{ searchQuery ? '无匹配结果' : '暂无依赖包' }}
</div>
<div v-else v-for="dep in filteredDeps" :key="dep.id"
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/30 transition-colors">
<span class="flex-1 font-mono text-sm truncate">
<div v-else v-for="(dep, index) in filteredDeps" :key="dep.id"
class="flex items-center gap-4 px-4 h-10 hover:bg-muted/30 transition-colors group">
<div class="w-12 shrink-0 text-[11px] text-muted-foreground tabular-nums">#{{ filteredDeps.length - index }}</div>
<span class="flex-1 font-mono text-[13px] truncate font-medium">
<TextOverflow :text="dep.name" title="包名" />
</span>
<span class="w-32 text-sm text-muted-foreground">{{ dep.version || '-' }}</span>
<span class="w-48 text-sm text-muted-foreground truncate hidden md:block">
<span class="w-24 shrink-0 px-2 text-[12px] text-muted-foreground font-mono">{{ dep.version || '-' }}</span>
<span class="w-48 text-[12px] text-muted-foreground truncate hidden md:block shrink-0 px-2">
<TextOverflow :text="dep.remark || '-'" title="备注" />
</span>
<span class="w-32 flex justify-center gap-1">
<span class="w-32 flex justify-center gap-1 shrink-0 opacity-80 group-hover:opacity-100">
<Button v-if="dep.log || dep.id" variant="ghost" size="icon"
class="h-7 w-7 text-blue-500 hover:text-blue-600 hover:bg-blue-50/10" @click="showLog(dep)"
title="查看安装日志">
<FileText class="h-4 w-4" />
<FileText class="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon"
class="h-7 w-7 text-amber-500 hover:text-amber-600 hover:bg-amber-50/10" @click="reinstallPackage(dep)"
:disabled="reinstalling === dep.id" title="重新安装">
<RotateCw class="h-4 w-4" :class="{ 'animate-spin': reinstalling === dep.id }" />
<RotateCw class="h-3.5 w-3.5" :class="{ 'animate-spin': reinstalling === dep.id }" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive hover:bg-destructive/10"
@click="confirmDelete(dep)" title="卸载并删除记录">
<Trash2 class="h-4 w-4" />
<Trash2 class="h-3.5 w-3.5" />
</Button>
</span>
</div>
+42 -55
View File
@@ -2,7 +2,7 @@
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
@@ -457,62 +457,49 @@ onBeforeUnmount(() => {
</DialogContent>
</Dialog>
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
<div v-if="associatedTasks.length > 0" class="space-y-4 pt-1">
<div class="flex items-start gap-3 p-3 rounded-lg bg-destructive/10 border border-destructive/20">
<AlertTriangle class="h-5 w-5 text-destructive shrink-0 mt-0.5" />
<div class="space-y-1">
<p class="text-sm font-bold text-destructive">{{ activeTab === ENV_TYPE.SECRET ? '机密' : '环境变量' }}正在使用中</p>
<p class="text-xs text-muted-foreground leading-relaxed">
{{ activeTab === ENV_TYPE.SECRET ? '机密' : '变量' }}已被以下任务引用直接删除可能导致任务运行失败建议先移除引用或选择强制删除
</p>
</div>
</div>
<BaihuDialog v-model:open="showDeleteDialog" :title="associatedTasks.length > 0 ? '风险删除确认' : '确认删除'">
<div v-if="associatedTasks.length > 0" class="space-y-4">
<div class="flex items-start gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<AlertTriangle class="h-5 w-5 text-destructive shrink-0" />
<div class="space-y-1">
<p class="text-sm font-bold text-destructive">{{ activeTab === ENV_TYPE.SECRET ? '机密' : '环境变量' }}正在使用中</p>
<p class="text-[13px] text-muted-foreground/80 leading-relaxed">
{{ activeTab === ENV_TYPE.SECRET ? '机密' : '变量' }}已被以下任务引用直接删除可能导致任务运行失败建议先移除引用或选择强制删除
</p>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between px-1">
<p class="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">关联任务 ({{
associatedTasks.length }})</p>
</div>
<div class="bg-muted/30 rounded-lg p-1.5 max-h-40 overflow-y-auto space-y-1 border border-border/40">
<div v-for="task in associatedTasks" :key="task.id"
class="text-xs flex items-center justify-between bg-background/50 p-2 rounded-md border border-border/50 hover:bg-background transition-colors">
<div class="flex items-center gap-2 min-w-0">
<Terminal class="h-3 w-3 text-primary/70" />
<span class="font-medium truncate">{{ task.name }}</span>
</div>
<code
class="text-[10px] text-muted-foreground/70 font-mono bg-muted/50 px-1.5 py-0.5 rounded">{{ task.id }}</code>
</div>
</div>
</div>
<div class="p-3 rounded-lg bg-secondary/30 border border-border/20">
<p class="text-xs text-muted-foreground leading-relaxed">
<span class="font-bold text-foreground/80">提示</span>选择强制删除将自动解除以上任务对该{{ activeTab === 'secret' ? '机密' : '变量' }}的绑定并执行物理删除
</p>
<div class="space-y-2">
<p class="text-[11px] font-bold text-muted-foreground uppercase tracking-widest px-1">关联任务 ({{ associatedTasks.length }})</p>
<div class="bg-muted/30 rounded-xl p-2 max-h-48 overflow-y-auto space-y-1.5 border border-border/40">
<div v-for="task in associatedTasks" :key="task.id"
class="text-xs flex items-center justify-between bg-card p-2.5 rounded-lg border border-border/50 hover:border-primary/30 transition-all">
<div class="flex items-center gap-2.5 min-w-0">
<Terminal class="h-3.5 w-3.5 text-primary/70" />
<span class="font-medium truncate">{{ task.name }}</span>
</div>
<code class="text-[10px] text-muted-foreground/70 font-mono bg-muted/50 px-1.5 py-0.5 rounded">{{ task.id }}</code>
</div>
<p v-else class="py-2">确定要删除此{{ activeTab === ENV_TYPE.SECRET ? '机密' : '环境变量' }}此操作无法撤销请谨慎操作</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel :disabled="isDeleting">取消</AlertDialogCancel>
<Button v-if="associatedTasks.length > 0" variant="destructive" @click="deleteEnv(true)"
:disabled="isDeleting">
<template v-if="isDeleting">删除中...</template>
<template v-else>强制删除</template>
</Button>
<Button v-else variant="destructive" @click="deleteEnv(false)" :disabled="isDeleting">
<template v-if="isDeleting">删除中...</template>
<template v-else>确认删除</template>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div class="p-4 rounded-xl bg-muted/20 border border-border/10">
<p class="text-xs text-muted-foreground leading-relaxed italic">
提示选择强制删除将自动解除以上任务对该{{ activeTab === 'secret' ? '机密' : '变量' }}的绑定并执行物理删除
</p>
</div>
</div>
<p v-else class="text-[15px] leading-relaxed text-muted-foreground">确定要删除此{{ activeTab === ENV_TYPE.SECRET ? '机密' : '环境变量' }}此操作无法撤销请谨慎操作</p>
<template #footer>
<Button variant="ghost" :disabled="isDeleting" @click="showDeleteDialog = false">取消</Button>
<Button v-if="associatedTasks.length > 0" variant="destructive" class="shadow-lg shadow-destructive/20" @click="deleteEnv(true)" :disabled="isDeleting">
{{ isDeleting ? '删除中...' : '确认强制删除' }}
</Button>
<Button v-else variant="destructive" class="shadow-lg shadow-destructive/20" @click="deleteEnv(false)" :disabled="isDeleting">
{{ isDeleting ? '删除中...' : '确认删除' }}
</Button>
</template>
</BaihuDialog>
</div>
</template>
+20 -44
View File
@@ -13,16 +13,7 @@ import {
} from 'lucide-vue-next'
import { api, type TaskLog } from '@/api'
import LogDetailCard from '@/components/LogDetailCard.vue'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
import TextOverflow from '@/components/TextOverflow.vue'
@@ -506,42 +497,27 @@ watch(() => route.query, (newQuery) => {
:content="decompressedOutput" />
<!-- 清空日志确认弹窗 -->
<AlertDialog :open="showClearDialog" @update:open="showClearDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认清空日志?</AlertDialogTitle>
<AlertDialogDescription>
此操作将永久删除{{ filterTaskId ? '当前任务的' : '所有' }}任务历史记录包括控制台输出并且无法撤销
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="handleClearLogs"
class="bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:text-white dark:hover:bg-red-700">
清空
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showClearDialog" title="确认清空日志?">
<div class="text-sm text-muted-foreground leading-relaxed">
此操作将永久删除{{ filterTaskId ? '当前任务的' : '所有' }}任务历史记录包括控制台输出并且无法撤销
<p class="mt-2 text-destructive font-medium"> 风险该操作不可撤销</p>
</div>
<template #footer>
<Button variant="ghost" @click="showClearDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="handleClearLogs">立即清空</Button>
</template>
</BaihuDialog>
<!-- 单条删除确认弹窗 -->
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除这条日志?</AlertDialogTitle>
<AlertDialogDescription>
此操作将永久删除该次运行记录和日志文件且不可恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="handleDeleteLog"
class="bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:text-white dark:hover:bg-red-700">
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showDeleteDialog" title="确认删除这条日志?">
<div class="text-sm text-muted-foreground leading-relaxed">
此操作将永久删除该次运行记录和日志文件且不可恢复
</div>
<template #footer>
<Button variant="ghost" @click="showDeleteDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="handleDeleteLog">确认删除</Button>
</template>
</BaihuDialog>
</div>
</template>
+162 -93
View File
@@ -17,6 +17,7 @@ import { api, type MiseLanguage } from '@/api'
import { toast } from 'vue-sonner'
import XTerminal from '@/components/XTerminal.vue'
import { cn } from '@/lib/utils'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
const SUPPORTED_DEPS_LANGS = [
'python', 'node', 'ruby', 'go', 'rust', 'bun', 'php',
@@ -35,6 +36,13 @@ const searchQuery = ref('')
const errorMsg = ref('')
const syncing = ref(false)
const showSyncConfirm = ref(false)
const showDetailDialog = ref(false)
const selectedLang = ref<DisplayLanguage | null>(null)
function viewDetail(lang: DisplayLanguage) {
selectedLang.value = lang
showDetailDialog.value = true
}
const showInstallDialog = ref(false)
const newLangPlugin = ref('')
@@ -338,22 +346,22 @@ onMounted(() => {
<p class="text-muted-foreground text-sm mt-0.5">管理编程语言及相关包依赖(Mise)</p>
</div>
<div class="flex flex-col sm:flex-row items-center gap-3 shrink-0 w-full md:w-auto">
<Button @click="openInstallDialog" class="w-full sm:w-auto h-9">
<Plus class="h-4 w-4 mr-2" /> 新增语言
<div class="flex flex-row items-center gap-2 shrink-0 w-full md:w-auto overflow-x-auto pb-1 sm:pb-0">
<Button @click="openInstallDialog" class="flex-1 sm:flex-none h-9 text-sm px-2 sm:px-4">
<Plus class="h-3.5 w-3.5 sm:mr-2" /> <span class="whitespace-nowrap">新增语言</span>
</Button>
<!-- 这里使用 Tabs 内部的 TabsList所以外部需要包裹 Tabs -->
<Tabs v-model="activeTab" class="w-full sm:w-auto">
<TabsList class="grid w-full sm:w-64 grid-cols-2 h-9">
<TabsTrigger value="runtimes">安装列表</TabsTrigger>
<TabsTrigger value="envs">镜像加速</TabsTrigger>
<Tabs v-model="activeTab" class="flex-1 sm:w-auto">
<TabsList class="grid w-full sm:w-60 grid-cols-2 h-9">
<TabsTrigger value="runtimes" class="text-sm px-1 sm:px-3">安装列表</TabsTrigger>
<TabsTrigger value="envs" class="text-sm px-1 sm:px-3">镜像加速</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
<!-- 贴士栏全宽 Banner 增加视觉重心 -->
<div class="flex items-center gap-2.5 text-[13px] text-amber-600 dark:text-amber-500 bg-amber-500/10 px-4 py-2.5 rounded-lg border border-amber-500/20 leading-relaxed shadow-sm select-none">
<div
class="flex items-center gap-2.5 text-[13px] text-amber-600 dark:text-amber-500 bg-amber-500/10 px-4 py-2.5 rounded-lg border border-amber-500/20 leading-relaxed shadow-sm select-none">
<AlertCircle class="h-4 w-4 shrink-0" />
<span>
<b class="font-bold">设为默认</b>将选定版本设为系统全局默认 (mise use -g)生效后所有未通过高级配置指定特定环境的任务将默认调用此环境
@@ -371,21 +379,23 @@ onMounted(() => {
<!-- 列表部分 -->
<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" />
class="flex items-center justify-between px-3 sm:px-4 py-2 sm:py-3 border-b bg-muted/30 gap-2 sm:gap-4">
<div class="relative flex-1 max-w-sm">
<Search
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input v-model="searchQuery" placeholder="搜索语言或版本..."
class="h-8 sm:h-9 pl-8 text-xs sm:text-sm bg-background/50 focus:bg-background transition-all" />
</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"
<div class="flex items-center gap-1.5 sm:gap-2 shrink-0">
<Button variant="outline" class="h-8 sm:h-9 px-2 sm:px-3 text-xs sm:text-sm shadow-sm"
@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>
<RefreshCw class="h-3.5 w-3.5 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 variant="outline" size="icon" class="h-8 w-8 sm:h-9 sm:w-9 shrink-0 shadow-sm"
@click="loadLanguages" :disabled="loading">
<RefreshCw class="h-3.5 w-3.5 sm:h-4 sm:w-4" :class="{ 'animate-spin': loading }" />
</Button>
</div>
</div>
@@ -401,64 +411,75 @@ onMounted(() => {
{{ 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 relative">
<div class="flex items-center gap-4 min-w-0 pr-10 sm:pr-0">
<div v-for="(lang, index) in filteredLanguages" :key="lang.plugin + lang.version"
class="flex flex-row items-center px-2 sm:px-4 py-2.5 sm:py-3 hover:bg-muted/5 transition-colors gap-2 sm:gap-4 relative group border-l-2 border-transparent hover:border-primary/40">
<div class="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
<div class="flex items-center justify-center w-6 sm:w-8 shrink-0">
<span
class="text-xs font-mono text-muted-foreground tabular-nums">#{{
filteredLanguages.length - index }}</span>
</div>
<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">
class="h-8 w-8 sm:h-9 sm:w-9 rounded-lg bg-primary/10 flex items-center justify-center font-bold text-primary uppercase overflow-hidden shrink-0 border border-primary/10 shadow-sm ml-1 sm:ml-0">
<template v-if="getLangIcon(lang.plugin)">
<div class="w-full h-full bg-white/80 p-2 flex items-center justify-center">
<div
class="w-full h-full bg-white/90 p-1.5 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 }}
<span class="text-xs">{{ lang.plugin.length > 2 ?
lang.plugin.substring(0, 2) : lang.plugin }}</span>
</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>
<div
class="flex flex-col lg:flex-row lg:items-center gap-0.5 lg:gap-4 flex-1 min-w-0">
<div class="flex items-center gap-2 min-w-[120px] shrink-0">
<span class="font-bold capitalize truncate text-[13px] sm:text-sm">{{
lang.plugin }}</span>
<Badge variant="outline"
class="font-mono text-[10px] h-4 px-1 bg-background/50">{{ 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">
class="bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20 text-[10px] h-4 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 class="hidden lg:block w-px h-3 bg-border/50 shrink-0" />
<div class="text-xs text-muted-foreground truncate opacity-60 hover:opacity-100 hover:text-primary transition-all cursor-help"
@click="viewDetail(lang)">
<span class="hidden md:inline mr-1 opacity-60">来源:</span>
<span class="font-mono truncate" :title="lang.source">{{ lang.source
}}</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 sm:flex sm:items-center gap-2 sm:ml-auto w-full sm:w-auto">
<Button v-if="SUPPORTED_DEPS_LANGS.includes(lang.plugin)" variant="outline" size="sm"
class="whitespace-nowrap w-full sm:w-auto"
@click="$router.push(`/dependencies?language=${lang.plugin}&version=${lang.version}`)">
依赖管理
</Button>
<Badge v-else variant="secondary"
class="h-9 opacity-60 flex justify-center whitespace-nowrap sm:w-auto">
不支持管理
</Badge>
<Button variant="outline" size="sm" class="whitespace-nowrap w-full sm:w-auto"
@click="handleVerify(lang)">
环境验证
</Button>
<Button variant="outline" size="sm"
:class="cn('whitespace-nowrap w-full sm:w-auto col-span-2 sm:col-span-1', lang.isGlobal && 'text-amber-600 border-amber-500/50 bg-amber-500/5 hover:bg-amber-500/10')"
@click="toggleDefault(lang)">
{{ lang.isGlobal ? '取消默认设置' : '设为默认版本' }}
</Button>
<div class="flex items-center gap-1 sm:gap-1.5 shrink-0 ml-auto">
<div
class="flex items-center gap-1 bg-muted/20 p-0.5 rounded-lg border border-border/40">
<Button v-if="SUPPORTED_DEPS_LANGS.includes(lang.plugin)" variant="ghost"
size="sm"
class="h-7 px-1.5 sm:px-2 text-xs font-semibold whitespace-nowrap hover:bg-background hover:shadow-sm"
@click="$router.push(`/dependencies?language=${lang.plugin}&version=${lang.version}`)">
依赖
</Button>
<Button variant="ghost" size="sm"
class="h-7 px-1.5 sm:px-2 text-xs font-semibold whitespace-nowrap hover:bg-background hover:shadow-sm"
@click="handleVerify(lang)">
验证
</Button>
<Button variant="ghost" size="sm"
:class="cn('h-7 px-1.5 sm:px-2 text-xs font-semibold whitespace-nowrap hover:bg-background hover:shadow-sm', lang.isGlobal ? 'text-amber-600 bg-amber-500/10' : '')"
@click="toggleDefault(lang)">
默认
</Button>
</div>
<Button variant="ghost" size="icon"
class="text-destructive h-9 w-9 shrink-0 absolute top-4 right-4 sm:relative sm:top-0 sm:right-0" @click="confirmDelete(lang)"
title="卸载">
<Trash2 class="h-4 w-4" />
class="h-7.5 w-7.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all opacity-40 group-hover:opacity-100"
@click="confirmDelete(lang)" title="卸载">
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</div>
@@ -483,25 +504,25 @@ onMounted(() => {
<!-- 常用预设网格 -->
<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 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">
<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">
<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>
@@ -510,12 +531,13 @@ onMounted(() => {
<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 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"
<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" />
@@ -583,7 +605,7 @@ onMounted(() => {
class="absolute -right-2 -top-1 h-3 w-3 text-primary bg-background rounded-full border shadow-sm" />
</div>
<span :class="{ 'font-bold text-primary': newLangPlugin === p }">{{ p
}}</span>
}}</span>
</button>
</template>
</div>
@@ -617,22 +639,24 @@ onMounted(() => {
</div>
<ScrollArea class="h-64">
<div class="p-1">
<div v-if="loadingVersions" class="flex items-center justify-center py-6">
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
<div class="p-1">
<div v-if="loadingVersions" class="flex items-center justify-center py-6">
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
</div>
<div v-else-if="filteredVersions.length === 0"
class="py-6 text-center text-xs text-muted-foreground">
未找到匹配版本
</div>
<template v-else>
<button v-for="v in filteredVersions" :key="v"
@click="() => { newLangVersion = v; openVersionPopover = false }"
class="w-full flex items-center px-2 py-1.5 text-sm rounded-sm hover:bg-muted text-left transition-colors">
<Check
:class="cn('mr-2 h-3.5 w-3.5', newLangVersion === v ? 'opacity-100' : 'opacity-0')" />
{{ v }}
</button>
</template>
</div>
<div v-else-if="filteredVersions.length === 0"
class="py-6 text-center text-xs text-muted-foreground">
未找到匹配版本
</div>
<template v-else>
<button v-for="v in filteredVersions" :key="v"
@click="() => { newLangVersion = v; openVersionPopover = false }"
class="w-full flex items-center px-2 py-1.5 text-sm rounded-sm hover:bg-muted text-left transition-colors">
<Check
:class="cn('mr-2 h-3.5 w-3.5', newLangVersion === v ? 'opacity-100' : 'opacity-0')" />
{{ v }}
</button>
</template>
</div>
</ScrollArea>
</PopoverContent>
@@ -720,5 +744,50 @@ onMounted(() => {
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 语言详情弹窗 -->
<BaihuDialog v-model:open="showDetailDialog" title="运行环境详情" icon="Info">
<template v-if="selectedLang">
<div class="space-y-4">
<div class="flex items-center gap-4 p-3 rounded-lg bg-muted/30 border border-border/40">
<div
class="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center font-bold text-primary uppercase overflow-hidden shrink-0 border border-primary/10">
<template v-if="getLangIcon(selectedLang.plugin)">
<div class="w-full h-full bg-white/90 p-2.5">
<img :src="getLangIcon(selectedLang.plugin)" class="w-full h-full object-contain" />
</div>
</template>
<template v-else>
<span class="text-lg">{{ selectedLang.plugin.substring(0, 2) }}</span>
</template>
</div>
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-lg capitalize">{{ selectedLang.plugin }}</span>
<Badge variant="outline" class="font-mono text-xs">{{ selectedLang.version }}</Badge>
</div>
<div class="text-xs text-muted-foreground mt-1">
{{ selectedLang.isGlobal ? '系统全局默认版本' : '普通已安装版本' }}
</div>
</div>
</div>
<div class="space-y-3">
<div class="space-y-1.5">
<div class="text-xs font-bold text-muted-foreground uppercase tracking-wider px-1">
完整安装路径</div>
<div
class="p-3 bg-muted/50 rounded-lg border border-border/40 break-all text-xs font-mono leading-relaxed select-all">
{{ selectedLang.source }}
</div>
</div>
<div v-if="selectedLang.installed_at" class="flex flex-row items-center justify-between px-1">
<div class="text-xs font-bold text-muted-foreground uppercase tracking-wider">安装日期</div>
<div class="text-xs font-mono text-muted-foreground">{{ selectedLang.installed_at }}</div>
</div>
</div>
</div>
</template>
</BaihuDialog>
</div>
</template>
+42 -35
View File
@@ -8,13 +8,7 @@ import TextOverflow from '@/components/TextOverflow.vue'
import { api } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription
} from '@/components/ui/dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
const { pageSize } = useSiteSettings()
@@ -214,36 +208,49 @@ onMounted(loadLogs)
</div>
<!-- IP 地理位置弹窗 -->
<Dialog v-model:open="ipDialogOpen">
<DialogContent class="max-w-[90vw] sm:max-w-md">
<DialogHeader>
<DialogTitle>IP 详情</DialogTitle>
<DialogDescription>
<code class="text-xs bg-muted px-2 py-0.5 rounded">{{ selectedIp }}</code>
</DialogDescription>
</DialogHeader>
<div v-if="ipGeoLoading" class="flex items-center justify-center py-8">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
</div>
<div v-else-if="ipGeoInfo" class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-xs sm:text-sm">
<div class="text-muted-foreground">国家</div>
<div class="font-medium">{{ ipGeoInfo.country }} ({{ ipGeoInfo.country_code }})</div>
<div class="text-muted-foreground">运营商</div>
<div class="font-medium truncate">{{ ipGeoInfo.isp || '-' }}</div>
<div class="text-muted-foreground">组织</div>
<div class="font-medium truncate">{{ ipGeoInfo.organization || '-' }}</div>
<div class="text-muted-foreground">ASN</div>
<div class="font-medium truncate">{{ ipGeoInfo.asn }} - {{ ipGeoInfo.asn_organization || '-' }}
<BaihuDialog v-model:open="ipDialogOpen" title="IP 详情信息">
<template #description>
<code class="text-[10px] bg-muted px-2 py-0.5 rounded-md font-mono">{{ selectedIp }}</code>
</template>
<div v-if="ipGeoLoading" class="flex items-center justify-center py-12">
<Loader2 class="h-8 w-8 animate-spin text-primary/40" />
</div>
<div v-else-if="ipGeoInfo" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="p-3 rounded-xl bg-muted/20 border border-border/10 space-y-1">
<p class="text-[10px] uppercase tracking-wider text-muted-foreground font-bold">国家/地区</p>
<p class="text-sm font-medium">{{ ipGeoInfo.country }} ({{ ipGeoInfo.country_code }})</p>
</div>
<div class="p-3 rounded-xl bg-muted/20 border border-border/10 space-y-1">
<p class="text-[10px] uppercase tracking-wider text-muted-foreground font-bold">时区</p>
<p class="text-sm font-medium">{{ ipGeoInfo.timezone || '-' }}</p>
</div>
<div class="text-muted-foreground">时区</div>
<div class="font-medium">{{ ipGeoInfo.timezone || '-' }}</div>
<div class="text-muted-foreground">坐标</div>
<div class="font-medium">{{ ipGeoInfo.latitude }}, {{ ipGeoInfo.longitude }}</div>
</div>
<div v-else class="text-center text-muted-foreground py-4">
无法获取 IP 信息
<div class="p-4 rounded-xl bg-muted/20 border border-border/10 space-y-3">
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground">运营商</span>
<span class="font-medium truncate ml-4">{{ ipGeoInfo.isp || '-' }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground">组织</span>
<span class="font-medium truncate ml-4">{{ ipGeoInfo.organization || '-' }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground">ASN</span>
<span class="font-medium truncate ml-4">{{ ipGeoInfo.asn }} - {{ ipGeoInfo.asn_organization || '-' }}</span>
</div>
</div>
</DialogContent>
</Dialog>
<div class="flex items-center justify-between p-3 rounded-xl bg-primary/5 border border-primary/10 text-xs">
<span class="text-muted-foreground font-medium">地理坐标</span>
<span class="font-mono text-primary/70">{{ ipGeoInfo.latitude }}, {{ ipGeoInfo.longitude }}</span>
</div>
</div>
<div v-else class="text-center text-muted-foreground py-8 italic text-sm">
无法获取 IP 信息请稍后再试
</div>
</BaihuDialog>
</div>
</template>
+39 -91
View File
@@ -12,27 +12,12 @@ import {
SelectValue,
} from '@/components/ui/select'
import Pagination from '@/components/Pagination.vue'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { toast } from 'vue-sonner'
import { format } from 'date-fns'
import {
RefreshCw, Trash2, Search, Info, AlertTriangle, AlertCircle
RefreshCw, Search, Info, AlertTriangle, AlertCircle
} from 'lucide-vue-next'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { useSiteSettings } from '@/composables/useSiteSettings'
const { pageSize } = useSiteSettings()
@@ -193,27 +178,15 @@ function onDialogClose(open: boolean) {
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
</Button>
</div>
<AlertDialog :open="showClearConfirm" @update:open="showClearConfirm = $event">
<Button variant="outline"
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20 w-full sm:w-auto"
@click="showClearConfirm = true">
<Trash2 class="h-4 w-4 mr-2" /> <span>清空记录</span>
</Button>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认清空所有系统事件</AlertDialogTitle>
<AlertDialogDescription>
此操作将永久清空当前分类下的所有系统事件记录操作后无法恢复确认要继续吗
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="handleClear" variant="destructive">
确认清空
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showClearConfirm" title="清空系统事件确认">
<div class="text-sm text-muted-foreground leading-relaxed">
此操作将永久清空当前分类下的所有系统事件记录操作后无法恢复确认要继续吗
</div>
<template #footer>
<Button variant="ghost" @click="showClearConfirm = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="handleClear">确认清空</Button>
</template>
</BaihuDialog>
</div>
<div class="rounded-lg border bg-card overflow-hidden">
@@ -302,65 +275,40 @@ function onDialogClose(open: boolean) {
<Pagination :total="total" :page="filters.page" @update:page="handlePageChange" />
</div>
<Dialog v-model:open="detailDialogProps.open" @update:open="onDialogClose">
<DialogContent class="sm:max-w-2xl max-h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader class="px-6 py-4 border-b bg-muted/20">
<div class="flex items-center justify-between pr-8">
<DialogTitle>事件详情</DialogTitle>
<Badge variant="outline" :class="[
'px-2 py-0.5 text-[10px] font-bold rounded-md border shadow-sm',
selectedLog ? getLevelBadgeClass(selectedLog.level) : ''
]">
<div class="flex items-center gap-1 uppercase tracking-tighter">
<component :is="getLevelIcon(selectedLog?.level || 'info')" class="h-3 w-3" />
<span>{{ selectedLog?.level || 'INFO' }}</span>
</div>
</Badge>
</div>
</DialogHeader>
<BaihuDialog v-model:open="detailDialogProps.open" :title="detailDialogProps.title" @update:open="onDialogClose">
<template #description>
<div class="flex items-center gap-2">
<Badge v-if="selectedLog" variant="outline" :class="[
'px-2 py-0.5 text-[10px] font-bold rounded-md border shadow-sm',
getLevelBadgeClass(selectedLog.level)
]">
<div class="flex items-center gap-1 uppercase tracking-tighter">
<component :is="getLevelIcon(selectedLog.level)" class="h-3 w-3" />
<span>{{ selectedLog.level }}</span>
</div>
</Badge>
<span class="text-[10px] text-muted-foreground font-mono">{{ selectedLog ? formatDate(selectedLog.created_at) : '-' }}</span>
</div>
</template>
<div class="flex-1 overflow-y-auto">
<div class="px-6 py-4 border-b space-y-3 bg-card">
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground font-medium">标题</span>
<span class="font-bold text-foreground">{{ detailDialogProps.title }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground font-medium">发生时间</span>
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ?
formatDate(selectedLog.created_at) : '-' }}</span>
<div class="space-y-6">
<div class="space-y-4">
<div class="p-4 rounded-xl bg-muted/20 border border-border/10 space-y-2">
<p class="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">事件内容</p>
<div v-if="detailDialogProps.content" class="text-sm leading-relaxed text-foreground/80 break-all whitespace-pre-wrap">
{{ detailDialogProps.content }}
</div>
<div v-else class="text-sm text-muted-foreground italic">无内容</div>
</div>
<div class="flex flex-col min-h-0 bg-muted/5">
<div
class="px-6 py-2.5 text-[10px] font-bold text-muted-foreground border-b bg-muted/10 uppercase tracking-widest">
内容详情
<div v-if="detailDialogProps.error" class="p-4 rounded-xl bg-destructive/5 border border-destructive/10 space-y-2">
<p class="text-[10px] uppercase tracking-widest text-destructive font-bold">错误堆栈/信息</p>
<div class="text-sm leading-relaxed text-destructive/90 break-all whitespace-pre-wrap font-mono bg-destructive/5 p-3 rounded-lg border border-destructive/10">
{{ detailDialogProps.error }}
</div>
<div class="p-6">
<div v-if="detailDialogProps.content"
class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
{{ detailDialogProps.content }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无内容</div>
</div>
<template v-if="detailDialogProps.error">
<div
class="px-6 py-2.5 text-[10px] font-bold uppercase tracking-widest border-y bg-muted/10 text-muted-foreground border-border/60">
错误信息
</div>
<div class="p-6">
<div v-if="detailDialogProps.error"
class="text-sm p-5 rounded-xl border whitespace-pre-wrap break-all leading-relaxed shadow-sm bg-muted/20 border-border/60 text-foreground">
{{ detailDialogProps.error }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无错误信息</div>
</div>
</template>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</BaihuDialog>
</div>
</template>
+11 -24
View File
@@ -1,16 +1,8 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { api, type NotifyChannel, type ChannelType, type EventType, type NotifyBinding, type Task } from '@/api'
import { toast } from 'vue-sonner'
import ChannelList from './components/ChannelList.vue'
@@ -363,19 +355,14 @@ onMounted(() => {
@save="saveChannel" />
<!-- 删除确认 -->
<AlertDialog :open="showDeleteConfirm" @update:open="showDeleteConfirm = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
删除后将无法恢复同时会取消该渠道的所有事件绑定确定要删除吗
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="deleteChannel">确认删除</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showDeleteConfirm" title="确认删除通知渠道?">
<div class="text-[15px] leading-relaxed text-muted-foreground">
删除后将无法恢复同时会取消该渠道的所有事件绑定确定要删除吗
</div>
<template #footer>
<Button variant="ghost" @click="showDeleteConfirm = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="deleteChannel">确认删除</Button>
</template>
</BaihuDialog>
</div>
</template>
+24 -26
View File
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { RefreshCw, FolderPlus, FilePlus, Save } from 'lucide-vue-next'
import { api, type FileNode } from '@/api'
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
@@ -380,31 +380,29 @@ onMounted(loadTree)
</DialogContent>
</Dialog>
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="text-sm">确认删除</AlertDialogTitle>
<AlertDialogDescription class="text-xs">确定要删除 {{ deletePath }} 此操作无法撤销</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel class="h-7 text-xs">取消</AlertDialogCancel>
<AlertDialogAction class="h-7 text-xs bg-destructive text-white hover:bg-destructive/90"
@click="handleDelete">删除</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 删除确认 -->
<BaihuDialog v-model:open="showDeleteDialog" title="确认删除文件?">
<div class="space-y-3">
<p class="text-[15px] leading-relaxed text-muted-foreground">确定要删除以下脚本吗此操作无法撤销</p>
<div class="bg-muted/30 p-3 rounded-lg border border-border/40 font-mono text-[11px] break-all text-destructive/80">
{{ deletePath }}
</div>
</div>
<template #footer>
<Button variant="ghost" @click="showDeleteDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="handleDelete">确认删除</Button>
</template>
</BaihuDialog>
<AlertDialog v-model:open="showUnsavedDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="text-sm">未保存的更改</AlertDialogTitle>
<AlertDialogDescription class="text-xs">当前文件有未保存的更改确定要切换文件吗</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel class="h-7 text-xs">取消</AlertDialogCancel>
<AlertDialogAction class="h-7 text-xs" @click="confirmSwitchFile">确定切换</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 未保存更改确认 -->
<BaihuDialog v-model:open="showUnsavedDialog" title="未保存的更改">
<div class="text-[15px] leading-relaxed text-muted-foreground">
当前文件有未保存的更改确定要切换文件吗未保存的内容将会丢失
</div>
<template #footer>
<Button variant="ghost" @click="showUnsavedDialog = false">留在此页</Button>
<Button @click="confirmSwitchFile">确定切换</Button>
</template>
</BaihuDialog>
</div>
</template>
+26 -34
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, computed, watch, onUnmounted } from 'vue'
import { Button } from '@/components/ui/button'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
import { Input } from '@/components/ui/input'
import Pagination from '@/components/Pagination.vue'
import TaskDialog from './TaskDialog.vue'
@@ -347,15 +347,15 @@ watch(() => route.query.agent_id, (newVal: any) => {
<p class="text-muted-foreground text-sm">管理和调度自动化执行任务</p>
</div>
<div class="flex flex-col sm:flex-row flex-wrap gap-3 w-full lg:w-auto lg:ml-auto lg:justify-end">
<div class="flex flex-row items-center flex-wrap gap-2 w-full lg:w-auto lg:ml-auto lg:justify-end">
<!-- 搜索与标签 -->
<div class="flex flex-col sm:flex-row items-center gap-2 w-full sm:flex-1 lg:flex-none lg:w-auto text-sm">
<div class="relative w-full sm:flex-1 lg:max-w-[240px] group">
<div class="flex flex-row items-center gap-2 flex-1 sm:flex-1 lg:flex-none lg:w-auto text-sm">
<div class="relative flex-1 sm:flex-1 lg:max-w-[240px] group">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background"
@input="handleSearch" />
</div>
<div class="relative w-full sm:flex-1 lg:max-w-[180px] group">
<div class="relative flex-1 sm:flex-1 lg:max-w-[180px] group">
<Tag class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
<Input v-model="filterTags" placeholder="搜索标签..." class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background"
@input="handleSearch" />
@@ -612,37 +612,29 @@ watch(() => route.query.agent_id, (newVal: any) => {
:empty-title="logEmptyTitle"
:empty-description="logEmptyDesc" />
<!-- 删除确认 (批量) -->
<AlertDialog v-model:open="showBatchDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认批量删除</AlertDialogTitle>
<AlertDialogDescription>
将会删除当前所有过滤条件下匹配的 <b>{{ total }}</b> 个任务。操作不可撤销。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="batchDeleteTasks">
确认删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showBatchDeleteDialog" title="确认批量删除">
<div class="text-sm text-muted-foreground leading-relaxed">
将会删除当前所有过滤条件下匹配的 <b class="text-foreground text-lg px-1">{{ total }}</b> 个任务。
<p class="mt-2 text-destructive font-medium">⚠️ 操作不可撤销,请谨慎操作。</p>
</div>
<template #footer>
<Button variant="ghost" @click="showBatchDeleteDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="batchDeleteTasks">确认批量删除</Button>
</template>
</BaihuDialog>
<!-- 删除确认 (单个) -->
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>确定要删除此任务吗?此操作无法撤销。</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteTask">删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<BaihuDialog v-model:open="showDeleteDialog" title="确认删除任务">
<div class="text-sm text-muted-foreground leading-relaxed">
确定要删除任务 <b class="text-foreground">{{ tasks.find(t => t.id === deleteTaskId)?.name }}</b> 吗?
<p class="mt-2 text-destructive font-medium">⚠️ 此操作无法撤销。</p>
</div>
<template #footer>
<Button variant="ghost" @click="showDeleteDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="deleteTask">确认删除</Button>
</template>
</BaihuDialog>
</div>
</template>