- 成就主题/等级管理 - 梦想目标图标管理(dream-presets)与路由「梦想图标」 Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function listThemes() {
|
||||
return request({ url: '/api/admin/achievement/themes', method: 'get' })
|
||||
}
|
||||
|
||||
export function getTheme(id) {
|
||||
return request({ url: `/api/admin/achievement/themes/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
export function createTheme(data) {
|
||||
return request({ url: '/api/admin/achievement/themes', method: 'post', data })
|
||||
}
|
||||
|
||||
export function updateTheme(id, data) {
|
||||
return request({ url: `/api/admin/achievement/themes/${id}`, method: 'put', data })
|
||||
}
|
||||
|
||||
export function deleteTheme(id) {
|
||||
return request({ url: `/api/admin/achievement/themes/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
export function listLevels(themeId) {
|
||||
return request({ url: `/api/admin/achievement/themes/${themeId}/levels`, method: 'get' })
|
||||
}
|
||||
|
||||
export function createLevel(data) {
|
||||
return request({ url: '/api/admin/achievement/levels', method: 'post', data })
|
||||
}
|
||||
|
||||
export function updateLevel(id, data) {
|
||||
return request({ url: `/api/admin/achievement/levels/${id}`, method: 'put', data })
|
||||
}
|
||||
|
||||
export function deleteLevel(id) {
|
||||
return request({ url: `/api/admin/achievement/levels/${id}`, method: 'delete' })
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function listDreamPresets() {
|
||||
return request({ url: '/api/admin/dream-presets', method: 'get' })
|
||||
}
|
||||
|
||||
export function createDreamPreset(data) {
|
||||
return request({ url: '/api/admin/dream-presets', method: 'post', data })
|
||||
}
|
||||
|
||||
export function updateDreamPreset(id, data) {
|
||||
return request({ url: `/api/admin/dream-presets/${id}`, method: 'put', data })
|
||||
}
|
||||
|
||||
export function deleteDreamPreset(id) {
|
||||
return request({ url: `/api/admin/dream-presets/${id}`, method: 'delete' })
|
||||
}
|
||||
@@ -102,6 +102,18 @@ const routes = [
|
||||
component: () => import('../views/smoke/index.vue'),
|
||||
meta: { title: '戒烟小程序', icon: 'Opportunity' }
|
||||
},
|
||||
{
|
||||
path: 'achievement',
|
||||
name: 'Achievement',
|
||||
component: () => import('../views/achievement/index.vue'),
|
||||
meta: { title: '成就管理', icon: 'Trophy' }
|
||||
},
|
||||
{
|
||||
path: 'dream-presets',
|
||||
name: 'DreamPresets',
|
||||
component: () => import('../views/dream-presets/index.vue'),
|
||||
meta: { title: '梦想图标', icon: 'Flag' }
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="achievement-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>成就主题管理</span>
|
||||
<el-button type="primary" @click="openThemeDialog()">新增主题</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="themes" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="icon" label="图标" width="60" />
|
||||
<el-table-column prop="name" label="名称" width="100" />
|
||||
<el-table-column prop="key" label="标识" width="100" />
|
||||
<el-table-column label="等级数" width="80">
|
||||
<template #default="{ row }">{{ row.levels?.length || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="等级预览" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<span v-for="(level, idx) in (row.levels || [])" :key="level.id" class="level-preview">
|
||||
{{ level.name }}({{ level.required_days }}天)<span v-if="idx < row.levels.length - 1"> → </span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="60" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
|
||||
{{ row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openLevelDialog(row)">管理等级</el-button>
|
||||
<el-button size="small" @click="openThemeDialog(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteTheme(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 主题编辑弹窗 -->
|
||||
<el-dialog v-model="themeDialogVisible" :title="editingTheme ? '编辑主题' : '新增主题'" width="460px">
|
||||
<el-form :model="themeForm" label-width="80px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="themeForm.name" placeholder="如:修仙" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标识">
|
||||
<el-input v-model="themeForm.key" placeholder="如:xiuxian" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标">
|
||||
<el-input v-model="themeForm.icon" placeholder="如:⚔️" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="themeForm.sort_order" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="themeForm.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="themeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTheme" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 等级管理弹窗 -->
|
||||
<el-dialog v-model="levelDialogVisible" :title="`管理等级 - ${currentTheme?.name || ''}`" width="680px">
|
||||
<div class="level-toolbar">
|
||||
<el-button type="primary" size="small" @click="openLevelForm()">新增等级</el-button>
|
||||
</div>
|
||||
<el-table :data="levels" v-loading="levelLoading" stripe size="small">
|
||||
<el-table-column prop="name" label="名称" width="120" />
|
||||
<el-table-column prop="icon" label="图标" width="60" />
|
||||
<el-table-column prop="required_days" label="所需天数" width="100" />
|
||||
<el-table-column prop="sort_order" label="排序" width="60" />
|
||||
<el-table-column label="操作" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openLevelForm(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteLevel(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-divider v-if="showLevelForm" />
|
||||
<el-form v-if="showLevelForm" :model="levelForm" label-width="80px" size="small" class="level-form">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="levelForm.name" placeholder="如:炼体" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标">
|
||||
<el-input v-model="levelForm.icon" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所需天数">
|
||||
<el-input-number v-model="levelForm.required_days" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="levelForm.sort_order" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="showLevelForm = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveLevel" :loading="levelSaving">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
listThemes, createTheme, updateTheme, deleteTheme,
|
||||
listLevels, createLevel, updateLevel, deleteLevel
|
||||
} from '../../api/achievement'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const themes = ref([])
|
||||
const themeDialogVisible = ref(false)
|
||||
const editingTheme = ref(null)
|
||||
const themeForm = ref({ name: '', key: '', icon: '', sort_order: 0, is_active: true })
|
||||
|
||||
const levelDialogVisible = ref(false)
|
||||
const levelLoading = ref(false)
|
||||
const levelSaving = ref(false)
|
||||
const currentTheme = ref(null)
|
||||
const levels = ref([])
|
||||
const showLevelForm = ref(false)
|
||||
const editingLevel = ref(null)
|
||||
const levelForm = ref({ name: '', icon: '', required_days: 0, sort_order: 0 })
|
||||
|
||||
async function loadThemes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listThemes()
|
||||
themes.value = res.data?.themes || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载主题失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openThemeDialog(theme = null) {
|
||||
editingTheme.value = theme
|
||||
if (theme) {
|
||||
themeForm.value = { name: theme.name, key: theme.key, icon: theme.icon, sort_order: theme.sort_order, is_active: theme.is_active }
|
||||
} else {
|
||||
themeForm.value = { name: '', key: '', icon: '', sort_order: 0, is_active: true }
|
||||
}
|
||||
themeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
if (!themeForm.value.name || !themeForm.value.key) {
|
||||
ElMessage.warning('名称和标识必填')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingTheme.value) {
|
||||
await updateTheme(editingTheme.value.id, themeForm.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createTheme(themeForm.value)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
themeDialogVisible.value = false
|
||||
await loadThemes()
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteTheme(theme) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除主题「${theme.name}」?`, '确认')
|
||||
await deleteTheme(theme.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadThemes()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openLevelDialog(theme) {
|
||||
currentTheme.value = theme
|
||||
levelDialogVisible.value = true
|
||||
showLevelForm.value = false
|
||||
await loadLevels(theme.id)
|
||||
}
|
||||
|
||||
async function loadLevels(themeId) {
|
||||
levelLoading.value = true
|
||||
try {
|
||||
const res = await listLevels(themeId)
|
||||
levels.value = res.data?.levels || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载等级失败')
|
||||
} finally {
|
||||
levelLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openLevelForm(level = null) {
|
||||
editingLevel.value = level
|
||||
if (level) {
|
||||
levelForm.value = { name: level.name, icon: level.icon || '', required_days: level.required_days, sort_order: level.sort_order }
|
||||
} else {
|
||||
levelForm.value = { name: '', icon: '', required_days: 0, sort_order: levels.value.length }
|
||||
}
|
||||
showLevelForm.value = true
|
||||
}
|
||||
|
||||
async function saveLevel() {
|
||||
if (!levelForm.value.name) {
|
||||
ElMessage.warning('名称必填')
|
||||
return
|
||||
}
|
||||
levelSaving.value = true
|
||||
try {
|
||||
if (editingLevel.value) {
|
||||
await updateLevel(editingLevel.value.id, levelForm.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createLevel({ ...levelForm.value, theme_id: currentTheme.value.id })
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
showLevelForm.value = false
|
||||
await loadLevels(currentTheme.value.id)
|
||||
await loadThemes()
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
levelSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteLevel(level) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除等级「${level.name}」?`, '确认')
|
||||
await deleteLevel(level.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadLevels(currentTheme.value.id)
|
||||
await loadThemes()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadThemes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.level-preview {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.level-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.level-form {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="dream-icons-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>梦想目标图标管理</span>
|
||||
<el-button type="primary" @click="openDialog()">新增图标</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
|
||||
用户添加梦想目标时,从此处配置的图标中选择。名称和价格由用户自行填写。
|
||||
</el-alert>
|
||||
|
||||
<el-table :data="icons" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column label="图标" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.cover_image && row.cover_image.startsWith('icon:')" style="font-size: 28px">
|
||||
{{ row.cover_image.replace('icon:', '') }}
|
||||
</span>
|
||||
<el-image v-else-if="row.cover_image" :src="row.cover_image" style="width: 44px; height: 44px; border-radius: 8px" fit="cover" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="图标名称" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #999">{{ row.title || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
|
||||
{{ row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openDialog(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="handleDelete(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑图标' : '新增图标'" width="480px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="图标名称">
|
||||
<el-input v-model="form.title" placeholder="备注名(如:耳机、手机)" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Emoji" required>
|
||||
<div class="icon-grid">
|
||||
<div
|
||||
v-for="icon in emojiOptions"
|
||||
:key="icon"
|
||||
class="icon-option"
|
||||
:class="{ active: form.cover_image === `icon:${icon}` }"
|
||||
@click="selectEmoji(icon)"
|
||||
>
|
||||
{{ icon }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="或图片URL">
|
||||
<el-input v-model="form.imageUrl" placeholder="https://... 图片链接(优先于 Emoji)" @input="onImageUrlInput" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预览" v-if="previewImage">
|
||||
<div class="preview-box">
|
||||
<span v-if="previewImage.startsWith('icon:')" style="font-size: 36px">{{ previewImage.replace('icon:', '') }}</span>
|
||||
<el-image v-else :src="previewImage" style="width: 60px; height: 60px; border-radius: 10px" fit="cover" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listDreamPresets, createDreamPreset, updateDreamPreset, deleteDreamPreset } from '../../api/dreamPreset'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const icons = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref(null)
|
||||
|
||||
const emojiOptions = [
|
||||
'🎧', '👟', '📱', '⌚', '🎮', '📷',
|
||||
'💻', '🎸', '🏖️', '🎂', '🎁', '🚲',
|
||||
'🎒', '👜', '🕶️', '🧸', '🏠', '✈️',
|
||||
'🚗', '💎', '🎨', '📚', '🍰', '🌸',
|
||||
]
|
||||
|
||||
const defaultForm = () => ({
|
||||
title: '',
|
||||
cover_image: '',
|
||||
imageUrl: '',
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
})
|
||||
|
||||
const form = ref(defaultForm())
|
||||
|
||||
const previewImage = computed(() => {
|
||||
if (form.value.imageUrl.trim()) return form.value.imageUrl.trim()
|
||||
if (form.value.cover_image) return form.value.cover_image
|
||||
return ''
|
||||
})
|
||||
|
||||
function selectEmoji(emoji) {
|
||||
form.value.cover_image = `icon:${emoji}`
|
||||
form.value.imageUrl = ''
|
||||
}
|
||||
|
||||
function onImageUrlInput() {
|
||||
if (form.value.imageUrl.trim()) {
|
||||
form.value.cover_image = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listDreamPresets()
|
||||
icons.value = res.data?.items || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(row) {
|
||||
if (row) {
|
||||
editingId.value = row.id
|
||||
const isIcon = row.cover_image?.startsWith('icon:')
|
||||
form.value = {
|
||||
title: row.title || '',
|
||||
cover_image: isIcon ? row.cover_image : '',
|
||||
imageUrl: isIcon ? '' : (row.cover_image || ''),
|
||||
sort_order: row.sort_order,
|
||||
is_active: row.is_active,
|
||||
}
|
||||
} else {
|
||||
editingId.value = null
|
||||
form.value = defaultForm()
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const coverImage = form.value.imageUrl.trim() || form.value.cover_image || ''
|
||||
if (!coverImage) {
|
||||
ElMessage.warning('请选择 Emoji 或填入图片链接')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
const data = {
|
||||
title: form.value.title.trim(),
|
||||
cover_image: coverImage,
|
||||
sort_order: form.value.sort_order,
|
||||
is_active: form.value.is_active,
|
||||
}
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await updateDreamPreset(editingId.value, data)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createDreamPreset(data)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchList()
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id) {
|
||||
try {
|
||||
await deleteDreamPreset(id)
|
||||
ElMessage.success('已删除')
|
||||
await fetchList()
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-option {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
background: #f5f7fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.icon-option:hover {
|
||||
border-color: #c6e7d9;
|
||||
}
|
||||
|
||||
.icon-option.active {
|
||||
border-color: #14936d;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
background: #f9fafb;
|
||||
border: 1px dashed #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user