feat: 初始化慧遇书院官网一期
This commit is contained in:
111
app/pages/admin/media.vue
Normal file
111
app/pages/admin/media.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { PhArrowCounterClockwise, PhCheckCircle, PhFloppyDisk, PhImageSquare, PhTrash, PhUploadSimple } from '@phosphor-icons/vue'
|
||||
import type { SiteDocument } from '../../../server/utils/site-schema'
|
||||
|
||||
definePageMeta({ layout: false })
|
||||
useHead(() => ({ title: `素材中心|${site.value?.brand.name || '慧遇书院'}` }))
|
||||
|
||||
interface MediaAsset {
|
||||
id: number; key: string; originalName: string; mimeType: string; bytes: number; width: number; height: number
|
||||
url: string; thumbnailUrl: string; altText: string; source: string; copyright: string
|
||||
authorizationStatus: 'pending' | 'authorized' | 'restricted'; status: 'active' | 'trash' | 'deleted'; referenced: boolean
|
||||
createdAt: string; deletedAt: string | null
|
||||
}
|
||||
|
||||
const site = ref<SiteDocument>()
|
||||
const items = ref<MediaAsset[]>([])
|
||||
const selectedId = ref<number>()
|
||||
const view = ref<'active' | 'trash'>('active')
|
||||
const loading = ref(true), busy = ref(false), message = ref('')
|
||||
const uploadMeta = reactive({ altText: '', source: '', copyright: '', authorizationStatus: 'pending' as MediaAsset['authorizationStatus'] })
|
||||
const selected = computed(() => items.value.find(item => item.id === selectedId.value))
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await $fetch('/api/auth/me')
|
||||
site.value = (await $fetch<{ data: SiteDocument }>('/api/admin/site')).data
|
||||
await loadItems()
|
||||
} catch { await navigateTo('/admin/login') }
|
||||
finally { loading.value = false }
|
||||
})
|
||||
|
||||
async function loadItems() {
|
||||
const result = await $fetch<{ data: MediaAsset[] }>('/api/admin/media', { query: { status: view.value } })
|
||||
items.value = result.data
|
||||
selectedId.value = items.value[0]?.id
|
||||
}
|
||||
|
||||
async function changeView(next: 'active' | 'trash') { view.value = next; message.value = ''; await loadItems() }
|
||||
|
||||
async function upload(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
busy.value = true; message.value = ''
|
||||
try {
|
||||
const body = new FormData(); body.append('file', file)
|
||||
Object.entries(uploadMeta).forEach(([key, value]) => body.append(key, value))
|
||||
const result = await $fetch<{ media: MediaAsset }>('/api/admin/media', { method: 'POST', body })
|
||||
items.value.unshift(result.media); selectedId.value = result.media.id
|
||||
uploadMeta.altText = ''; uploadMeta.source = ''; uploadMeta.copyright = ''; uploadMeta.authorizationStatus = 'pending'
|
||||
message.value = '素材已上传并生成 WebP 与缩略图'
|
||||
} catch (error: any) { message.value = error?.data?.statusMessage || '上传失败' }
|
||||
finally { busy.value = false; input.value = '' }
|
||||
}
|
||||
|
||||
async function saveMetadata() {
|
||||
if (!selected.value) return
|
||||
busy.value = true; message.value = ''
|
||||
try {
|
||||
await $fetch(`/api/admin/media/${selected.value.id}`, { method: 'PATCH', body: { altText: selected.value.altText, source: selected.value.source, copyright: selected.value.copyright, authorizationStatus: selected.value.authorizationStatus } })
|
||||
message.value = '素材信息已保存'
|
||||
} catch (error: any) { message.value = error?.data?.statusMessage || '保存失败' }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function trash() {
|
||||
if (!selected.value || !window.confirm('确定把该素材移入回收站吗?')) return
|
||||
busy.value = true; message.value = ''
|
||||
try { await $fetch(`/api/admin/media/${selected.value.id}`, { method: 'DELETE' }); message.value = '素材已移入回收站'; await loadItems() }
|
||||
catch (error: any) { message.value = error?.data?.statusMessage || '删除失败' }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function restore() {
|
||||
if (!selected.value) return
|
||||
busy.value = true
|
||||
try { await $fetch(`/api/admin/media/${selected.value.id}/restore`, { method: 'POST' }); message.value = '素材已恢复'; await loadItems() }
|
||||
catch (error: any) { message.value = error?.data?.statusMessage || '恢复失败' }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => bytes < 1024 * 1024 ? `${Math.ceil(bytes / 1024)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="admin-shell">
|
||||
<AdminSidebar active="media" :logo="site?.brand.logo" :brand-name="site?.brand.name" :english-name="site?.brand.englishName" />
|
||||
<section class="admin-main">
|
||||
<header class="admin-topbar"><div><p>素材中心</p><small>原图、WebP 衍生图、授权信息与安全回收站</small></div><div class="admin-actions"><span v-if="message" class="save-message"><PhCheckCircle :size="18" />{{ message }}</span><button :class="{ 'publish-button': view === 'active' }" @click="changeView('active')">素材库</button><button :class="{ 'publish-button': view === 'trash' }" @click="changeView('trash')">回收站</button></div></header>
|
||||
<div v-if="loading" class="admin-loading">正在加载素材…</div>
|
||||
<div v-else class="media-admin-body">
|
||||
<section v-if="view === 'active'" class="media-upload-panel editor-panel">
|
||||
<div><PhUploadSimple :size="30" /><strong>上传新图片</strong><small>支持 JPG、PNG、WebP;最大 8MB、4000 万像素</small></div>
|
||||
<div class="media-upload-meta"><input v-model="uploadMeta.altText" placeholder="ALT 描述(建议填写)"><input v-model="uploadMeta.source" placeholder="素材来源"><input v-model="uploadMeta.copyright" placeholder="版权/授权说明"><select v-model="uploadMeta.authorizationStatus"><option value="pending">待核验</option><option value="authorized">已授权</option><option value="restricted">限制使用</option></select></div>
|
||||
<label class="media-upload-button"><input type="file" accept="image/jpeg,image/png,image/webp" :disabled="busy" @change="upload">{{ busy ? '处理中…' : '选择并上传图片' }}</label>
|
||||
</section>
|
||||
<div class="media-library-layout">
|
||||
<section class="media-grid-panel">
|
||||
<div v-if="!items.length" class="empty-editor"><PhImageSquare :size="38" /><p>{{ view === 'active' ? '尚未上传素材' : '回收站为空' }}</p></div>
|
||||
<button v-for="item in items" :key="item.id" :class="['media-tile', { active: item.id === selectedId }]" @click="selectedId = item.id"><img :src="item.thumbnailUrl" :alt="item.altText || item.originalName"><span>{{ item.originalName }}</span><small>{{ item.width }}×{{ item.height }} · {{ formatBytes(item.bytes) }}</small><b v-if="item.referenced">使用中</b></button>
|
||||
</section>
|
||||
<aside v-if="selected" class="media-detail editor-panel">
|
||||
<img :src="selected.url" :alt="selected.altText || selected.originalName"><div class="media-detail-meta"><strong>{{ selected.originalName }}</strong><small>{{ selected.mimeType }} · {{ selected.width }}×{{ selected.height }} · {{ formatBytes(selected.bytes) }}</small><a :href="selected.url" target="_blank">打开衍生图</a></div>
|
||||
<div v-if="view === 'active'" class="form-grid"><label class="full">ALT 描述<input v-model="selected.altText" maxlength="160"></label><label class="full">素材来源<input v-model="selected.source" maxlength="200"></label><label class="full">版权/授权说明<input v-model="selected.copyright" maxlength="200"></label><label class="full">授权状态<select v-model="selected.authorizationStatus"><option value="pending">待核验</option><option value="authorized">已授权</option><option value="restricted">限制使用</option></select></label></div>
|
||||
<div class="media-detail-actions"><template v-if="view === 'active'"><button :disabled="busy" @click="saveMetadata"><PhFloppyDisk :size="17" />保存信息</button><button class="danger-link" :disabled="busy || selected.referenced" @click="trash"><PhTrash :size="17" />{{ selected.referenced ? '页面使用中' : '移入回收站' }}</button></template><button v-else :disabled="busy" @click="restore"><PhArrowCounterClockwise :size="17" />恢复素材</button></div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user