feat: item detail and admin moderation UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
肉末
2026-07-14 13:02:11 +08:00
parent 445a940f7d
commit 600c536367
2 changed files with 461 additions and 5 deletions
+159 -3
View File
@@ -1,9 +1,165 @@
<script setup lang="ts">
import MarkdownIt from 'markdown-it'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { fetchItem, itemFileUrl, type Item } from '../api/client'
const md = new MarkdownIt({
html: false,
linkify: true,
breaks: true,
})
const route = useRoute()
const item = ref<Item | null>(null)
const loading = ref(true)
const error = ref('')
const showBurnAlert = ref(false)
const renderedBody = computed(() => {
if (!item.value?.body) return ''
return md.render(item.value.body)
})
const isImage = computed(() => {
const mime = item.value?.mime || ''
if (!mime.startsWith('image/')) return false
if (mime === 'image/svg+xml') return false
const name = (item.value?.file_name || '').toLowerCase()
return !name.endsWith('.svg')
})
const fileUrl = computed(() => {
if (!item.value || item.value.kind !== 'file') return ''
return itemFileUrl(item.value.slug)
})
async function load() {
const slug = String(route.params.slug || '')
loading.value = true
error.value = ''
item.value = null
showBurnAlert.value = false
try {
const data = await fetchItem(slug)
item.value = data
showBurnAlert.value = data.burn_after_read
} catch (err) {
error.value = err instanceof Error ? err.message : '加载失败'
ElMessage.error(error.value)
} finally {
loading.value = false
}
}
onMounted(load)
watch(
() => route.params.slug,
() => {
void load()
},
)
</script>
<template>
<div>
<h2>内容详情</h2>
<p>详情页占位</p>
<div v-loading="loading" class="item-view">
<el-alert
v-if="showBurnAlert"
class="burn-alert"
type="warning"
title="阅后即焚"
description="此内容仅可查看一次,离开或刷新后将无法再次打开。"
show-icon
:closable="true"
@close="showBurnAlert = false"
/>
<el-result v-if="!loading && error" icon="warning" :title="error || '未找到'" />
<template v-else-if="item">
<h2 class="title">{{ item.title || item.file_name || item.slug }}</h2>
<div class="meta">
<span>{{ item.kind === 'file' ? '文件' : '文本' }}</span>
<span v-if="item.burn_after_read">阅后即焚</span>
<span v-if="!item.is_public">仅链接可见</span>
</div>
<div v-if="item.kind === 'text'" class="markdown" v-html="renderedBody" />
<div v-else class="file-block">
<template v-if="isImage">
<img class="preview" :src="fileUrl" :alt="item.file_name || item.title" />
</template>
<template v-else>
<p>文件{{ item.file_name || item.slug }}</p>
<el-button type="primary" class="full-width-xs" tag="a" :href="fileUrl" download>
下载文件
</el-button>
</template>
</div>
</template>
</div>
</template>
<style scoped>
.item-view {
min-height: 200px;
}
.burn-alert {
margin-bottom: 16px;
}
.title {
margin: 0 0 8px;
font-size: 1.25rem;
word-break: break-word;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-bottom: 16px;
color: #909399;
font-size: 0.9rem;
}
.markdown {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
line-height: 1.6;
word-break: break-word;
}
.markdown :deep(pre) {
overflow-x: auto;
padding: 10px;
background: #f5f7fa;
border-radius: 6px;
}
.markdown :deep(img) {
max-width: 100%;
height: auto;
}
.file-block {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 14px;
}
.preview {
display: block;
max-width: 100%;
height: auto;
border-radius: 4px;
}
</style>