Files
nl-blogs/client/src/components/admin/TemplateDetailDrawer.vue
2026-08-24 13:06:04 +08:00

291 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Teleport to="body">
<div v-if="open" class="tdd-overlay" @click.self="close">
<div class="tdd-drawer">
<div class="tdd-head">
<span class="tdd-title">模板详情</span>
<button class="tdd-close" title="关闭" @click="close">
<Icon name="x" :size="16" />
</button>
</div>
<div v-if="loading" class="tdd-loading">加载中</div>
<template v-else-if="detail">
<!-- Tabs -->
<div class="tdd-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tdd-tab"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<div class="tdd-body">
<!-- 概览 -->
<div v-if="activeTab === 'overview'" class="tdd-overview">
<div class="tdd-row"><span class="tdd-k">名称</span><span class="tdd-v">{{ detail.name }}</span></div>
<div class="tdd-row"><span class="tdd-k">Slug</span><span class="tdd-v font-mono">{{ detail.slug }}</span></div>
<div class="tdd-row"><span class="tdd-k">类型</span><span class="tdd-v">{{ detail.type === 'builtin' ? '内置' : '自定义' }}</span></div>
<div class="tdd-row"><span class="tdd-k">状态</span>
<span class="tdd-v">
{{ detail.isActive ? '已启用' : '已停用' }}
<span v-if="detail.isCurrent" class="tdd-current">当前使用中</span>
</span>
</div>
<div class="tdd-row"><span class="tdd-k">描述</span><span class="tdd-v">{{ detail.description || '—' }}</span></div>
<div v-if="detail.skinCssPath" class="tdd-row"><span class="tdd-k">皮肤 CSS</span><span class="tdd-v font-mono" style="font-size: 0.75rem;">{{ detail.skinCssPath }}</span></div>
<div v-if="detail.assetsBase" class="tdd-row"><span class="tdd-k">资源目录</span><span class="tdd-v font-mono" style="font-size: 0.75rem;">{{ detail.assetsBase }}</span></div>
<div v-if="detail.createdAt" class="tdd-row"><span class="tdd-k">上传时间</span><span class="tdd-v">{{ formatTime(detail.createdAt) }}</span></div>
</div>
<!-- manifest / layout JSON -->
<div v-else class="tdd-json">
<div v-if="jsonText" class="tdd-json-wrap">
<button class="tdd-copy" @click="copyJson">复制</button>
<pre class="tdd-pre">{{ jsonText }}</pre>
</div>
<div v-else class="tdd-empty">由前端 layouts/ 直接实现 {{ activeTab }} 内容</div>
</div>
</div>
</template>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import Icon from '../Icon.vue'
import { getTemplateDetail, type TemplateDetail, type TemplateMeta } from '../../services/api'
const props = defineProps<{
open: boolean
template: TemplateMeta | null
}>()
const emit = defineEmits<{ (e: 'close'): void }>()
const loading = ref(false)
const detail = ref<TemplateDetail | null>(null)
const activeTab = ref<'overview' | 'manifest' | 'layout'>('overview')
const tabs = [
{ id: 'overview' as const, label: '概览' },
{ id: 'manifest' as const, label: 'manifest.json' },
{ id: 'layout' as const, label: 'layout.json' },
]
const jsonText = computed(() => {
const raw = activeTab.value === 'manifest' ? detail.value?.manifest : detail.value?.layout
if (!raw) return ''
try {
return JSON.stringify(raw, null, 2)
} catch {
return String(raw)
}
})
watch(() => props.open, (v) => {
if (!v || !props.template) return
activeTab.value = 'overview'
detail.value = null
loading.value = true
getTemplateDetail(props.template.id)
.then((d) => { detail.value = d })
.catch(() => { detail.value = null })
.finally(() => { loading.value = false })
})
function formatTime(ts: number): string {
const d = new Date(ts * 1000)
return d.toLocaleString()
}
function copyJson() {
const text = jsonText.value
if (!text) return
navigator.clipboard?.writeText(text).catch(() => { /* ignore */ })
}
function close() {
emit('close')
}
</script>
<style scoped>
.tdd-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 1000;
}
.tdd-drawer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: min(560px, 92vw);
background: #121214;
border-left: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
box-shadow: -20px 0 60px rgba(0, 0, 0, 0.5);
}
.tdd-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.9rem 1.2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tdd-title {
color: #fff;
font-weight: 600;
font-size: 1rem;
}
.tdd-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.15s;
}
.tdd-close:hover {
background: rgba(255, 80, 80, 0.2);
color: #ff8080;
}
.tdd-loading {
padding: 3rem;
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.tdd-tabs {
display: flex;
gap: 0.2rem;
padding: 0.6rem 1.2rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tdd-tab {
padding: 0.5rem 0.9rem;
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.55);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.15s;
}
.tdd-tab:hover {
color: #fff;
}
.tdd-tab.active {
color: rgb(var(--art-accent));
border-bottom-color: rgb(var(--art-accent));
}
.tdd-body {
flex: 1;
overflow-y: auto;
padding: 1.2rem;
}
.tdd-row {
display: flex;
gap: 0.8rem;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.85rem;
}
.tdd-k {
color: rgba(255, 255, 255, 0.45);
flex-shrink: 0;
width: 5.5rem;
}
.tdd-v {
color: rgba(255, 255, 255, 0.9);
word-break: break-all;
}
.tdd-current {
display: inline-block;
margin-left: 0.5rem;
background: rgb(var(--art-accent));
color: #000;
font-size: 0.7rem;
padding: 1px 8px;
border-radius: 9999px;
font-weight: 600;
}
.tdd-json {
position: relative;
}
.tdd-json-wrap {
position: relative;
}
.tdd-copy {
position: sticky;
top: 0;
float: right;
z-index: 2;
background: rgb(var(--art-accent));
color: #000;
border: none;
padding: 0.25rem 0.8rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
margin-bottom: 0.5rem;
}
.tdd-copy:hover {
opacity: 0.9;
}
.tdd-pre {
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 1rem;
font-size: 0.75rem;
line-height: 1.6;
color: #c9d1d9;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.tdd-empty {
color: rgba(255, 255, 255, 0.45);
font-size: 0.85rem;
padding: 1.5rem 0;
text-align: center;
}
</style>