2025-10-04 16:14:54 +08:00
|
|
|
<template>
|
2025-10-06 01:32:16 +08:00
|
|
|
<el-config-provider :locale="zhCn">
|
|
|
|
|
<!-- 主容器 -->
|
|
|
|
|
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
|
|
|
|
|
|
|
|
|
|
<!-- 头部区域 -->
|
|
|
|
|
<div class="flex justify-between items-center mb-6">
|
|
|
|
|
<span class="text-2xl font-bold text-gray-700">文章管理列表</span>
|
|
|
|
|
<el-button type="primary" :icon="Plus" @click="handleCreate">新建文章</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文章表格 -->
|
|
|
|
|
<el-table :data="tableData" style="width: 100%" row-key="id">
|
|
|
|
|
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
|
|
|
|
|
<el-table-column label="封面" width="180">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-image style="width: 120px; height: 70px; border-radius: 6px;" :src="scope.row.cover" :preview-src-list="[scope.row.cover]" fit="cover" :preview-teleported="true" hide-on-click-modal>
|
|
|
|
|
<template #error>
|
|
|
|
|
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">加载失败</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-image>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<span class="font-semibold">{{ scope.row.title }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="文章内容" min-width="300">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<div class="content-preview" :title="scope.row.content">
|
|
|
|
|
{{ scope.row.content.length > 100 ? `${scope.row.content.slice(0, 100)}...` : scope.row.content }}
|
|
|
|
|
</div>
|
|
|
|
|
<el-button size="mini" type="text" class="mt-1 text-blue-600" @click="handleViewContent(scope.row)">查看详情</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="topic" label="主题" width="120"></el-table-column>
|
|
|
|
|
<el-table-column prop="excerpt" label="摘要" min-width="250" show-overflow-tooltip></el-table-column>
|
|
|
|
|
<el-table-column prop="create_at" label="创建时间" width="180" sortable></el-table-column>
|
|
|
|
|
<el-table-column prop="update_at" label="更新时间" width="180" sortable></el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<el-button size="small" type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
|
|
|
|
|
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<!-- Content 详情弹窗 -->
|
|
|
|
|
<el-dialog v-model="contentDialogVisible" title="文章内容详情" :width="`800px`" :before-close="handleCloseDialog">
|
|
|
|
|
<el-card class="p-4">
|
|
|
|
|
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ currentArticle.title }}</h3>
|
|
|
|
|
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap">
|
|
|
|
|
{{ currentArticle.content || "当前文章暂无内容" }}
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 分页器容器 -->
|
|
|
|
|
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
|
|
|
|
|
<el-pagination class="custom-pagination" background layout="total, sizes, prev, pager, next, jumper" :total="total" v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- ==================================================== -->
|
|
|
|
|
<!-- 🔥 文章编辑/新建抽屉 (已修改) 🔥 -->
|
|
|
|
|
<!-- ==================================================== -->
|
|
|
|
|
<el-drawer v-model="drawerVisible" :title="isEditMode ? '编辑文章' : '发表新文章'" direction="rtl" size="60%" :before-close="handleDrawerClose" destroy-on-close>
|
|
|
|
|
<div class="publish-form-container">
|
|
|
|
|
<!-- 1. 文章标题输入 -->
|
|
|
|
|
<div class="form-group title-group">
|
|
|
|
|
<label class="form-label">文章标题</label>
|
|
|
|
|
<el-input v-model="form.title" placeholder="请输入文章标题" clearable :disabled="isSubmitting" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 2. 专题选择 (仅在新建时显示) -->
|
|
|
|
|
<div v-if="!isEditMode" class="form-group topic-group">
|
|
|
|
|
<label class="form-label">发表到专题</label>
|
|
|
|
|
<el-radio-group v-model="form.topic" :disabled="isSubmitting">
|
|
|
|
|
<el-radio label="news">新闻</el-radio>
|
|
|
|
|
<el-radio label="cases">案例资源</el-radio>
|
|
|
|
|
<el-radio label="community">社区服务</el-radio>
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 3. 封面图上传 -->
|
|
|
|
|
<div class="form-group cover-group">
|
|
|
|
|
<label class="form-label">文章封面</label>
|
|
|
|
|
<el-upload action="http://localhost:8080/api/upload/cover" name="image" :show-file-list="false" :on-success="handleCoverSuccess" :before-upload="beforeCoverUpload" :on-error="handleCoverError" :disabled="isSubmitting">
|
|
|
|
|
<img v-if="form.cover" :src="form.cover" class="cover-preview" alt="封面"/>
|
|
|
|
|
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
|
|
|
|
|
</el-upload>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 4. 文章摘要编辑 -->
|
|
|
|
|
<div class="form-group excerpt-group">
|
|
|
|
|
<label class="form-label">文章摘要</label>
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="form.excerpt"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="4"
|
|
|
|
|
placeholder="请输入文章摘要(可选,若不填则自动从正文截取)"
|
|
|
|
|
clearable
|
|
|
|
|
:disabled="isSubmitting"
|
2025-10-05 02:14:37 +08:00
|
|
|
/>
|
2025-10-06 01:32:16 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 5. 文章内容富文本编辑 -->
|
|
|
|
|
<div class="form-group content-group">
|
|
|
|
|
<label class="form-label">文章内容</label>
|
|
|
|
|
<div ref="editorRef" class="quill-editor"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-05 02:14:37 +08:00
|
|
|
<template #footer>
|
2025-10-06 01:32:16 +08:00
|
|
|
<div style="flex: auto">
|
|
|
|
|
<el-button @click="handleDrawerClose">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="submitArticle" :loading="isSubmitting">
|
|
|
|
|
{{ isEditMode ? '更新文章' : '发布文章' }}
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
2025-10-05 02:14:37 +08:00
|
|
|
</template>
|
2025-10-06 01:32:16 +08:00
|
|
|
</el-drawer>
|
|
|
|
|
|
|
|
|
|
</el-config-provider>
|
2025-10-04 16:14:54 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2025-10-06 01:32:16 +08:00
|
|
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
|
|
|
|
import { ElMessage, ElMessageBox, ElDialog, ElConfigProvider, ElDrawer, ElInput, ElRadioGroup, ElRadio, ElUpload } from 'element-plus';
|
|
|
|
|
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
|
|
|
|
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
|
|
|
|
|
import Quill from 'quill';
|
|
|
|
|
import 'quill/dist/quill.snow.css';
|
|
|
|
|
import type { UploadProps } from 'element-plus';
|
2025-10-05 02:14:37 +08:00
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
// --- 类型定义 ---
|
|
|
|
|
interface Article {
|
|
|
|
|
id: number;
|
|
|
|
|
title: string;
|
|
|
|
|
content: string;
|
|
|
|
|
cover: string;
|
|
|
|
|
create_at: string;
|
|
|
|
|
update_at: string;
|
|
|
|
|
is_delete: number;
|
|
|
|
|
topic: string;
|
|
|
|
|
excerpt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ListArticleReq {
|
|
|
|
|
page: number;
|
|
|
|
|
size: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- API 基地址 ---
|
|
|
|
|
const API_BASE_URL = 'http://localhost:8080/api';
|
|
|
|
|
|
|
|
|
|
// =================================================================
|
|
|
|
|
// 列表页相关状态与逻辑
|
|
|
|
|
// =================================================================
|
|
|
|
|
const loading = ref(true);
|
|
|
|
|
const tableData = ref<Article[]>([]);
|
|
|
|
|
const currentPage = ref(1);
|
|
|
|
|
const pageSize = ref(10);
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
const contentDialogVisible = ref(false);
|
|
|
|
|
const currentArticle = ref<Article>({ id: 0, title: '', content: '', cover: '', create_at: '', update_at: '', is_delete: 0, topic: '', excerpt: '' });
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const reqData: ListArticleReq = { page: currentPage.value, size: pageSize.value };
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/articles/getarticle`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(reqData)
|
|
|
|
|
});
|
|
|
|
|
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
tableData.value = data.Article_list || [];
|
|
|
|
|
total.value = data.total || 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[ERROR] 获取文章列表失败:', error);
|
|
|
|
|
ElMessage.error('获取文章列表失败,请检查接口或网络!');
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(fetchData);
|
|
|
|
|
|
|
|
|
|
const handleViewContent = (row: Article) => {
|
|
|
|
|
currentArticle.value = { ...row };
|
|
|
|
|
contentDialogVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCloseDialog = () => {
|
|
|
|
|
contentDialogVisible.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = (row: Article) => {
|
|
|
|
|
ElMessageBox.confirm(`确定要删除文章《${row.title}》吗?此操作无法撤销!`, '警告', { confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' })
|
|
|
|
|
.then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/articles/${row.id}`, { method: 'DELETE' });
|
|
|
|
|
if (!response.ok) throw new Error('删除失败');
|
|
|
|
|
ElMessage.success('删除成功!');
|
|
|
|
|
fetchData();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[ERROR] 删除文章失败:', error);
|
|
|
|
|
ElMessage.error('删除文章失败,请重试!');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => ElMessage.info('已取消删除'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSizeChange = (val: number) => {
|
|
|
|
|
pageSize.value = val;
|
|
|
|
|
currentPage.value = 1;
|
|
|
|
|
fetchData();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCurrentChange = (val: number) => {
|
|
|
|
|
currentPage.value = val;
|
|
|
|
|
fetchData();
|
|
|
|
|
};
|
2025-10-05 02:14:37 +08:00
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
// =================================================================
|
|
|
|
|
// 抽屉编辑/新建相关状态与逻辑
|
|
|
|
|
// =================================================================
|
|
|
|
|
const drawerVisible = ref(false);
|
|
|
|
|
const isEditMode = ref(false);
|
|
|
|
|
const isSubmitting = ref(false);
|
|
|
|
|
|
|
|
|
|
const defaultFormState = () => ({
|
|
|
|
|
id: null as number | null,
|
2025-10-05 02:14:37 +08:00
|
|
|
title: '',
|
2025-10-06 01:32:16 +08:00
|
|
|
topic: 'news',
|
2025-10-05 02:14:37 +08:00
|
|
|
cover: '',
|
|
|
|
|
content: '',
|
2025-10-06 01:32:16 +08:00
|
|
|
excerpt: '',
|
2025-10-05 02:14:37 +08:00
|
|
|
});
|
2025-10-06 01:32:16 +08:00
|
|
|
const form = ref(defaultFormState());
|
|
|
|
|
|
|
|
|
|
// --- 富文本编辑器 ---
|
|
|
|
|
const editorRef = ref<HTMLDivElement | null>(null);
|
|
|
|
|
let quillInstance: Quill | null = null;
|
2025-10-05 02:14:37 +08:00
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
const initQuillEditor = () => {
|
|
|
|
|
if (editorRef.value && !quillInstance) {
|
|
|
|
|
quillInstance = new Quill(editorRef.value, {
|
|
|
|
|
theme: 'snow',
|
|
|
|
|
modules: { toolbar: [['bold', 'italic', 'underline'], ['link', 'image']] },
|
|
|
|
|
placeholder: '请开始创作你的文章...'
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-05 02:14:37 +08:00
|
|
|
};
|
|
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
watch(drawerVisible, (visible) => {
|
|
|
|
|
if (visible) {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
initQuillEditor();
|
|
|
|
|
if (isEditMode.value) {
|
|
|
|
|
if (quillInstance) {
|
|
|
|
|
quillInstance.root.innerHTML = form.value.content;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (quillInstance) {
|
|
|
|
|
quillInstance.setContents([]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 🔥 关键修复:当抽屉关闭时,将实例重置为 null。
|
|
|
|
|
// `destroy-on-close` 属性会处理 DOM 的销毁。
|
|
|
|
|
quillInstance = null;
|
2025-10-05 02:14:37 +08:00
|
|
|
}
|
2025-10-06 01:32:16 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --- 操作处理 ---
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
form.value = defaultFormState();
|
|
|
|
|
if (quillInstance) {
|
|
|
|
|
quillInstance.setContents([]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreate = () => {
|
|
|
|
|
isEditMode.value = false;
|
|
|
|
|
resetForm();
|
|
|
|
|
drawerVisible.value = true;
|
2025-10-05 02:14:37 +08:00
|
|
|
};
|
|
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
const handleEdit = (row: Article) => {
|
|
|
|
|
isEditMode.value = true;
|
|
|
|
|
form.value = {
|
|
|
|
|
id: row.id,
|
|
|
|
|
title: row.title,
|
|
|
|
|
topic: row.topic,
|
|
|
|
|
cover: row.cover,
|
|
|
|
|
excerpt: row.excerpt,
|
|
|
|
|
content: row.content,
|
|
|
|
|
};
|
|
|
|
|
drawerVisible.value = true;
|
2025-10-05 02:14:37 +08:00
|
|
|
};
|
|
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
const handleDrawerClose = () => {
|
|
|
|
|
drawerVisible.value = false;
|
2025-10-05 02:14:37 +08:00
|
|
|
};
|
|
|
|
|
|
2025-10-06 01:32:16 +08:00
|
|
|
const submitArticle = async () => {
|
|
|
|
|
if (!form.value.title) {
|
|
|
|
|
ElMessage.warning('请输入文章标题');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSubmitting.value = true;
|
|
|
|
|
const contentHTML = quillInstance?.root.innerHTML || '';
|
|
|
|
|
const excerpt = form.value.excerpt.trim() || quillInstance?.getText().trim().slice(0, 150) || '';
|
|
|
|
|
|
|
|
|
|
const submitData = {
|
|
|
|
|
...form.value,
|
|
|
|
|
content: contentHTML,
|
|
|
|
|
excerpt: excerpt,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = isEditMode.value ? `${API_BASE_URL}/articles/${form.value.id}` : `${API_BASE_URL}/articles`;
|
|
|
|
|
const method = isEditMode.value ? 'PUT' : 'POST';
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: method,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(submitData),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('提交失败');
|
|
|
|
|
|
|
|
|
|
ElMessage.success(isEditMode.value ? '文章更新成功!' : '文章发布成功!');
|
|
|
|
|
drawerVisible.value = false;
|
|
|
|
|
fetchData(); // 刷新列表
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('提交文章失败!');
|
|
|
|
|
} finally {
|
|
|
|
|
isSubmitting.value = false;
|
|
|
|
|
}
|
2025-10-05 02:14:37 +08:00
|
|
|
};
|
2025-10-06 01:32:16 +08:00
|
|
|
|
|
|
|
|
// --- 封面上传处理 ---
|
|
|
|
|
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
|
|
|
|
|
const ossUrl = response.data?.url;
|
|
|
|
|
if (ossUrl) {
|
|
|
|
|
form.value.cover = ossUrl;
|
|
|
|
|
ElMessage.success('封面上传成功');
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error('封面上传失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|
|
|
|
const isLt5M = rawFile.size / 1024 / 1024 < 5;
|
|
|
|
|
if (!isLt5M) {
|
|
|
|
|
ElMessage.error('图片大小不能超过 5MB!');
|
|
|
|
|
}
|
|
|
|
|
return isLt5M;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCoverError = () => {
|
|
|
|
|
ElMessage.error('封面上传失败');
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-04 16:14:54 +08:00
|
|
|
</script>
|
2025-10-05 02:14:37 +08:00
|
|
|
|
|
|
|
|
<style scoped>
|
2025-10-06 01:32:16 +08:00
|
|
|
/* 表格样式 */
|
|
|
|
|
.el-table .el-table__cell { vertical-align: middle; }
|
|
|
|
|
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
|
|
|
|
|
.action-buttons .el-button { margin-right: 8px; }
|
|
|
|
|
.content-preview { color: #666; line-height: 1.5; word-break: break-all; }
|
|
|
|
|
.content-full { min-height: 300px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
|
|
|
|
|
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
|
|
|
|
|
|
|
|
|
|
/* 分页器样式 */
|
|
|
|
|
.custom-pagination { justify-content: flex-end !important; }
|
|
|
|
|
.custom-pagination .el-pagination__total,
|
|
|
|
|
.custom-pagination .el-pagination__sizes,
|
|
|
|
|
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
|
|
|
|
|
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
|
|
|
|
|
|
|
|
|
|
/* 抽屉内表单样式 */
|
|
|
|
|
.publish-form-container { padding: 0 20px; }
|
|
|
|
|
.form-group { margin-bottom: 24px; }
|
|
|
|
|
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
|
|
|
|
|
.quill-editor { height: 350px; border-radius: 4px; border: 1px solid #dcdfe6; }
|
|
|
|
|
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
|
|
|
|
|
.cover-uploader .el-upload:hover { border-color: #409EFF; }
|
|
|
|
|
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; }
|
|
|
|
|
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; }
|
|
|
|
|
</style>
|
|
|
|
|
|