feat: openapi supoort -- fix list page

This commit is contained in:
engigu
2026-03-05 22:56:15 +08:00
parent 863a298609
commit 40c3c58312
7 changed files with 99 additions and 61 deletions
+19 -4
View File
@@ -178,11 +178,12 @@ func ClearAuthCookie(c *gin.Context) {
// SwaggerAuth Swagger 认证中间件 (Basic Auth) // SwaggerAuth Swagger 认证中间件 (Basic Auth)
func SwaggerAuth() gin.HandlerFunc { func SwaggerAuth() gin.HandlerFunc {
settingsSvc := services.NewSettingsService()
return func(c *gin.Context) { return func(c *gin.Context) {
settingsSvc := services.NewSettingsService()
siteConfig := settingsSvc.GetSection(constant.SectionSite) siteConfig := settingsSvc.GetSection(constant.SectionSite)
tokenJson, ok := siteConfig[constant.KeyOpenapiToken] tokenJson := siteConfig[constant.KeyOpenapiToken]
if !ok || tokenJson == "" {
if tokenJson == "" {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
c.Abort() c.Abort()
return return
@@ -202,6 +203,20 @@ func SwaggerAuth() gin.HandlerFunc {
return return
} }
// 检查过期时间
if tokenConfig.ExpireAt != "" {
expire, err := time.ParseInLocation("2006/01/02", tokenConfig.ExpireAt, time.Local)
if err == nil {
// 包含当天,所以设置到当天 23:59:59
expire = expire.Add(24*time.Hour - time.Second)
if time.Now().After(expire) {
c.Status(http.StatusNotFound)
c.Abort()
return
}
}
}
_, password, hasAuth := c.Request.BasicAuth() _, password, hasAuth := c.Request.BasicAuth()
// 允许使用任意用户名,但密码必须匹配 OpenAPI Token // 允许使用任意用户名,但密码必须匹配 OpenAPI Token
if hasAuth && password == tokenConfig.Token && tokenConfig.Token != "" { if hasAuth && password == tokenConfig.Token && tokenConfig.Token != "" {
@@ -209,7 +224,7 @@ func SwaggerAuth() gin.HandlerFunc {
return return
} }
// 认证失败,提示输入密码 (如果未提供认证) // 未提供认证,触发浏览器登录弹窗
if !hasAuth { if !hasAuth {
c.Header("WWW-Authenticate", `Basic realm="OpenAPI Access Token (Any username)"`) c.Header("WWW-Authenticate", `Basic realm="OpenAPI Access Token (Any username)"`)
c.Status(http.StatusUnauthorized) c.Status(http.StatusUnauthorized)
+25 -10
View File
@@ -89,7 +89,7 @@ func Setup(c *Controllers) *gin.Engine {
} }
// OpenAPI documentation using Scalar UI (带 Basic Auth 认证) // OpenAPI documentation using Scalar UI (带 Basic Auth 认证)
router.GET("/openapi/*any", func(c *gin.Context) { root.GET("/openapi/*any", func(c *gin.Context) {
settingsSvc := services.NewSettingsService() settingsSvc := services.NewSettingsService()
siteConfig := settingsSvc.GetSection(constant.SectionSite) siteConfig := settingsSvc.GetSection(constant.SectionSite)
tokenJson := siteConfig[constant.KeyOpenapiToken] tokenJson := siteConfig[constant.KeyOpenapiToken]
@@ -118,12 +118,13 @@ func Setup(c *Controllers) *gin.Engine {
return return
} }
// 获取内部路径(移除开头的斜杠) // 获取内部路径并标准化(移除前后的所有斜杠)
path := strings.TrimPrefix(c.Param("any"), "/") // c.Param("any") 对于 *any 匹配通常包含领先斜杠,如 "/index.html"
path := strings.Trim(c.Param("any"), "/")
// 1. 根路径或空路径 -> 重定向到 index.html // 1. 根路径或空路径 -> 重定向到 index.html
if path == "" || path == "/" { if path == "" {
c.Redirect(http.StatusMovedPermanently, urlPrefix+"/openapi/index.html") c.Redirect(http.StatusMovedPermanently, c.Request.URL.Path+"index.html")
return return
} }
@@ -145,7 +146,8 @@ func Setup(c *Controllers) *gin.Engine {
</body> </body>
</html>` </html>`
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, scalarHTML) c.Status(http.StatusOK)
c.Writer.Write([]byte(scalarHTML))
c.Abort() c.Abort()
return return
} }
@@ -158,7 +160,7 @@ func Setup(c *Controllers) *gin.Engine {
return return
} }
// 4. 其余路径一律返回 SPA 的 404 // 其他未匹配路径 -> 返回 404 SPA 页面
serveSPA(c, urlPrefix, 404) serveSPA(c, urlPrefix, 404)
}) })
@@ -399,10 +401,23 @@ func Setup(c *Controllers) *gin.Engine {
func serveSPA(ctx *gin.Context, urlPrefix string, status int) { func serveSPA(ctx *gin.Context, urlPrefix string, status int) {
data, err := static.ReadFile("index.html") data, err := static.ReadFile("index.html")
if err != nil { if err != nil {
// 如果读不到 index.html (如 dev 模式未 build),返回基础 HTML 触发前端路由 // 如果读不到 index.html (如 dev 模式未 build),返回基础 HTML
// 如果已经是 /404 路径,则不再重定向以免死循环
path := ctx.Request.URL.Path
if strings.HasSuffix(path, "/404") {
ctx.Data(status, "text/html; charset=utf-8", []byte("<!DOCTYPE html><html><body><h1>404 Not Found</h1><p>Frontend assets not found. Please run 'npm run build' or check dev server.</p><a href='/'>Go Home</a></body></html>"))
ctx.Abort()
return
}
fallback := `<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404 Not Found</title></head><body> fallback := `<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404 Not Found</title></head><body>
<script>window.location.href = (window.__BASE_URL__ || "/") + "404";</script> <script>
<p>Not Found. Redirecting to home...</p> const baseUrl = window.__BASE_URL__ || "/";
if (!window.location.pathname.endsWith("/404")) {
window.location.href = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "404";
}
</script>
<p>Not Found. Redirecting...</p>
</body></html>` </body></html>`
ctx.Header("Content-Type", "text/html; charset=utf-8") ctx.Header("Content-Type", "text/html; charset=utf-8")
ctx.Data(status, "text/html", []byte(fallback)) ctx.Data(status, "text/html", []byte(fallback))
+4 -4
View File
@@ -344,7 +344,7 @@ export interface ExecutionResult {
} }
export interface TaskListResponse { export interface TaskListResponse {
data: Task[] list: Task[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -365,7 +365,7 @@ export interface EnvVar {
} }
export interface EnvListResponse { export interface EnvListResponse {
data: EnvVar[] list: EnvVar[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -396,7 +396,7 @@ export interface TaskLog {
} }
export interface LogListResponse { export interface LogListResponse {
data: TaskLog[] list: TaskLog[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -457,7 +457,7 @@ export interface LoginLog {
} }
export interface LoginLogListResponse { export interface LoginLogListResponse {
data: LoginLog[] list: LoginLog[]
total: number total: number
page: number page: number
page_size: number page_size: number
+6 -4
View File
@@ -34,10 +34,10 @@ let searchTimer: ReturnType<typeof setTimeout> | null = null
async function loadEnvVars() { async function loadEnvVars() {
try { try {
const res = await api.env.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined }) const res = await api.env.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined })
envVars.value = res.data envVars.value = res.list
total.value = res.total total.value = res.total
// hidden // hidden
res.data.forEach(env => { res.list.forEach(env => {
showValues.value[env.id] = !env.hidden showValues.value[env.id] = !env.hidden
}) })
} catch { toast.error('加载环境变量失败') } } catch { toast.error('加载环境变量失败') }
@@ -251,7 +251,8 @@ onMounted(loadEnvVars)
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between px-1"> <div class="flex items-center justify-between px-1">
<p class="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">关联任务 ({{ associatedTasks.length }})</p> <p class="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">关联任务 ({{
associatedTasks.length }})</p>
</div> </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 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" <div v-for="task in associatedTasks" :key="task.id"
@@ -260,7 +261,8 @@ onMounted(loadEnvVars)
<Terminal class="h-3 w-3 text-primary/70" /> <Terminal class="h-3 w-3 text-primary/70" />
<span class="font-medium truncate">{{ task.name }}</span> <span class="font-medium truncate">{{ task.name }}</span>
</div> </div>
<code class="text-[10px] text-muted-foreground/70 font-mono bg-muted/50 px-1.5 py-0.5 rounded">{{ task.id }}</code> <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>
</div> </div>
+25 -14
View File
@@ -76,7 +76,7 @@ async function loadLogs() {
params.status = filterStatus.value params.status = filterStatus.value
} }
const response = await api.logs.list(params) const response = await api.logs.list(params)
logs.value = response.data logs.value = response.list
total.value = response.total total.value = response.total
} catch { } catch {
toast.error('加载日志失败') toast.error('加载日志失败')
@@ -251,12 +251,12 @@ async function handleDeleteLog() {
try { try {
await api.logs.delete(deleteLogId.value) await api.logs.delete(deleteLogId.value)
toast.success('该日志已删除') toast.success('该日志已删除')
// //
if (selectedLog.value?.id === deleteLogId.value) { if (selectedLog.value?.id === deleteLogId.value) {
closeDetail() closeDetail()
} }
showDeleteDialog.value = false showDeleteDialog.value = false
loadLogs() loadLogs()
} catch (err: any) { } catch (err: any) {
@@ -340,7 +340,9 @@ watch(() => route.query, (newQuery) => {
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" title="刷新"> <Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" title="刷新">
<RefreshCw class="h-4 w-4" /> <RefreshCw class="h-4 w-4" />
</Button> </Button>
<Button variant="outline" class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20" @click="showClearDialog = true"> <Button variant="outline"
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20"
@click="showClearDialog = true">
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline" style="padding-left: 2px;">清空日志</span> <Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline" style="padding-left: 2px;">清空日志</span>
</Button> </Button>
</div> </div>
@@ -382,7 +384,8 @@ watch(() => route.query, (newQuery) => {
]" @click="selectLog(log)"> ]" @click="selectLog(log)">
<!-- 小屏行 --> <!-- 小屏行 -->
<div class="flex sm:hidden items-center gap-2 px-3 py-2"> <div class="flex sm:hidden items-center gap-2 px-3 py-2">
<span class="w-14 shrink-0 text-muted-foreground text-xs">#{{ total - (currentPage - 1) * pageSize - index }}</span> <span class="w-14 shrink-0 text-muted-foreground text-xs">#{{ total - (currentPage - 1) * pageSize - index
}}</span>
<span class="w-6 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')"> <span class="w-6 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" /> <GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
<Terminal v-else class="h-3.5 w-3.5 text-primary" /> <Terminal v-else class="h-3.5 w-3.5 text-primary" />
@@ -415,16 +418,19 @@ watch(() => route.query, (newQuery) => {
</div> </div>
</span> </span>
<span class="w-12 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration) <span class="w-12 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration)
}}</span> }}</span>
<span class="w-8 shrink-0 flex justify-center opacity-100"> <span class="w-8 shrink-0 flex justify-center opacity-100">
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0" @click.stop="confirmDeleteLog(log.id)" title="删除该日志"> <Button variant="ghost" size="icon"
class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0"
@click.stop="confirmDeleteLog(log.id)" title="删除该日志">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
</span> </span>
</div> </div>
<!-- 大屏行 --> <!-- 大屏行 -->
<div class="hidden sm:flex items-center gap-4 px-4 py-2"> <div class="hidden sm:flex items-center gap-4 px-4 py-2">
<span class="w-16 shrink-0 text-muted-foreground text-sm">#{{ total - (currentPage - 1) * pageSize - index }}</span> <span class="w-16 shrink-0 text-muted-foreground text-sm">#{{ total - (currentPage - 1) * pageSize - index
}}</span>
<span class="w-10 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')"> <span class="w-10 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" /> <GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
<Terminal v-else class="h-4 w-4 text-primary" /> <Terminal v-else class="h-4 w-4 text-primary" />
@@ -460,12 +466,14 @@ watch(() => route.query, (newQuery) => {
</div> </div>
</span> </span>
<span class="w-16 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration) <span class="w-16 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration)
}}</span> }}</span>
<span v-if="!selectedLog" <span v-if="!selectedLog"
class="w-40 text-right shrink-0 text-muted-foreground text-xs hidden md:block">{{ log.start_time || class="w-40 text-right shrink-0 text-muted-foreground text-xs hidden md:block">{{ log.start_time ||
log.created_at }}</span> log.created_at }}</span>
<span class="w-10 shrink-0 flex justify-center opacity-100"> <span class="w-10 shrink-0 flex justify-center opacity-100">
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0" @click.stop="confirmDeleteLog(log.id)" title="删除该日志"> <Button variant="ghost" size="icon"
class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0"
@click.stop="confirmDeleteLog(log.id)" title="删除该日志">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
</span> </span>
@@ -488,7 +496,8 @@ watch(() => route.query, (newQuery) => {
</Button> </Button>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button variant="ghost" size="icon" class="h-7 w-7 text-muted-foreground hover:text-destructive" title="删除该日志" @click="confirmDeleteLog(selectedLog.id)"> <Button variant="ghost" size="icon" class="h-7 w-7 text-muted-foreground hover:text-destructive"
title="删除该日志" @click="confirmDeleteLog(selectedLog.id)">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail" title="关闭"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail" title="关闭">
@@ -567,7 +576,7 @@ watch(() => route.query, (newQuery) => {
<!-- 全屏查看日志 --> <!-- 全屏查看日志 -->
<LogViewer v-model:open="showFullscreen" :title="`日志输出 - ${selectedLog?.task_name || ''}`" <LogViewer v-model:open="showFullscreen" :title="`日志输出 - ${selectedLog?.task_name || ''}`"
:content="decompressedOutput" :status="selectedLog?.status" /> :content="decompressedOutput" :status="selectedLog?.status" />
<!-- 清空日志确认弹窗 --> <!-- 清空日志确认弹窗 -->
<AlertDialog :open="showClearDialog" @update:open="showClearDialog = $event"> <AlertDialog :open="showClearDialog" @update:open="showClearDialog = $event">
<AlertDialogContent> <AlertDialogContent>
@@ -579,7 +588,8 @@ watch(() => route.query, (newQuery) => {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel> <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 @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> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
@@ -597,7 +607,8 @@ watch(() => route.query, (newQuery) => {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel> <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 @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> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
+9 -16
View File
@@ -81,7 +81,7 @@ async function loadLogs() {
page_size: pageSize.value, page_size: pageSize.value,
username: filterUsername.value || undefined username: filterUsername.value || undefined
}) })
logs.value = res.data logs.value = res.list
total.value = res.total total.value = res.total
} catch { } catch {
toast.error('加载登录日志失败') toast.error('加载登录日志失败')
@@ -116,12 +116,8 @@ onMounted(loadLogs)
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="relative flex-1 sm:flex-none"> <div class="relative flex-1 sm:flex-none">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input v-model="filterUsername" placeholder="搜索用户名..." class="h-9 pl-9 w-full sm:w-56 text-sm"
v-model="filterUsername" @input="handleSearch" />
placeholder="搜索用户名..."
class="h-9 pl-9 w-full sm:w-56 text-sm"
@input="handleSearch"
/>
</div> </div>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" :disabled="loading"> <Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" :disabled="loading">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" /> <RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
@@ -131,7 +127,8 @@ onMounted(loadLogs)
<div class="rounded-lg border bg-card overflow-x-auto"> <div class="rounded-lg border bg-card overflow-x-auto">
<!-- 表头 --> <!-- 表头 -->
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium sm:min-w-[500px]"> <div
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium sm:min-w-[500px]">
<span class="w-16 sm:w-24 shrink-0">用户名</span> <span class="w-16 sm:w-24 shrink-0">用户名</span>
<span class="w-20 sm:w-32 shrink-0">IP 地址</span> <span class="w-20 sm:w-32 shrink-0">IP 地址</span>
<span class="w-10 sm:w-16 shrink-0 text-center">状态</span> <span class="w-10 sm:w-16 shrink-0 text-center">状态</span>
@@ -143,16 +140,12 @@ onMounted(loadLogs)
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8"> <div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
暂无登录日志 暂无登录日志
</div> </div>
<div <div v-for="log in logs" :key="log.id"
v-for="log in logs" class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
:key="log.id"
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors"
>
<span class="w-16 sm:w-24 shrink-0 font-medium text-xs sm:text-sm truncate">{{ log.username }}</span> <span class="w-16 sm:w-24 shrink-0 font-medium text-xs sm:text-sm truncate">{{ log.username }}</span>
<code <code
class="w-20 sm:w-32 shrink-0 text-xs text-muted-foreground bg-muted px-1 sm:px-2 py-0.5 sm:py-1 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors" class="w-20 sm:w-32 shrink-0 text-xs text-muted-foreground bg-muted px-1 sm:px-2 py-0.5 sm:py-1 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors"
@click="showIpInfo(log.ip)" @click="showIpInfo(log.ip)">{{ log.ip }}</code>
>{{ log.ip }}</code>
<span class="w-10 sm:w-16 shrink-0 flex justify-center"> <span class="w-10 sm:w-16 shrink-0 flex justify-center">
<span :class="['h-2 w-2 rounded-full', log.status === 'success' ? 'bg-green-500' : 'bg-red-500']"></span> <span :class="['h-2 w-2 rounded-full', log.status === 'success' ? 'bg-green-500' : 'bg-red-500']"></span>
</span> </span>
+11 -9
View File
@@ -74,7 +74,7 @@ async function loadTasks() {
type: filterType.value === 'all' ? undefined : filterType.value, type: filterType.value === 'all' ? undefined : filterType.value,
agent_id: filterAgentId.value || undefined agent_id: filterAgentId.value || undefined
}) })
tasks.value = res.data tasks.value = res.list
total.value = res.total total.value = res.total
} catch { toast.error('加载任务失败') } } catch { toast.error('加载任务失败') }
} }
@@ -287,10 +287,12 @@ watch(() => route.query.agent_id, (newVal) => {
<span class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 max-sm:order-3">名称</span> <span class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 max-sm:order-3">名称</span>
<span class="w-24 sm:w-32 shrink-0 hidden md:block">执行位置</span> <span class="w-24 sm:w-32 shrink-0 hidden md:block">执行位置</span>
<span class="w-8 shrink-0 text-center max-sm:order-4 max-sm:ml-auto">状态</span> <span class="w-8 shrink-0 text-center max-sm:order-4 max-sm:ml-auto">状态</span>
<div class="w-full hidden max-sm:block max-sm:order-5 mt-1 border-t border-muted/10 opacity-50"></div> <div class="w-full hidden max-sm:block max-sm:order-5 mt-1 border-t border-muted/10 opacity-50"></div>
<span class="flex-1 min-w-[120px] max-sm:order-6 block sm:block max-sm:mt-1 flex items-center gap-1.5"><Terminal class="h-3.5 w-3.5 sm:hidden opacity-50"/>命令/地址</span> <span class="flex-1 min-w-[120px] max-sm:order-6 block sm:block max-sm:mt-1 flex items-center gap-1.5">
<Terminal class="h-3.5 w-3.5 sm:hidden opacity-50" />命令/地址
</span>
<span class="w-28 shrink-0 hidden md:block">定时规则</span> <span class="w-28 shrink-0 hidden md:block">定时规则</span>
<span class="w-40 shrink-0 hidden lg:block">执行时间</span> <span class="w-40 shrink-0 hidden lg:block">执行时间</span>
<span class="w-28 sm:w-32 shrink-0 text-right sm:text-center max-sm:order-7 max-sm:mt-1">操作</span> <span class="w-28 sm:w-32 shrink-0 text-right sm:text-center max-sm:order-7 max-sm:mt-1">操作</span>
@@ -302,7 +304,8 @@ watch(() => route.query.agent_id, (newVal) => {
</div> </div>
<div v-for="(task, index) in tasks" :key="task.id" <div v-for="(task, index) in tasks" :key="task.id"
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2.5 sm:py-1.5 hover:bg-muted/30 transition-colors"> class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2.5 sm:py-1.5 hover:bg-muted/30 transition-colors">
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm max-sm:order-1">#{{ total - (currentPage - 1) * pageSize - index }}</span> <span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm max-sm:order-1">#{{ total -
(currentPage - 1) * pageSize - index }}</span>
<span class="w-8 shrink-0 flex justify-center max-sm:order-2" :title="getTaskTypeTitle(task.type || 'task')"> <span class="w-8 shrink-0 flex justify-center max-sm:order-2" :title="getTaskTypeTitle(task.type || 'task')">
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" /> <GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
<Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" /> <Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
@@ -359,11 +362,10 @@ watch(() => route.query.agent_id, (newVal) => {
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" /> <ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
</div> </div>
</span> </span>
<div class="w-full hidden max-sm:block max-sm:order-5 -my-0.5"></div> <div class="w-full hidden max-sm:block max-sm:order-5 -my-0.5"></div>
<span <span class="w-auto sm:w-32 shrink-0 flex justify-end sm:justify-center gap-1 max-sm:order-7 max-sm:mt-1">
class="w-auto sm:w-32 shrink-0 flex justify-end sm:justify-center gap-1 max-sm:order-7 max-sm:mt-1">
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="runTask(task.id)" title="执行" <Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="runTask(task.id)" title="执行"
:disabled="executingTaskId === task.id"> :disabled="executingTaskId === task.id">
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 sm:h-3.5 sm:w-3.5 animate-spin" /> <Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 sm:h-3.5 sm:w-3.5 animate-spin" />