chore: update page style mobile #20
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user