chore: opt mobile style
This commit is contained in:
@@ -275,7 +275,9 @@ export const api = {
|
||||
listTokens: () => request<AgentToken[]>('/agents/tokens'),
|
||||
createToken: (data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||||
request<AgentToken>('/agents/tokens', { method: 'POST', body: JSON.stringify(data) }),
|
||||
deleteToken: (id: string) => request('/agents/tokens/' + id, { method: 'DELETE' })
|
||||
deleteToken: (id: string) => request('/agents/tokens/' + id, { method: 'DELETE' }),
|
||||
updateToken: (id: string, data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||||
request<AgentToken>('/agents/tokens/' + id, { method: 'PUT', body: JSON.stringify(data) })
|
||||
},
|
||||
mise: {
|
||||
list: () => request<MiseLanguage[]>('/mise/ls'),
|
||||
|
||||
+104
-25
@@ -29,9 +29,12 @@ const showEditDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showDownloadDialog = ref(false)
|
||||
const showTokenDialog = ref(false)
|
||||
const showEditTokenDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const formData = ref({ name: '', description: '' })
|
||||
const tokenForm = ref({ remark: '', max_uses: 0, expires_at: '' })
|
||||
const editingToken = ref<AgentToken | null>(null)
|
||||
const editTokenForm = ref({ remark: '', max_uses: 0, expires_at: '' })
|
||||
const editingAgent = ref<Agent | null>(null)
|
||||
const deletingAgent = ref<Agent | null>(null)
|
||||
const viewingAgent = ref<Agent | null>(null)
|
||||
@@ -71,11 +74,13 @@ async function loadAgents() {
|
||||
}
|
||||
|
||||
function viewDetail(agent: Agent) {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
viewingAgent.value = agent
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(agent: Agent) {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
editingAgent.value = agent
|
||||
formData.value = { name: agent.name, description: agent.description }
|
||||
showEditDialog.value = true
|
||||
@@ -105,10 +110,21 @@ async function toggleEnabled(agent: Agent) {
|
||||
}
|
||||
|
||||
function confirmDelete(agent: Agent) {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
deletingAgent.value = agent
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
function openDownloadDialog() {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
showDownloadDialog.value = true
|
||||
}
|
||||
|
||||
function openTokenDialog() {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
showTokenDialog.value = true
|
||||
}
|
||||
|
||||
async function deleteAgent() {
|
||||
if (!deletingAgent.value) return
|
||||
try {
|
||||
@@ -170,6 +186,35 @@ async function deleteToken(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function openEditToken(token: AgentToken) {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
editingToken.value = token
|
||||
// expires_at 后端格式 "2006-01-02 15:04:05",转回 datetime-local 格式
|
||||
const rawExpires = token.expires_at?.replace(' ', 'T').slice(0, 16) || ''
|
||||
editTokenForm.value = { remark: token.remark || '', max_uses: token.max_uses, expires_at: rawExpires }
|
||||
showEditTokenDialog.value = true
|
||||
}
|
||||
|
||||
async function updateToken() {
|
||||
if (!editingToken.value) return
|
||||
try {
|
||||
let expiresAt = editTokenForm.value.expires_at
|
||||
if (expiresAt) {
|
||||
expiresAt = expiresAt.replace('T', ' ') + ':00'
|
||||
}
|
||||
await api.agents.updateToken(editingToken.value.id, {
|
||||
remark: editTokenForm.value.remark,
|
||||
max_uses: editTokenForm.value.max_uses,
|
||||
expires_at: expiresAt || undefined
|
||||
})
|
||||
showEditTokenDialog.value = false
|
||||
await loadAgents()
|
||||
toast.success('更新成功')
|
||||
} catch (e: unknown) {
|
||||
toast.error((e as Error).message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
function isTokenExpired(token: AgentToken) {
|
||||
if (!token.expires_at) return false
|
||||
// 将 "YYYY-MM-DD HH:mm:ss" 转换为 ISO 格式 "YYYY-MM-DDTHH:mm:ss" 以提高浏览器兼容性
|
||||
@@ -214,7 +259,7 @@ onUnmounted(() => {
|
||||
<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>
|
||||
<Button variant="outline" size="sm" class="h-9 shrink-0" @click="showDownloadDialog = true">
|
||||
<Button variant="outline" size="sm" class="h-9 shrink-0" @click="openDownloadDialog">
|
||||
<Download class="h-4 w-4 sm:mr-1.5" /> <span class="hidden sm:inline">下载</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadAgents" :disabled="loading">
|
||||
@@ -224,7 +269,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<Tabs v-model="activeTab">
|
||||
<TabsList>
|
||||
<TabsList class="w-full grid grid-cols-2">
|
||||
<TabsTrigger value="agents">Agent 列表</TabsTrigger>
|
||||
<TabsTrigger value="regcodes">
|
||||
<Ticket class="h-4 w-4 mr-1" />令牌
|
||||
@@ -380,28 +425,29 @@ onUnmounted(() => {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="regcodes" class="mt-4">
|
||||
<div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar">
|
||||
<div class="min-w-full w-max">
|
||||
<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">
|
||||
<span class="w-6 shrink-0"></span>
|
||||
<span class="flex-1 min-w-[200px]">令牌</span>
|
||||
<span class="w-24 sm:w-32 shrink-0">备注</span>
|
||||
<span class="w-16 sm:w-20 shrink-0 text-center">使用次数</span>
|
||||
<span class="w-28 sm:w-36 shrink-0 hidden sm:block">过期时间</span>
|
||||
<span class="w-20 shrink-0 flex justify-center">
|
||||
<Button size="sm" class="h-7" @click="showTokenDialog = true">
|
||||
<Plus class="h-3.5 w-3.5 mr-1" />生成
|
||||
<div class="rounded-lg border bg-card">
|
||||
<!-- 表头 -->
|
||||
<div class="flex items-center gap-2 px-3 py-2 border-b bg-muted/50 text-xs text-muted-foreground font-medium">
|
||||
<span class="w-5 shrink-0"></span>
|
||||
<span class="flex-1 min-w-0">令牌</span>
|
||||
<span class="w-20 shrink-0 hidden sm:block">备注</span>
|
||||
<span class="w-14 shrink-0 text-center hidden sm:block">次数</span>
|
||||
<span class="w-28 shrink-0 hidden md:block">过期时间</span>
|
||||
<span class="w-16 shrink-0 flex justify-end">
|
||||
<Button size="sm" class="h-7 px-2" @click="openTokenDialog">
|
||||
<Plus class="h-3.5 w-3.5 sm:mr-1" /><span class="hidden sm:inline">生成</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 数据行 -->
|
||||
<div class="divide-y">
|
||||
<div v-if="tokens.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
<Ticket class="h-8 w-8 mx-auto mb-2 opacity-50" />暂无令牌
|
||||
</div>
|
||||
<div v-for="token in tokens" :key="token.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-6 shrink-0 flex justify-center">
|
||||
class="flex items-center gap-2 px-3 py-2 hover:bg-muted/50 transition-colors">
|
||||
<!-- 状态 -->
|
||||
<span class="w-5 shrink-0 flex justify-center">
|
||||
<div v-if="!isTokenExpired(token) && !isTokenExhausted(token)"
|
||||
class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<Check class="h-3 w-3 text-green-500 stroke-[3]" />
|
||||
@@ -410,28 +456,33 @@ onUnmounted(() => {
|
||||
<X class="h-3 w-3 text-red-500 stroke-[3]" />
|
||||
</div>
|
||||
</span>
|
||||
<!-- Token(截断省略号,撑满剩余空间) -->
|
||||
<code
|
||||
class="flex-1 min-w-[200px] font-mono text-xs bg-muted px-2 py-0.5 rounded truncate">{{ token.token }}</code>
|
||||
<span class="w-24 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate">{{ token.remark ||
|
||||
'-' }}</span>
|
||||
<span class="w-16 sm:w-20 shrink-0 text-xs sm:text-sm text-muted-foreground text-center">
|
||||
class="flex-1 min-w-0 font-mono text-xs bg-muted px-2 py-0.5 rounded truncate">{{ token.token }}</code>
|
||||
<!-- 备注(小屏隐藏) -->
|
||||
<span class="w-20 shrink-0 text-xs text-muted-foreground truncate hidden sm:block">{{ token.remark || '-' }}</span>
|
||||
<!-- 使用次数(小屏隐藏) -->
|
||||
<span class="w-14 shrink-0 text-xs text-muted-foreground text-center hidden sm:block">
|
||||
{{ token.used_count }}/{{ token.max_uses === 0 ? '∞' : token.max_uses }}
|
||||
</span>
|
||||
<span class="w-28 sm:w-36 shrink-0 text-xs sm:text-sm text-muted-foreground hidden sm:block truncate">
|
||||
<!-- 过期时间(md 以上才显示) -->
|
||||
<span class="w-28 shrink-0 text-xs text-muted-foreground truncate hidden md:block">
|
||||
{{ token.expires_at || '永不过期' }}
|
||||
</span>
|
||||
<span class="w-20 shrink-0 flex justify-center gap-1">
|
||||
<!-- 操作 -->
|
||||
<span class="w-24 shrink-0 flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="copyToken(token.token)" title="复制">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteToken(token.id)"
|
||||
title="删除">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEditToken(token)" title="编辑">
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteToken(token.id)" title="删除">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -629,5 +680,33 @@ onUnmounted(() => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 编辑令牌对话框 -->
|
||||
<Dialog v-model:open="showEditTokenDialog">
|
||||
<DialogContent @openAutoFocus.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑令牌</DialogTitle>
|
||||
<DialogDescription class="sr-only">修改令牌的备注、使用次数和过期时间</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label>备注</Label>
|
||||
<Input v-model="editTokenForm.remark" placeholder="备注信息(可选)" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>最大使用次数</Label>
|
||||
<Input v-model.number="editTokenForm.max_uses" type="number" placeholder="0 表示无限制" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>过期时间</Label>
|
||||
<Input v-model="editTokenForm.expires_at" type="datetime-local" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showEditTokenDialog = false">取消</Button>
|
||||
<Button @click="updateToken">保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -287,31 +287,87 @@ onBeforeUnmount(() => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-lg border bg-card overflow-x-auto mt-4">
|
||||
<!-- 表头 -->
|
||||
<div v-else class="rounded-lg border bg-card mt-4 overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<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-[500px]">
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||
<span class="w-32 sm:w-48 shrink-0">名称</span>
|
||||
<span class="w-24 sm:flex-1 shrink-0 sm:shrink">值</span>
|
||||
<span class="flex-1 min-w-0">值</span>
|
||||
<span class="w-32 sm:w-48 shrink-0 hidden md:block">备注说明</span>
|
||||
<span class="w-28 sm:w-36 shrink-0 text-center">操作</span>
|
||||
<span class="w-32 shrink-0 text-center">操作</span>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y min-w-[500px]">
|
||||
|
||||
<div class="divide-y">
|
||||
<div v-if="envVars.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||
{{ activeTab === ENV_TYPE.SECRET ? '暂无机密' : '暂无环境变量' }}
|
||||
</div>
|
||||
<div v-for="env in envVars" :key="env.id"
|
||||
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/30 transition-colors">
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(env, index) in envVars" :key="`mobile-${env.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
||||
<span class="text-xs text-muted-foreground shrink-0 tabular-nums">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<code class="font-bold text-xs bg-muted/60 px-2 py-0.5 rounded break-all truncate">{{ env.name }}</code>
|
||||
</div>
|
||||
<span class="cursor-pointer group shrink-0"
|
||||
@click="toggleEnabled(env)" :title="env.enabled ? '已启用' : '已禁用'">
|
||||
<div v-if="env.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 详情信息列表 -->
|
||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-3 px-1">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="w-8 shrink-0 font-medium mt-0.5 opacity-70">内容:</span>
|
||||
<div class="flex-1 min-w-0 text-foreground break-all leading-relaxed">
|
||||
<TextOverflow :text="showValues[env.id] ? env.value : maskValue(env.value)" title="查看值" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="env.remark" class="flex items-start gap-3">
|
||||
<span class="w-8 shrink-0 font-medium mt-0.5 opacity-70">备注:</span>
|
||||
<span class="flex-1 text-[11px] leading-relaxed">{{ env.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-1 pt-2 border-t">
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="toggleShow(env.id)">
|
||||
<Eye v-if="!showValues[env.id]" class="h-3 w-3 mr-1" />
|
||||
<EyeOff v-else class="h-3 w-3 mr-1" />
|
||||
{{ showValues[env.id] ? '隐藏' : '显示' }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="openEdit(env)">
|
||||
<Pencil class="h-3 w-3 mr-1" />编辑
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="confirmDelete(env.id)">
|
||||
<Trash2 class="h-3 w-3 mr-1" />删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(env, index) in envVars" :key="`desktop-${env.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/30 transition-colors">
|
||||
<div class="w-12 shrink-0 pl-1">
|
||||
<span class="text-muted-foreground text-xs tabular-nums">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
</div>
|
||||
<code
|
||||
class="w-32 sm:w-48 font-medium truncate shrink-0 text-xs bg-muted/40 px-2 py-1 rounded">{{ env.name }}</code>
|
||||
<span class="w-24 sm:flex-1 shrink-0 sm:shrink font-mono text-muted-foreground truncate text-xs">
|
||||
<span class="flex-1 min-w-0 text-muted-foreground truncate text-xs px-1">
|
||||
<TextOverflow :text="showValues[env.id] ? env.value : maskValue(env.value)" title="查看值" />
|
||||
</span>
|
||||
<span class="w-32 sm:w-48 shrink-0 text-muted-foreground truncate text-sm hidden md:block">
|
||||
<TextOverflow :text="env.remark || '-'" title="备注描述" />
|
||||
</span>
|
||||
<span class="w-28 sm:w-36 shrink-0 flex justify-center gap-1">
|
||||
<div class="w-32 shrink-0 flex justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 rounded-md transition-all"
|
||||
:class="env.enabled ? 'text-green-500 bg-green-500/10 hover:bg-green-500/20' : 'text-muted-foreground bg-muted/50 hover:bg-muted'"
|
||||
@click="toggleEnabled(env)" :title="env.enabled ? '已启用(点击禁用)' : '已禁用(点击启用)'">
|
||||
@@ -330,7 +386,7 @@ onBeforeUnmount(() => {
|
||||
title="删除">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
|
||||
@@ -335,7 +335,7 @@ onMounted(() => {
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">语言依赖</h2>
|
||||
<p class="text-muted-foreground text-sm mt-0.5">管理系统环境中的编程语言运行时及相关包依赖 (Mise)</p>
|
||||
<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">
|
||||
|
||||
@@ -124,37 +124,69 @@ onMounted(loadLogs)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
||||
<!-- 表头 -->
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<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-20 sm:w-32 shrink-0">IP 地址</span>
|
||||
<span class="w-10 sm:w-16 shrink-0 text-center">状态</span>
|
||||
<span class="hidden sm:flex sm:flex-1">User Agent</span>
|
||||
<span class="shrink-0 sm:w-40 sm:text-right">时间</span>
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-24 sm:w-32 shrink-0">用户名</span>
|
||||
<span class="w-32 sm:w-40 shrink-0">IP 地址</span>
|
||||
<span class="w-16 shrink-0 text-center">状态</span>
|
||||
<span class="flex-1 min-w-0">User Agent</span>
|
||||
<span class="w-40 shrink-0 text-right">登录时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y sm:min-w-[500px]">
|
||||
<div class="divide-y">
|
||||
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无登录日志
|
||||
</div>
|
||||
<div v-for="log in logs" :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>
|
||||
<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"
|
||||
@click="showIpInfo(log.ip)">{{ log.ip }}</code>
|
||||
<span class="w-10 sm:w-16 shrink-0 flex justify-center">
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 mr-2">
|
||||
<span class="text-xs text-muted-foreground shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<span class="font-bold text-sm truncate">{{ log.username }}</span>
|
||||
</div>
|
||||
<span
|
||||
:class="['h-2 w-2 mt-1.5 rounded-full shrink-0', log.status === 'success' ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]']"></span>
|
||||
</div>
|
||||
|
||||
<!-- 详情信息列表 -->
|
||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-1 px-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-8 shrink-0 font-medium opacity-70">IP:</span>
|
||||
<span
|
||||
class="text-foreground bg-muted/40 px-1.5 py-0.5 rounded cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
@click="showIpInfo(log.ip)">{{ log.ip }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-8 shrink-0 font-medium opacity-70">时间:</span>
|
||||
<span class="text-muted-foreground">{{ log.created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm pl-1">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<span class="w-24 sm:w-32 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
|
||||
<div class="w-32 sm:w-40 shrink-0">
|
||||
<code
|
||||
class="text-xs text-muted-foreground bg-muted px-2 py-1 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors inline-block"
|
||||
@click="showIpInfo(log.ip)">{{ log.ip }}</code>
|
||||
</div>
|
||||
<span class="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>
|
||||
<span class="hidden sm:flex sm:flex-1 text-xs text-muted-foreground truncate">
|
||||
<span class="flex-1 min-w-0 text-xs text-muted-foreground truncate">
|
||||
<TextOverflow :text="log.user_agent || '-'" title="User Agent" />
|
||||
</span>
|
||||
<span class="shrink-0 sm:w-40 sm:text-right text-xs text-muted-foreground">{{ log.created_at
|
||||
}}</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
|
||||
@@ -127,11 +127,11 @@ const selectedLog = computed(() => logs.value.find((l: AppLog) => l.id === selec
|
||||
function getLevelBadgeClass(level: string) {
|
||||
switch (level) {
|
||||
case LOG_LEVEL.INFO:
|
||||
return 'bg-blue-500/10 text-blue-700 border-blue-200/50'
|
||||
return 'bg-blue-500/15 text-blue-500 border-blue-500/30'
|
||||
case LOG_LEVEL.WARNING:
|
||||
return 'bg-yellow-500/10 text-yellow-700 border-yellow-200/50'
|
||||
return 'bg-amber-500/15 text-amber-500 border-amber-500/30'
|
||||
case LOG_LEVEL.ERROR:
|
||||
return 'bg-red-500/10 text-red-700 border-red-200/50'
|
||||
return 'bg-red-500/15 text-red-500 border-red-500/30'
|
||||
default:
|
||||
return 'bg-secondary text-secondary-foreground border-transparent'
|
||||
}
|
||||
@@ -217,34 +217,63 @@ function onDialogClose(open: boolean) {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<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-[700px]">
|
||||
<span class="w-12 sm:w-16 shrink-0">级别</span>
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-12 sm:w-16 shrink-0 text-center">级别</span>
|
||||
<span class="w-40 sm:w-56 shrink-0">事件标题</span>
|
||||
<span class="hidden sm:flex sm:flex-1">详情内容</span>
|
||||
<span class="shrink-0 w-24 sm:w-40 sm:text-right">发生时间</span>
|
||||
<span class="flex-1 min-w-0">详情内容</span>
|
||||
<span class="w-40 shrink-0 text-right">发生时间</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y sm:min-w-[700px]">
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y">
|
||||
<div v-if="logs.length === 0 && !loading" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无系统事件
|
||||
</div>
|
||||
<div v-for="log in logs" :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 cursor-pointer group"
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 mr-2">
|
||||
<span class="text-xs text-muted-foreground shrink-0 tabular-nums">#{{ total - (filters.page - 1) * pageSize - index }}</span>
|
||||
<component :is="getLevelIcon(log.level)" :class="['h-4 w-4 shrink-0',
|
||||
log.level === LOG_LEVEL.INFO ? 'text-blue-500' :
|
||||
log.level === LOG_LEVEL.WARNING ? 'text-yellow-500' : 'text-red-500']" />
|
||||
<span class="font-medium text-sm truncate" :title="log.title">{{ log.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-muted/30 rounded px-2 py-1.5 mb-2">
|
||||
<div class="text-muted-foreground text-xs truncate">
|
||||
{{ log.content || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[10px] text-muted-foreground text-right tabular-nums">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm tabular-nums pl-1">#{{ total - (filters.page - 1) * pageSize - index }}</span>
|
||||
<span class="w-12 sm:w-16 shrink-0 flex justify-center">
|
||||
<component :is="getLevelIcon(log.level)" :class="['h-4 w-4',
|
||||
log.level === LOG_LEVEL.INFO ? 'text-blue-500' :
|
||||
log.level === LOG_LEVEL.WARNING ? 'text-yellow-500' : 'text-red-500']" />
|
||||
</span>
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">{{
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-sm truncate" :title="log.title">{{
|
||||
log.title }}</span>
|
||||
<span class="hidden sm:flex sm:flex-1 text-xs sm:text-sm text-muted-foreground truncate"
|
||||
<span class="flex-1 min-w-0 text-sm text-muted-foreground truncate"
|
||||
:title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="shrink-0 w-24 sm:w-40 sm:text-right text-xs text-muted-foreground">
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -259,10 +288,10 @@ function onDialogClose(open: boolean) {
|
||||
<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-full border shadow-sm',
|
||||
'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.5 uppercase tracking-wider">
|
||||
<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>
|
||||
@@ -273,11 +302,11 @@ function onDialogClose(open: boolean) {
|
||||
<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">标题</span>
|
||||
<span class="font-medium text-foreground">{{ detailDialogProps.title }}</span>
|
||||
<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">发生时间</span>
|
||||
<span class="text-muted-foreground font-medium">发生时间</span>
|
||||
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ?
|
||||
formatDate(selectedLog.created_at) : '-' }}</span>
|
||||
</div>
|
||||
@@ -285,7 +314,7 @@ function onDialogClose(open: boolean) {
|
||||
|
||||
<div class="flex flex-col min-h-0 bg-muted/5">
|
||||
<div
|
||||
class="px-6 py-2.5 text-xs font-semibold text-muted-foreground border-b bg-muted/10 uppercase tracking-wider">
|
||||
class="px-6 py-2.5 text-[10px] font-bold text-muted-foreground border-b bg-muted/10 uppercase tracking-widest">
|
||||
内容详情
|
||||
</div>
|
||||
<div class="p-6">
|
||||
@@ -298,7 +327,7 @@ function onDialogClose(open: boolean) {
|
||||
|
||||
<template v-if="detailDialogProps.error">
|
||||
<div
|
||||
class="px-6 py-2.5 text-xs font-semibold uppercase tracking-wider border-y bg-muted/10 text-muted-foreground border-border/60">
|
||||
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">
|
||||
|
||||
@@ -129,9 +129,9 @@ const selectedLog = computed(() => logs.value.find((l: AppLog) => l.id === selec
|
||||
function getStatusBadgeClass(status: string) {
|
||||
switch (status) {
|
||||
case LOG_STATUS.SUCCESS:
|
||||
return 'bg-green-500/10 text-green-700 border-green-200/50 dark:bg-green-500/20 dark:text-green-400 dark:border-green-900/50'
|
||||
return 'bg-green-500/15 text-green-500 border-green-500/30'
|
||||
case LOG_STATUS.FAILED:
|
||||
return 'bg-red-500/10 text-red-700 border-red-200/50 dark:bg-red-500/20 dark:text-red-400 dark:border-red-900/50'
|
||||
return 'bg-red-500/15 text-red-500 border-red-500/30'
|
||||
default:
|
||||
return 'bg-secondary text-secondary-foreground border-transparent'
|
||||
}
|
||||
@@ -212,38 +212,72 @@ function onDialogClose(open: boolean) {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
||||
<!-- 表头 -->
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<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-[700px]">
|
||||
<span class="w-12 sm:w-16 shrink-0">序号</span>
|
||||
<span class="w-40 sm:w-56 shrink-0">标题及渠道</span>
|
||||
<span class="hidden sm:flex sm:flex-1">内容详情</span>
|
||||
<span class="w-10 sm:w-16 shrink-0 text-center">状态</span>
|
||||
<span class="shrink-0 w-24 sm:w-40 sm:text-right">发送时间</span>
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-56 shrink-0">标题及渠道</span>
|
||||
<span class="flex-1 min-w-0">内容详情</span>
|
||||
<span class="w-16 shrink-0 text-center">状态</span>
|
||||
<span class="w-40 shrink-0 text-right">发送时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y sm:min-w-[700px]">
|
||||
<div class="divide-y">
|
||||
<div v-if="logs.length === 0 && !loading" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无推送记录
|
||||
</div>
|
||||
<div v-for="(log, index) in logs" :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 cursor-pointer group"
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors cursor-pointer group" @click="showDetail(log)"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']">
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 mr-2">
|
||||
<span class="text-xs text-muted-foreground shrink-0">#{{ getLogIndex(index) }}</span>
|
||||
<span class="font-bold text-sm truncate">{{ log.title }}</span>
|
||||
</div>
|
||||
<span
|
||||
:class="['h-2 w-2 mt-1.5 rounded-full shrink-0', log.status === LOG_STATUS.SUCCESS ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]']"></span>
|
||||
</div>
|
||||
|
||||
<!-- 详情信息列表 -->
|
||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-1 px-1">
|
||||
<div v-if="log.channel_name" class="flex items-center gap-3">
|
||||
<span class="w-8 shrink-0 font-medium opacity-70">渠道:</span>
|
||||
<span class="text-foreground bg-muted/40 px-1.5 py-0.5 rounded text-[10px]">{{ log.channel_name }}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="w-8 shrink-0 font-medium mt-0.5 opacity-70">内容:</span>
|
||||
<div class="flex-1 min-w-0 text-foreground break-all leading-relaxed line-clamp-2">
|
||||
{{ log.content || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-8 shrink-0 font-medium opacity-70">时间:</span>
|
||||
<span class="text-[10px] text-muted-foreground">{{ formatDate(log.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<span class="w-12 sm:w-16 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ getLogIndex(index) }}</span>
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm pl-1">#{{ getLogIndex(index) }}</span>
|
||||
<span class="w-56 shrink-0 font-medium text-sm truncate" :title="log.title">
|
||||
<span v-if="log.channel_name" class="mr-1 text-muted-foreground">[{{ log.channel_name }}]</span>{{ log.title
|
||||
}}
|
||||
</span>
|
||||
<span class="hidden sm:flex sm:flex-1 text-xs sm:text-sm text-muted-foreground truncate" :title="log.content">
|
||||
<span class="flex-1 min-w-0 text-sm text-muted-foreground truncate" :title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="w-10 sm:w-16 shrink-0 flex justify-center">
|
||||
<span class="w-16 shrink-0 flex justify-center">
|
||||
<span
|
||||
:class="['h-2 w-2 rounded-full', log.status === LOG_STATUS.SUCCESS ? 'bg-green-500' : 'bg-red-500']"></span>
|
||||
</span>
|
||||
<span class="shrink-0 w-24 sm:w-40 sm:text-right text-xs text-muted-foreground">
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -259,10 +293,10 @@ function onDialogClose(open: boolean) {
|
||||
<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-full border shadow-sm transition-all duration-300',
|
||||
'px-2 py-0.5 text-[10px] font-bold rounded-md border shadow-sm transition-all duration-300',
|
||||
selectedLog ? getStatusBadgeClass(selectedLog.status) : ''
|
||||
]">
|
||||
<div class="flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<div class="flex items-center gap-1 uppercase tracking-tighter">
|
||||
<Check v-if="selectedLog?.status === LOG_STATUS.SUCCESS" class="h-3 w-3" />
|
||||
<X v-else class="h-3 w-3" />
|
||||
<span>{{ selectedLog?.status === LOG_STATUS.SUCCESS ? 'Success' : 'Failed' }}</span>
|
||||
|
||||
+132
-64
@@ -400,56 +400,129 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
||||
<!-- 表头 -->
|
||||
<div class="rounded-lg border bg-card">
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
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 sm:py-1.5 border-b bg-muted/20 text-xs sm:text-sm text-muted-foreground font-medium min-w-0 sm:min-w-[1000px]">
|
||||
<div class="w-10 sm:w-12 shrink-0 flex items-center gap-2 max-sm:order-1 pl-1">
|
||||
<span class="text-xs sm:text-sm">序号</span>
|
||||
</div>
|
||||
<span class="w-8 shrink-0 text-center max-sm:order-2">类型</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-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>
|
||||
|
||||
<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" />命令/地址
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||
<span class="w-8 shrink-0 text-center">类型</span>
|
||||
<span class="w-40 md:w-48 lg:w-56 shrink-0">名称</span>
|
||||
<span class="w-32 shrink-0 hidden md:block">执行位置</span>
|
||||
<span class="w-8 shrink-0 text-center">状态</span>
|
||||
<span class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<Terminal class="h-3.5 w-3.5 opacity-50" />命令/地址
|
||||
</span>
|
||||
<span class="w-28 shrink-0 hidden md: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-36 shrink-0 text-center">操作</span>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y min-w-0 sm:min-w-[1000px]">
|
||||
|
||||
<div class="divide-y">
|
||||
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无任务
|
||||
</div>
|
||||
<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">
|
||||
<div class="w-10 sm:w-12 shrink-0 flex items-center gap-2 max-sm:order-1 pl-1">
|
||||
<span class="text-muted-foreground text-xs sm:text-sm">#{{ total -
|
||||
(currentPage - 1) * pageSize - index }}</span>
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(task, index) in tasks" :key="`mobile-${task.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||
<!-- 标题行 -->
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
||||
<span class="text-xs text-muted-foreground tabular-nums flex-shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<span class="shrink-0" :title="getTaskTypeTitle(task.type || 'task')">
|
||||
<GitBranch v-if="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" />
|
||||
</span>
|
||||
<span class="font-bold text-sm truncate" :title="task.name">{{ task.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="cursor-pointer group"
|
||||
@click="toggleTask(task, !task.enabled)"
|
||||
:title="task.enabled ? '点击禁用' : '点击启用'">
|
||||
<div v-if="task.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
|
||||
|
||||
<!-- 详情信息列表 -->
|
||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-3 px-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-10 shrink-0 font-medium opacity-70">定时:</span>
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP"
|
||||
class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded font-medium">服务启动时</span>
|
||||
<span v-else-if="task.schedule" class="text-xs text-foreground bg-muted/40 px-1.5 py-0.5 rounded">{{ task.schedule }}</span>
|
||||
<span v-else class="text-muted-foreground opacity-50">-</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">命令:</span>
|
||||
<div class="flex-1 min-w-0 overflow-hidden text-foreground">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="task.tags" class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">标签:</span>
|
||||
<div class="flex flex-wrap gap-1 flex-1">
|
||||
<span v-for="tag in task.tags.split(',').filter(Boolean)" :key="tag"
|
||||
class="text-[9px] leading-none px-1.5 py-0.5 bg-primary/10 text-primary/80 rounded-full border border-primary/20 font-semibold">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="task.remark" class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">备注:</span>
|
||||
<span class="flex-1 text-[11px] truncate">{{ task.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮行 -->
|
||||
<div class="flex items-center justify-end gap-0.5 pt-2 border-t border-border/40">
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="runTask(task.id)" title="执行" :disabled="executingTaskId === task.id">
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 animate-spin" />
|
||||
<Play v-else class="h-3 w-3" />执行
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="viewLogs(task.id)" title="日志">
|
||||
<ScrollText class="h-3 w-3" />日志
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="openEdit(task)" title="编辑">
|
||||
<Pencil class="h-3 w-3" />编辑
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="duplicateTask(task)" title="克隆">
|
||||
<Copy class="h-3 w-3" />克隆
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1 text-destructive" @click="confirmDelete(task.id)" title="删除">
|
||||
<Trash2 class="h-3 w-3" />删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(task, index) in tasks" :key="`desktop-${task.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-1.5 hover:bg-muted/30 transition-colors">
|
||||
<div class="w-12 shrink-0 pl-1">
|
||||
<span class="text-muted-foreground text-sm">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
</div>
|
||||
<span class="w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
|
||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
||||
<Terminal v-else class="h-4 w-4 text-primary" />
|
||||
</span>
|
||||
<div
|
||||
class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden max-sm:order-3">
|
||||
<span class="font-medium truncate text-xs sm:text-sm cursor-help block w-full" :title="task.name">{{
|
||||
task.name }}</span>
|
||||
<div class="w-40 md:w-48 lg:w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden">
|
||||
<span class="font-medium truncate text-sm cursor-help" :title="task.name">{{ task.name }}</span>
|
||||
<div v-if="task.tags" class="flex items-center gap-1 overflow-hidden" :title="task.tags">
|
||||
<span v-for="tag in task.tags.split(',').filter(Boolean).slice(0, 3)" :key="tag"
|
||||
class="truncate text-[10px] leading-none px-1 py-0.5 bg-secondary text-secondary-foreground rounded border">
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span v-if="task.tags.split(',').filter(Boolean).length > 3"
|
||||
class="text-[10px] text-muted-foreground">...</span>
|
||||
<span v-if="task.tags.split(',').filter(Boolean).length > 3" class="text-[10px] text-muted-foreground">...</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="w-24 sm:w-32 shrink-0 hidden md:flex items-center gap-1 text-xs" :title="getExecutorName(task)">
|
||||
<span class="w-32 shrink-0 hidden md:flex items-center gap-1 text-xs" :title="getExecutorName(task)">
|
||||
<Monitor v-if="!task.agent_id" class="h-3 w-3 text-muted-foreground" />
|
||||
<template v-else>
|
||||
<Wifi v-if="getExecutorStatus(task) === 'online'" class="h-3 w-3 text-green-500" />
|
||||
@@ -457,11 +530,20 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
</template>
|
||||
<span class="truncate">{{ getExecutorName(task) }}</span>
|
||||
</span>
|
||||
|
||||
<code
|
||||
class="flex-1 min-w-[120px] text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded block sm:block max-sm:order-6 overflow-hidden max-sm:mt-1">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate" />
|
||||
</code>
|
||||
<span class="w-8 flex justify-center shrink-0 cursor-pointer group"
|
||||
@click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
|
||||
<div v-if="task.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
<code class="flex-1 min-w-0 text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded overflow-hidden">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate" />
|
||||
</code>
|
||||
<div class="w-28 shrink-0 hidden md:flex flex-col items-start justify-center gap-1 overflow-hidden">
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP"
|
||||
class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-2 py-1 rounded-md whitespace-nowrap font-medium">服务启动时</span>
|
||||
@@ -477,38 +559,24 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
下: {{ task.next_run || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="w-8 flex justify-center shrink-0 cursor-pointer group max-sm:order-4 max-sm:ml-auto"
|
||||
@click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
|
||||
<div v-if="task.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="w-full hidden max-sm:block max-sm:order-5 -my-0.5"></div>
|
||||
|
||||
<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">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="runTask(task.id)" title="执行"
|
||||
<span class="w-36 shrink-0 flex justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="runTask(task.id)" title="执行"
|
||||
: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" />
|
||||
<Play v-else class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Play v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="viewLogs(task.id)" title="日志">
|
||||
<ScrollText class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewLogs(task.id)" title="日志">
|
||||
<ScrollText class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="openEdit(task)" title="编辑">
|
||||
<Pencil class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(task)" title="编辑">
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="duplicateTask(task)" title="克隆">
|
||||
<Copy class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="duplicateTask(task)" title="克隆">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7 text-destructive"
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive"
|
||||
@click="confirmDelete(task.id)" title="删除">
|
||||
<Trash2 class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user