Files
guanwang/app/pages/admin/media.vue

112 lines
7.8 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.
<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>支持 JPGPNGWebP最大 8MB4000 万像素Logo 建议使用透明 PNG宽度 6001600px</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>