九宫格优化

This commit is contained in:
李琦
2026-08-01 16:55:39 +08:00
parent 8d015f8b37
commit 9950b86841
18 changed files with 1465 additions and 38 deletions

View File

@@ -151,6 +151,16 @@
<a-switch v-model:checked="cfg.introEnabled" />
</a-form-item>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<a-form-item label="祝福弹幕">
<a-switch v-model:checked="cfg.danmakuEnabled" />
<div class="text-[11px] text-[#8A8680] mt-1">关闭后前台不展示弹幕也不显示送祝福入口</div>
</a-form-item>
<a-form-item label="弹幕显示时间">
<a-switch v-model:checked="cfg.danmakuShowTime" :disabled="!cfg.danmakuEnabled" />
<div class="text-[11px] text-[#8A8680] mt-1">开启后弹幕后缀展示相对时间刚刚x分钟前等</div>
</a-form-item>
</div>
</a-form>
</a-card>
</a-tab-pane>
@@ -161,6 +171,10 @@
<div class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
上传多首背景音乐支持 mp3 等格式访客首次交互后自动播放当前曲目可在前台随时切换或暂停
</div>
<a-form-item label="随机播放">
<a-switch v-model:checked="cfg.musicRandomEnabled" />
<div class="text-[11px] text-[#8A8680] mt-1">开启后访客进入时随机选曲本地缓存 24 小时内再次进入保持同一首</div>
</a-form-item>
<a-empty v-if="!cfg.musicList.length" description="尚未上传音乐" />
<a-card v-for="(t, i) in cfg.musicList" :key="i" :bordered="false"
class="!shadow-sm mb-3 !border" :class="t.url === cfg.activeMusicUrl ? '!border-[#a88c6b]' : '!border-[#a88c6b]/15'">
@@ -386,19 +400,65 @@
</template>
</a-table>
</a-tab-pane>
<!-- 弹幕审核 -->
<a-tab-pane key="danmaku" tab="弹幕审核">
<div class="flex items-center justify-between mb-3 gap-3 flex-wrap">
<a-radio-group v-model:value="danmakuFilter" button-style="solid" size="small">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="pending">待审核</a-radio-button>
<a-radio-button value="approved">已通过</a-radio-button>
<a-radio-button value="rejected">已拒绝</a-radio-button>
</a-radio-group>
<a-button size="small" @click="loadDanmaku"><i class="fa-solid fa-rotate-right mr-1"></i>刷新</a-button>
</div>
<a-table :columns="danmakuColumns" :data-source="danmakuList" :loading="danmakuLoading" row-key="id" size="small" :pagination="{ pageSize: 8 }">
<template #bodyCell="{ column, record }">
<template v-if="column.key==='content'">
<span class="inline-flex items-center gap-2 min-w-0">
<i class="danmaku-admin-dot" :style="{ background: resolveDanmakuColor(record.color) }" />
<span class="truncate">{{ record.content }}</span>
</span>
</template>
<template v-else-if="column.key==='source'">
<a-tag :color="record.source === 'rsvp' ? 'blue' : 'gold'">{{ record.source === 'rsvp' ? '回执' : '弹幕' }}</a-tag>
</template>
<template v-else-if="column.key==='status'">
<a-tag :color="danmakuStatusColor(record.status)">{{ danmakuStatusLabel(record.status) }}</a-tag>
</template>
<template v-else-if="column.key==='created_at'">
{{ formatTime(record.created_at) }}
</template>
<template v-else-if="column.key==='actions'">
<a-space>
<a-button size="small" type="primary" class="!bg-[#a88c6b] !border-[#a88c6b]"
:disabled="record.status === 'approved'"
@click="setDanmakuStatus(record.id, 'approved')">通过</a-button>
<a-button size="small" danger
:disabled="record.status === 'rejected'"
@click="setDanmakuStatus(record.id, 'rejected')">拒绝</a-button>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<script setup>
import { computed, reactive, ref, onMounted } from 'vue'
import { computed, reactive, ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { getConfig, saveConfig, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig } from '@/api/wedding'
import {
getConfig, saveConfig, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig,
getDanmakuList, updateDanmakuStatus,
} from '@/api/wedding'
import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
import { resolveDanmakuColor } from '@/utils/danmakuColors'
const router = useRouter()
const adminStore = useAdminStore()
@@ -410,6 +470,9 @@ const uploading = ref(false)
const uploadConfigSaving = ref(false)
const rsvpLoading = ref(false)
const rsvpList = ref([])
const danmakuLoading = ref(false)
const danmakuList = ref([])
const danmakuFilter = ref('pending')
const cfg = reactive(defaultConfig())
const uploadCfg = reactive(defaultUploadConfig())
@@ -425,6 +488,9 @@ function defaultConfig() {
endImg: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=800&q=80',
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }],
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
musicRandomEnabled: false,
danmakuEnabled: true,
danmakuShowTime: true,
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
introExitEffect: 'wind',
freeScrollInterval: 50, pageSwitchDuration: 800, firstScreenDuration: 4500,
@@ -693,6 +759,41 @@ async function loadRsvp() {
finally { rsvpLoading.value = false }
}
const danmakuColumns = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true },
{ title: '来源', key: 'source', width: 80 },
{ title: '状态', key: 'status', width: 90 },
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
{ title: '操作', key: 'actions', width: 160 },
]
const danmakuStatusLabel = (s) => ({ pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s)
const danmakuStatusColor = (s) => ({ pending: 'orange', approved: 'green', rejected: 'default' }[s] || 'default')
async function loadDanmaku() {
danmakuLoading.value = true
try {
const params = danmakuFilter.value ? { status: danmakuFilter.value } : undefined
const res = await getDanmakuList(params)
danmakuList.value = res.data || []
} catch (e) { /* 忽略 */ }
finally { danmakuLoading.value = false }
}
async function setDanmakuStatus(id, status) {
try {
await updateDanmakuStatus(id, status)
message.success(status === 'approved' ? '已通过' : '已拒绝')
loadDanmaku()
} catch (e) {
message.error(e.message || '操作失败')
}
}
watch(danmakuFilter, () => {
if (adminStore.isAdmin) loadDanmaku()
})
onMounted(async () => {
try {
const res = await getConfig()
@@ -714,10 +815,14 @@ onMounted(async () => {
cfg.freeScrollInterval = Number(cfg.freeScrollInterval) || 50
cfg.pageSwitchDuration = Number(cfg.pageSwitchDuration) || 800
cfg.firstScreenDuration = Number.isFinite(Number(cfg.firstScreenDuration)) ? Number(cfg.firstScreenDuration) : 4500
if (typeof cfg.musicRandomEnabled !== 'boolean') cfg.musicRandomEnabled = false
if (typeof cfg.danmakuEnabled !== 'boolean') cfg.danmakuEnabled = true
if (typeof cfg.danmakuShowTime !== 'boolean') cfg.danmakuShowTime = true
}
} catch (e) { /* 使用默认配置 */ }
if (adminStore.isAdmin) {
loadRsvp()
loadDanmaku()
loadUploadConfig()
}
})
@@ -727,4 +832,8 @@ onMounted(async () => {
.admin-tabs :deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) { color: #a88c6b; }
.admin-tabs :deep(.ant-tabs-ink-bar) { background: #a88c6b; }
.admin-tabs :deep(.ant-tabs-tab:hover .ant-tabs-tab-btn) { color: #967b5c; }
.danmaku-admin-dot {
width: 10px; height: 10px; border-radius: 9999px; flex-shrink: 0;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);
}
</style>