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

177 lines
9.0 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 {
PhArrowSquareOut as ArrowSquareOut,
PhCheckCircle as CheckCircle,
PhFloppyDisk as FloppyDisk,
PhImageSquare as ImageSquare,
PhUploadSimple as UploadSimple,
} from '@phosphor-icons/vue'
import { apiErrorMessage } from '~/composables/useUnsavedChanges'
import type { SiteDocument } from '../../../server/utils/site-schema'
definePageMeta({ layout: false })
useHead(() => ({ title: `首页管理|${document.value?.brand.name || '慧遇书院'}` }))
const loading = ref(true)
const saving = ref(false)
const publishing = ref(false)
const message = ref('')
const document = ref<SiteDocument>()
const user = ref<{ displayName: string }>()
const uploadBusy = ref<string>()
const { dirty, markClean } = useUnsavedChanges(document)
onMounted(async () => {
try {
const [me, site] = await Promise.all([
$fetch<{ user: { displayName: string } }>('/api/auth/me'),
$fetch<{ data: SiteDocument }>('/api/admin/site'),
])
user.value = me.user
document.value = site.data
await nextTick()
markClean()
} catch {
await navigateTo('/admin/login')
} finally {
loading.value = false
}
})
async function save() {
if (!document.value) return
saving.value = true
message.value = ''
try {
await $fetch('/api/admin/site', { method: 'PUT', body: document.value })
message.value = '草稿已保存'
markClean()
} catch (error: any) {
message.value = apiErrorMessage(error, '保存失败,请检查页面内容')
} finally {
saving.value = false
}
}
async function publish() {
await save()
publishing.value = true
try {
const result = await $fetch<{ version: number }>('/api/admin/site/publish', { method: 'POST' })
message.value = `已发布 V${result.version}`
markClean()
} catch (error: any) {
message.value = apiErrorMessage(error, '发布失败,请检查页面内容')
} finally {
publishing.value = false
}
}
async function uploadImage(event: Event, target: 'brand.logo' | 'hero.backgroundImage' | 'teacher.portrait') {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file || !document.value) return
uploadBusy.value = target
message.value = ''
try {
const body = new FormData()
body.append('file', file)
const result = await $fetch<{ url: string }>('/api/admin/media', { method: 'POST', body })
if (target === 'brand.logo') document.value.brand.logo = result.url
if (target === 'hero.backgroundImage') document.value.hero.backgroundImage = result.url
if (target === 'teacher.portrait') document.value.teacher.portrait = result.url
message.value = '图片已上传,请保存草稿并发布'
} catch (error: any) {
message.value = error?.data?.statusMessage || '图片上传失败'
} finally {
uploadBusy.value = undefined
input.value = ''
}
}
</script>
<template>
<main class="admin-shell">
<AdminSidebar active="home" :logo="document?.brand.logo" :brand-name="document?.brand.name" :english-name="document?.brand.englishName" />
<section class="admin-main">
<header class="admin-topbar">
<div><p>首页管理</p><small>维护官网核心文案并实时预览</small></div>
<div class="admin-actions">
<span v-if="dirty" class="dirty-indicator">有未保存修改</span>
<span v-if="message" class="save-message"><CheckCircle :size="18" />{{ message }}</span>
<a href="/?preview=draft" target="_blank"><ArrowSquareOut :size="19" />预览草稿</a>
<button type="button" :disabled="saving" @click="save"><FloppyDisk :size="19" />{{ saving ? '保存中' : '保存草稿' }}</button>
<button class="publish-button" type="button" :disabled="publishing" @click="publish"><UploadSimple :size="19" />{{ publishing ? '发布中' : '发布更新' }}</button>
</div>
</header>
<div v-if="loading" class="admin-loading">正在加载配置</div>
<div v-else-if="document" class="admin-workspace">
<section class="editor-panel">
<div class="panel-heading"><div><span>00</span><h1>公司与品牌</h1></div><small>全站统一信息</small></div>
<div class="form-grid">
<label>官网展示名称<input v-model="document.brand.name" maxlength="40" placeholder="例如:慧遇书院"></label>
<label>公司主体全称<input v-model="document.brand.companyName" maxlength="100" placeholder="营业执照登记名称"></label>
<label>英文名称<input v-model="document.brand.englishName" maxlength="80"></label>
<label>品牌主张<input v-model="document.brand.slogan" maxlength="40"></label>
<label>品牌副文案<input v-model="document.brand.subSlogan" maxlength="80"></label>
<label class="full upload-field">品牌 Logo
<span class="image-input"><img :src="document.brand.logo" alt="当前品牌 Logo"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, 'brand.logo')"><b>{{ uploadBusy === 'brand.logo' ? '上传中…' : '选择图片' }}</b></span>
<MediaPicker v-model="document.brand.logo" />
</label>
</div>
<div class="panel-heading secondary"><div><span>01</span><h2>英雄区</h2></div><small>首屏核心信息</small></div>
<div class="form-grid">
<label>第一行标题<input v-model="document.hero.titleLine1" maxlength="20"></label>
<label>第二行标题<input v-model="document.hero.titleLine2" maxlength="20"></label>
<label class="full">副标题<input v-model="document.hero.subtitle" maxlength="80"></label>
<label>按钮文字<input v-model="document.hero.cta" maxlength="20"></label>
<label>SEO 标题<input v-model="document.seo.title" maxlength="70"></label>
<label class="full upload-field">首屏背景图
<span class="image-input"><img :src="document.hero.backgroundImage" alt="当前首屏背景"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, 'hero.backgroundImage')"><b>{{ uploadBusy === 'hero.backgroundImage' ? '上传中…' : '选择图片' }}</b></span>
<MediaPicker v-model="document.hero.backgroundImage" />
</label>
</div>
<div id="teacher-form" class="panel-heading secondary"><div><span>02</span><h2>卢慧老师</h2></div><small>首页展示摘要</small></div>
<div class="form-grid">
<label>姓名<input v-model="document.teacher.name" maxlength="30"></label>
<label>身份说明<input v-model="document.teacher.title" maxlength="80"></label>
<label class="full">简介<textarea v-model="document.teacher.summary" rows="5" maxlength="400" /></label>
<label class="full upload-field">人物肖像
<span class="image-input portrait"><img :src="document.teacher.portrait" :alt="document.teacher.name"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, 'teacher.portrait')"><b>{{ uploadBusy === 'teacher.portrait' ? '上传中…' : '选择图片' }}</b></span>
<MediaPicker v-model="document.teacher.portrait" />
</label>
</div>
<div class="panel-heading secondary"><div><span>03</span><h2>联系与搜索信息</h2></div><small>页脚和搜索引擎展示</small></div>
<div class="form-grid">
<label>联系电话<input v-model="document.contact.phone" maxlength="30"></label>
<label>联系邮箱<input v-model="document.contact.email" maxlength="100"></label>
<label class="full">公司地址<input v-model="document.contact.address" maxlength="160"></label>
<label class="full">SEO 描述<textarea v-model="document.seo.description" rows="3" maxlength="180" /></label>
</div>
<div id="media" class="media-placeholder">
<ImageSquare :size="32" weight="light" />
<div><strong>本地素材存储</strong><p>一期图片保存在服务器数据卷Logo首屏背景与人物肖像已支持上传配置</p></div>
</div>
</section>
<aside class="preview-panel">
<div class="preview-label"><span>网站实时预览</span><small>{{ user?.displayName }}</small></div>
<div class="preview-window" :style="{ backgroundImage: `url(${document.hero.backgroundImage})` }">
<BrandLogo :src="document.brand.logo" :name="document.brand.name" :english-name="document.brand.englishName" />
<div><h2>{{ document.hero.titleLine1 }}<br>{{ document.hero.titleLine2 }}</h2><p>{{ document.hero.subtitle }}</p><span>{{ document.hero.cta }} </span></div>
</div>
<div class="preview-teacher">
<img :src="document.teacher.portrait" :alt="document.teacher.name">
<div><h3>{{ document.teacher.name }}</h3><p>{{ document.teacher.title }}</p><small>{{ document.teacher.summary }}</small></div>
</div>
</aside>
</div>
</section>
</main>
</template>