style(ui): introduce StatusBadge and use it in LogDetailCard

This commit is contained in:
duorameng
2026-06-29 22:43:50 +08:00
parent b52af6c4fd
commit 837a53ecd3
2 changed files with 84 additions and 51 deletions
+5 -51
View File
@@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted, watch } from 'vue'
import { import {
X, Trash2, Maximize2, CheckCircle2, XCircle, AlertCircle, Clock, Ban, X, Trash2, Maximize2, Ban, Search
Zap as ZapIcon, Search, MinusCircle
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import LogContent from './LogContent.vue' import LogContent from './LogContent.vue'
import { TASK_STATUS, TASK_STATUS_TEXT } from '@/constants' import StatusBadge from './StatusBadge.vue'
import { TASK_STATUS } from '@/constants'
import type { TaskLog } from '@/api' import type { TaskLog } from '@/api'
interface Props { interface Props {
@@ -92,27 +91,6 @@ watch(() => props.log?.duration, (newVal) => {
currentDuration.value = newVal || 0 currentDuration.value = newVal || 0
} }
}) })
function getStatusBadgeClass(status: string) {
switch (status) {
case TASK_STATUS.SUCCESS:
return 'bg-emerald-500/5 text-emerald-500 border-emerald-500/15 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20'
case TASK_STATUS.FAILED:
return 'bg-rose-500/5 text-rose-500 border-rose-500/15 dark:bg-rose-500/10 dark:text-rose-400 dark:border-rose-500/20'
case TASK_STATUS.RUNNING:
return 'bg-sky-500/5 text-sky-500 border-sky-500/15 dark:bg-sky-500/10 dark:text-sky-400 dark:border-sky-500/20'
case TASK_STATUS.PENDING:
return 'bg-amber-500/5 text-amber-500 border-amber-500/15 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20'
case TASK_STATUS.TIMEOUT:
return 'bg-orange-500/5 text-orange-500 border-orange-500/15 dark:bg-orange-500/10 dark:text-orange-400 dark:border-orange-500/20'
case TASK_STATUS.CANCELLED:
return 'bg-zinc-500/5 text-zinc-400 border-zinc-500/15 dark:bg-zinc-500/10 dark:text-zinc-400 dark:border-zinc-500/20'
case 'UNEXECUTED':
return 'bg-zinc-500/5 text-zinc-400 border-zinc-500/15 border-dashed dark:bg-zinc-500/10 dark:text-zinc-400 dark:border-zinc-500/20'
default:
return 'bg-secondary text-secondary-foreground border-transparent'
}
}
</script> </script>
<template> <template>
@@ -126,12 +104,7 @@ function getStatusBadgeClass(status: string) {
<span class="text-xs font-medium text-foreground whitespace-nowrap truncate">{{ title }}</span> <span class="text-xs font-medium text-foreground whitespace-nowrap truncate">{{ title }}</span>
<!-- Simple 模式下的状态显示 --> <!-- Simple 模式下的状态显示 -->
<Badge v-if="variant === 'simple'" variant="outline" :class="[ <StatusBadge v-if="variant === 'simple'" :status="log.status" />
'px-2 py-0.5 font-normal rounded-full border text-[10px] flex',
getStatusBadgeClass(log.status)
]">
{{ TASK_STATUS_TEXT[log.status] || log.status }}
</Badge>
<!-- 详情模式下的停止按钮 (始终在第一行进入) --> <!-- 详情模式下的停止按钮 (始终在第一行进入) -->
<Button v-if="log.status === TASK_STATUS.RUNNING && variant === 'full'" variant="destructive" size="sm" <Button v-if="log.status === TASK_STATUS.RUNNING && variant === 'full'" variant="destructive" size="sm"
@@ -201,26 +174,7 @@ function getStatusBadgeClass(status: string) {
</div> </div>
<div class="flex justify-between items-center h-8"> <div class="flex justify-between items-center h-8">
<span class="text-sm font-normal text-muted-foreground">状态</span> <span class="text-sm font-normal text-muted-foreground">状态</span>
<Badge variant="outline" :class="[ <StatusBadge :status="log.status" />
'capitalize px-2.5 py-0.5 font-normal rounded-full border transition-all duration-300',
getStatusBadgeClass(log.status)
]">
<div class="flex items-center gap-1">
<CheckCircle2 v-if="log.status === TASK_STATUS.SUCCESS" class="h-3 w-3 fill-emerald-500/10" />
<XCircle v-else-if="log.status === TASK_STATUS.FAILED" class="h-3 w-3 fill-rose-500/10" />
<span v-else-if="log.status === TASK_STATUS.RUNNING" class="relative flex h-1.5 w-1.5 mr-0.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-sky-500"></span>
</span>
<Clock v-else-if="log.status === TASK_STATUS.PENDING" class="h-3 w-3 fill-amber-500/10" />
<AlertCircle v-else-if="log.status === TASK_STATUS.TIMEOUT" class="h-3 w-3 fill-orange-500/10" />
<Ban v-else-if="log.status === TASK_STATUS.CANCELLED" class="h-3 w-3" />
<MinusCircle v-else-if="log.status === 'UNEXECUTED'" class="h-3 w-3 opacity-40" />
<span class="text-[10px] font-normal tracking-wide">
{{ TASK_STATUS_TEXT[log.status] || log.status }}
</span>
</div>
</Badge>
</div> </div>
<div class="flex justify-between items-center h-6"> <div class="flex justify-between items-center h-6">
<span class="text-sm font-normal text-muted-foreground">耗时</span> <span class="text-sm font-normal text-muted-foreground">耗时</span>
+79
View File
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TASK_STATUS, TASK_STATUS_TEXT } from '@/constants'
const props = withDefaults(
defineProps<{
status: string
showDot?: boolean
}>(),
{
showDot: true
}
)
const badgeClasses = computed(() => {
switch (props.status) {
case TASK_STATUS.SUCCESS:
return 'border-green-500/20 text-green-600 dark:text-green-400 bg-green-500/10 dark:bg-green-500/20'
case TASK_STATUS.FAILED:
return 'border-red-500/20 text-red-600 dark:text-red-400 bg-red-500/10 dark:bg-red-500/20'
case TASK_STATUS.RUNNING:
return 'border-amber-500/20 text-amber-600 dark:text-amber-400 bg-amber-500/10 dark:bg-amber-500/20'
case TASK_STATUS.PENDING:
case 'queued':
return 'border-blue-400/20 text-blue-500 dark:text-blue-400 bg-blue-400/10 dark:bg-blue-400/20'
case TASK_STATUS.TIMEOUT:
return 'border-orange-500/20 text-orange-600 dark:text-orange-400 bg-orange-500/10 dark:bg-orange-500/20'
case TASK_STATUS.CANCELLED:
return 'border-muted-foreground/10 text-muted-foreground bg-muted/50'
default:
return 'border-transparent text-secondary-foreground bg-secondary'
}
})
const dotClasses = computed(() => {
switch (props.status) {
case TASK_STATUS.SUCCESS:
return 'bg-green-500'
case TASK_STATUS.FAILED:
return 'bg-red-500'
case TASK_STATUS.RUNNING:
return 'bg-amber-500'
case TASK_STATUS.PENDING:
case 'queued':
return 'bg-blue-400'
case TASK_STATUS.TIMEOUT:
return 'bg-orange-500'
case TASK_STATUS.CANCELLED:
return 'bg-muted-foreground'
default:
return 'bg-muted-foreground'
}
})
const statusText = computed(() => {
return TASK_STATUS_TEXT[props.status as keyof typeof TASK_STATUS_TEXT] || props.status
})
</script>
<template>
<div
class="inline-flex items-center gap-1.5 px-2.5 py-0.5 text-[10px] font-normal rounded-full border transition-all duration-300 select-none"
:class="badgeClasses"
>
<!-- 正在运行状态使用双层波纹微动效的呼吸灯 -->
<span v-if="showDot && status === TASK_STATUS.RUNNING" class="relative flex h-1.5 w-1.5 shrink-0">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-amber-500"></span>
</span>
<!-- 其他状态使用常规实心圆点 -->
<span
v-else-if="showDot"
class="h-1.5 w-1.5 rounded-full shrink-0"
:class="dotClasses"
></span>
<span class="tracking-wide text-[10px]">{{ statusText }}</span>
</div>
</template>