初始化
This commit is contained in:
352
vite-tailwindcss/src/views/admin/AdminEditor.vue
Normal file
352
vite-tailwindcss/src/views/admin/AdminEditor.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#f6f4ef] text-[#2c2c2c]">
|
||||
<!-- 顶部栏 -->
|
||||
<header class="sticky top-0 z-30 bg-white/90 backdrop-blur border-b border-[#a88c6b]/15 px-6 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl text-[#a88c6b] font-serif">❀</span>
|
||||
<div>
|
||||
<div class="text-sm tracking-[0.2em] font-light">婚礼纪 · 内容编辑后台</div>
|
||||
<div class="text-[10px] text-[#8A8680] tracking-[0.3em] uppercase">Invitation Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a-button size="small" @click="openPreview">
|
||||
<i class="fa-solid fa-eye mr-1 text-[#a88c6b]"></i>预览请柬
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" :loading="saving" @click="onSave"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存配置
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<a-button size="small">{{ adminStore.user?.account || '管理员' }}<i class="fa-solid fa-chevron-down ml-1 text-[10px]"></i></a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="logout" @click="onLogout"><i class="fa-solid fa-right-from-bracket mr-2"></i>退出登录</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="max-w-[960px] mx-auto px-4 py-6">
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs">
|
||||
<!-- 基础信息 -->
|
||||
<a-tab-pane key="basic" tab="基础信息">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="新郎">
|
||||
<a-input v-model:value="cfg.groom" placeholder="新郎姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="新娘">
|
||||
<a-input v-model:value="cfg.bride" placeholder="新娘姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="公历日期">
|
||||
<a-input v-model:value="cfg.date" placeholder="如 2026-05-20" />
|
||||
</a-form-item>
|
||||
<a-form-item label="农历 / 星期">
|
||||
<a-input v-model:value="cfg.lunar" placeholder="如 农历四月初四 星期三" />
|
||||
</a-form-item>
|
||||
<a-form-item label="酒店名称">
|
||||
<a-input v-model:value="cfg.hotel" placeholder="宴会酒店" />
|
||||
</a-form-item>
|
||||
<a-form-item label="详细地址">
|
||||
<a-textarea v-model:value="cfg.address" :rows="2" placeholder="可换行" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-form-item label="封面 / 尾页主图">
|
||||
<image-field v-model="cfg.heroImg" @pick="openUpload(u => cfg.heroImg = u)" :uploading="uploading" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 文案 -->
|
||||
<a-tab-pane key="copy" tab="邀请文案">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="竖排文案 · 上联">
|
||||
<a-input v-model:value="cfg.formalText1" placeholder="如 两姓联姻 一堂缔约" />
|
||||
</a-form-item>
|
||||
<a-form-item label="竖排文案 · 下联">
|
||||
<a-input v-model:value="cfg.formalText2" placeholder="如 良缘永结 匹配同称" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 视觉 / 动效 -->
|
||||
<a-tab-pane key="visual" tab="视觉与动效">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="背景音乐链接 (URL)">
|
||||
<a-input v-model:value="cfg.musicUrl" placeholder="留空则不播放音乐" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">支持 mp3 直链,访客首次交互后自动播放,可随时暂停。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="滚动方式">
|
||||
<a-radio-group v-model:value="cfg.scrollMode">
|
||||
<a-radio value="snap">整页 / 分屏吸附滚动</a-radio>
|
||||
<a-radio value="free">自由滚动</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">整页模式会带来更有仪式感的逐屏切换效果。</div>
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a-form-item label="飘落花瓣">
|
||||
<a-switch v-model:checked="cfg.petalEnabled" />
|
||||
</a-form-item>
|
||||
<a-form-item label="漂浮爱心">
|
||||
<a-switch v-model:checked="cfg.bubbleEnabled" />
|
||||
</a-form-item>
|
||||
<a-form-item label="入场揭幕">
|
||||
<a-switch v-model:checked="cfg.introEnabled" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 相册页 -->
|
||||
<a-tab-pane key="photos" tab="相册页管理">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-[12px] text-[#8A8680]">共 {{ cfg.photoPages.length }} 个相册页</span>
|
||||
<a-button size="small" @click="addPage"><i class="fa-solid fa-plus mr-1 text-[#a88c6b]"></i>新增相册页</a-button>
|
||||
</div>
|
||||
<a-empty v-if="!cfg.photoPages.length" description="暂无相册页" />
|
||||
<a-card v-for="(page, idx) in cfg.photoPages" :key="idx" :bordered="false" class="!shadow-sm mb-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-[#a88c6b] text-sm">第 {{ idx + 1 }} 页</span>
|
||||
<div class="flex gap-1">
|
||||
<a-button size="small" :disabled="idx===0" @click="movePage(idx,-1)"><i class="fa-solid fa-arrow-up"></i></a-button>
|
||||
<a-button size="small" :disabled="idx===cfg.photoPages.length-1" @click="movePage(idx,1)"><i class="fa-solid fa-arrow-down"></i></a-button>
|
||||
<a-button size="small" danger @click="removePage(idx)"><i class="fa-solid fa-trash"></i></a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="版式">
|
||||
<a-select v-model:value="page.type" @change="() => ensurePageShape(page)">
|
||||
<a-select-option value="single">单图</a-select-option>
|
||||
<a-select-option value="overlap">错落双图</a-select-option>
|
||||
<a-select-option value="one-large-five-small">一大五小</a-select-option>
|
||||
<a-select-option value="nine-grid">九宫格</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="小标题 (英文)">
|
||||
<a-input v-model:value="page.subtitle" />
|
||||
</a-form-item>
|
||||
<a-form-item label="标题">
|
||||
<a-input v-model:value="page.title" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="page.desc" :rows="2" />
|
||||
</a-form-item>
|
||||
<!-- 图片上传区:按版式渲染 -->
|
||||
<div class="flex flex-wrap gap-3 mt-2">
|
||||
<template v-if="page.type==='single'">
|
||||
<image-field v-model="page.img1" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
|
||||
</template>
|
||||
<template v-else-if="page.type==='overlap'">
|
||||
<image-field v-model="page.img1" label="图一" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
|
||||
<image-field v-model="page.img2" label="图二" @pick="openUpload(u => page.img2 = u)" :uploading="uploading" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<image-field v-for="(img, iidx) in page.images" :key="iidx"
|
||||
v-model="page.images[iidx]"
|
||||
:label="`图${iidx+1}`"
|
||||
@pick="openUpload(u => page.images[iidx] = u)"
|
||||
:uploading="uploading" />
|
||||
</template>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 婚礼流程 -->
|
||||
<a-tab-pane key="schedule" tab="婚礼流程">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-[12px] text-[#8A8680]">共 {{ cfg.schedule.length }} 个环节</span>
|
||||
<a-button size="small" @click="addSchedule"><i class="fa-solid fa-plus mr-1 text-[#a88c6b]"></i>新增环节</a-button>
|
||||
</div>
|
||||
<a-card v-for="(item, idx) in cfg.schedule" :key="idx" :bordered="false" class="!shadow-sm mb-3">
|
||||
<div class="flex items-end gap-3 flex-wrap">
|
||||
<div class="flex-1 min-w-[120px]">
|
||||
<div class="text-[11px] text-[#a88c6b] mb-1">时间</div>
|
||||
<a-input v-model:value="item.time" placeholder="如 17:08" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<div class="text-[11px] text-[#a88c6b] mb-1">环节</div>
|
||||
<a-input v-model:value="item.event" placeholder="如 仪式开始" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<div class="text-[11px] text-[#a88c6b] mb-1">英文副标题</div>
|
||||
<a-input v-model:value="item.desc" placeholder="如 Ceremony" />
|
||||
</div>
|
||||
<a-button danger size="small" @click="removeSchedule(idx)"><i class="fa-solid fa-trash"></i></a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 回执列表 -->
|
||||
<a-tab-pane key="rsvp" tab="出席回执">
|
||||
<a-table :columns="rsvpColumns" :data-source="rsvpList" :loading="rsvpLoading" row-key="id" size="small" :pagination="{ pageSize: 8 }">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key==='guest_count'">
|
||||
<a-tag :color="record.guest_count==='0' ? 'default' : 'pink'">{{ rsvpCountLabel(record.guest_count) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key==='created_at'">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getConfig, saveConfig, uploadImage, getRsvpList } from '@/api/wedding'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
const activeTab = ref('basic')
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const rsvpLoading = ref(false)
|
||||
const rsvpList = ref([])
|
||||
|
||||
const cfg = reactive(defaultConfig())
|
||||
|
||||
function defaultConfig() {
|
||||
return {
|
||||
groom: '李明', bride: '王华', date: '2026-05-20', lunar: '农历四月初四 星期三',
|
||||
hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅',
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80',
|
||||
musicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6) },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9) },
|
||||
],
|
||||
schedule: [
|
||||
{ time: '16:00', event: '签到迎宾', desc: 'Welcome' },
|
||||
{ time: '17:08', event: '仪式开始', desc: 'Ceremony' },
|
||||
{ time: '18:00', event: '敬备喜宴', desc: 'Banquet' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function fill(n) {
|
||||
return Array.from({ length: n }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80')
|
||||
}
|
||||
|
||||
function ensurePageShape(p) {
|
||||
if (p.type === 'single') { p.img1 = p.img1 || ''; delete p.img2; delete p.images }
|
||||
else if (p.type === 'overlap') { p.img1 = p.img1 || ''; p.img2 = p.img2 || ''; delete p.images }
|
||||
else {
|
||||
const need = p.type === 'one-large-five-small' ? 6 : 9
|
||||
p.images = Array.isArray(p.images) ? p.images : []
|
||||
while (p.images.length < need) p.images.push('')
|
||||
if (p.images.length > need) p.images.length = need
|
||||
delete p.img1; delete p.img2
|
||||
}
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
return p
|
||||
}
|
||||
|
||||
// 相册页操作
|
||||
function addPage() { cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' })) }
|
||||
function removePage(i) { cfg.photoPages.splice(i, 1) }
|
||||
function movePage(i, dir) { const j = i + dir; if (j < 0 || j >= cfg.photoPages.length) return; const t = cfg.photoPages[i]; cfg.photoPages[i] = cfg.photoPages[j]; cfg.photoPages[j] = t }
|
||||
// 流程操作
|
||||
function addSchedule() { cfg.schedule.push({ time: '', event: '', desc: '' }) }
|
||||
function removeSchedule(i) { cfg.schedule.splice(i, 1) }
|
||||
|
||||
// 上传
|
||||
function openUpload(onUrl) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'; input.accept = 'image/*'
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0]; if (!file) return
|
||||
uploading.value = true
|
||||
try {
|
||||
const res = await uploadImage(file)
|
||||
onUrl(res.url); message.success('上传成功')
|
||||
} catch (err) {
|
||||
message.error('上传失败')
|
||||
} finally {
|
||||
uploading.value = false; e.target.value = ''
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
// 保存
|
||||
async function onSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await saveConfig(JSON.parse(JSON.stringify(cfg)))
|
||||
message.success('配置已保存')
|
||||
} catch (e) {
|
||||
const msg = e?.response?.status === 401 ? '登录已失效,请重新登录' : (e?.message || '保存失败')
|
||||
message.error(msg)
|
||||
if (e?.response?.status === 401) router.replace({ name: 'Login', query: { redirect: '/admin' } })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview() { window.open('/', '_blank') }
|
||||
|
||||
function onLogout() {
|
||||
adminStore.logout()
|
||||
router.replace({ name: 'Login', query: { redirect: '/admin' } })
|
||||
}
|
||||
|
||||
// RSVP
|
||||
const rsvpColumns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '出席', dataIndex: 'guest_count', key: 'guest_count' },
|
||||
{ title: '祝福', dataIndex: 'wishes', key: 'wishes', ellipsis: true },
|
||||
{ title: '提交时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
]
|
||||
const rsvpCountLabel = (v) => ({ '1': '1 人出席', '2': '2 人出席', '3': '3 人及以上', '0': '遗憾缺席' }[v] || v)
|
||||
function formatTime(t) {
|
||||
if (!t) return ''
|
||||
const d = new Date(t)
|
||||
if (isNaN(d)) return t
|
||||
return d.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
async function loadRsvp() {
|
||||
rsvpLoading.value = true
|
||||
try { const res = await getRsvpList(); rsvpList.value = res.data || [] }
|
||||
catch (e) { /* 忽略 */ }
|
||||
finally { rsvpLoading.value = false }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getConfig()
|
||||
if (res.data && res.data !== '{}') {
|
||||
const loaded = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||||
Object.assign(cfg, loaded)
|
||||
cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape)
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
if (adminStore.isAdmin) loadRsvp()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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; }
|
||||
</style>
|
||||
65
vite-tailwindcss/src/views/admin/AdminLogin.vue
Normal file
65
vite-tailwindcss/src/views/admin/AdminLogin.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="min-h-screen w-full flex items-center justify-center bg-[#Fdfbf7] relative overflow-hidden">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="absolute -top-24 -right-24 w-80 h-80 rounded-full bg-[#a88c6b]/10 blur-3xl"></div>
|
||||
<div class="absolute -bottom-24 -left-24 w-80 h-80 rounded-full bg-[#e8a5b2]/10 blur-3xl"></div>
|
||||
<div class="absolute inset-0 pointer-events-none opacity-[0.04]"
|
||||
style="background-image: radial-gradient(#a88c6b 1px, transparent 1px); background-size: 22px 22px;"></div>
|
||||
|
||||
<div class="relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up">
|
||||
<div class="text-center mb-8">
|
||||
<div class="text-3xl mb-3 text-[#a88c6b] font-serif">❀</div>
|
||||
<h1 class="text-xl tracking-[0.3em] text-[#2c2c2c] font-light">婚礼纪 · 后台</h1>
|
||||
<p class="text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase">Admin Console</p>
|
||||
</div>
|
||||
|
||||
<a-form :model="form" layout="vertical" @finish="onSubmit">
|
||||
<a-form-item name="account" :rules="[{ required: true, message: '请输入账号' }]">
|
||||
<a-input v-model:value="form.account" size="large" placeholder="管理员账号">
|
||||
<template #prefix><i class="fa-solid fa-user text-[#a88c6b]"></i></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="password" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<a-input-password v-model:value="form.password" size="large" placeholder="密码">
|
||||
<template #prefix><i class="fa-solid fa-lock text-[#a88c6b]"></i></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b] !rounded-xl !tracking-[0.2em]">
|
||||
登 录
|
||||
</a-button>
|
||||
</a-form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<router-link to="/" class="text-[12px] text-[#a88c6b] hover:underline">返回请柬预览</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const adminStore = useAdminStore()
|
||||
const loading = ref(false)
|
||||
const form = reactive({ account: '', password: '' })
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await adminStore.login(form)
|
||||
message.success('登录成功')
|
||||
router.replace(route.query.redirect || '/admin')
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.error || e?.message || '登录失败'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user