chore: add loading animation
This commit is contained in:
@@ -27,6 +27,7 @@ const selectedFile = ref<string | null>(null)
|
|||||||
const fileContent = ref('')
|
const fileContent = ref('')
|
||||||
const originalContent = ref('')
|
const originalContent = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
const isEditMode = ref(false)
|
const isEditMode = ref(false)
|
||||||
const hasChanges = computed(() => fileContent.value !== originalContent.value)
|
const hasChanges = computed(() => fileContent.value !== originalContent.value)
|
||||||
|
|
||||||
@@ -142,10 +143,13 @@ function handleResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadTree() {
|
async function loadTree() {
|
||||||
|
isRefreshing.value = true
|
||||||
try {
|
try {
|
||||||
fileTree.value = await api.files.tree()
|
fileTree.value = await api.files.tree()
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('加载文件树失败')
|
toast.error('加载文件树失败')
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +442,7 @@ onUnmounted(() => {
|
|||||||
:file-tree="fileTree"
|
:file-tree="fileTree"
|
||||||
:expanded-dirs="expandedDirs"
|
:expanded-dirs="expandedDirs"
|
||||||
:selected-path="selectedPath"
|
:selected-path="selectedPath"
|
||||||
|
:is-refreshing="isRefreshing"
|
||||||
@refresh="loadTree"
|
@refresh="loadTree"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
@delete="confirm => dialogsRef?.openDelete(confirm)"
|
@delete="confirm => dialogsRef?.openDelete(confirm)"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defineProps<{
|
|||||||
fileTree: FileNode[]
|
fileTree: FileNode[]
|
||||||
expandedDirs: Set<string>
|
expandedDirs: Set<string>
|
||||||
selectedPath: string | null
|
selectedPath: string | null
|
||||||
|
isRefreshing?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -67,8 +68,8 @@ function handleFilesUpload(e: Event) {
|
|||||||
<div class="flex items-center justify-between p-2 border-b">
|
<div class="flex items-center justify-between p-2 border-b">
|
||||||
<span class="text-xs font-medium">脚本文件</span>
|
<span class="text-xs font-medium">脚本文件</span>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('refresh')" title="刷新">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('refresh')" :disabled="isRefreshing" title="刷新">
|
||||||
<RefreshCw class="h-3 w-3" />
|
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': isRefreshing }" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="triggerFilesUpload('')" title="上传文件/文件夹(放在根目录)">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="triggerFilesUpload('')" title="上传文件/文件夹(放在根目录)">
|
||||||
<FolderUp class="h-3 w-3" />
|
<FolderUp class="h-3 w-3" />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Pagination from '@/components/Pagination.vue'
|
|||||||
import TaskDialog from './TaskDialog.vue'
|
import TaskDialog from './TaskDialog.vue'
|
||||||
import RepoDialog from './RepoDialog.vue'
|
import RepoDialog from './RepoDialog.vue'
|
||||||
import LogViewer from '@/views/history/LogViewer.vue'
|
import LogViewer from '@/views/history/LogViewer.vue'
|
||||||
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, Wifi, WifiOff, Zap, ZapOff, Copy, Tag } from 'lucide-vue-next'
|
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag } from 'lucide-vue-next'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { api, type Agent, type Task } from '@/api'
|
import { api, type Agent, type Task } from '@/api'
|
||||||
@@ -36,6 +36,7 @@ const filterType = ref<string>(TASK_TYPE.NORMAL)
|
|||||||
const filterAgentId = ref<string | null>(null)
|
const filterAgentId = ref<string | null>(null)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 创建 agent 映射表
|
// 创建 agent 映射表
|
||||||
@@ -67,6 +68,7 @@ function getExecutorStatus(task: Task): 'local' | 'online' | 'offline' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadTasks() {
|
async function loadTasks() {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await api.tasks.list({
|
const res = await api.tasks.list({
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
@@ -78,7 +80,9 @@ async function loadTasks() {
|
|||||||
})
|
})
|
||||||
tasks.value = res.data
|
tasks.value = res.data
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
} catch { toast.error('加载任务失败') }
|
} catch { toast.error('加载任务失败') } finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAgents() {
|
async function loadAgents() {
|
||||||
@@ -347,6 +351,10 @@ watch(() => route.query.agent_id, (newVal) => {
|
|||||||
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
|
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadTasks" :disabled="loading" title="刷新">
|
||||||
|
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 shrink-0 justify-end">
|
<div class="flex items-center gap-2 shrink-0 justify-end">
|
||||||
<!-- 动态新增按钮 -->
|
<!-- 动态新增按钮 -->
|
||||||
<Button variant="outline" class="shrink-0 px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete">
|
<Button variant="outline" class="shrink-0 px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete">
|
||||||
|
|||||||
@@ -18,8 +18,15 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isReconnecting = ref(false)
|
||||||
|
|
||||||
function reconnect() {
|
function reconnect() {
|
||||||
|
isReconnecting.value = true
|
||||||
terminalRef.value?.reconnect()
|
terminalRef.value?.reconnect()
|
||||||
|
// Add a slight delay to the animation so it's visible even for fast reconnections
|
||||||
|
setTimeout(() => {
|
||||||
|
isReconnecting.value = false
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -52,8 +59,8 @@ function reconnect() {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect"
|
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect"
|
||||||
title="重新连接">
|
:disabled="isReconnecting" title="重新连接">
|
||||||
<RefreshCw class="h-3 w-3" />
|
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': isReconnecting }" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 border border-[#3c3c3c] border-t-0 rounded-b-md overflow-hidden bg-[#1e1e1e]">
|
<div class="flex-1 border border-[#3c3c3c] border-t-0 rounded-b-md overflow-hidden bg-[#1e1e1e]">
|
||||||
|
|||||||
Reference in New Issue
Block a user