|
- {% extends "./layout.html" %}
-
- {% block title %}内容管理{% endblock %}
-
- {% block css %}
- <link rel="stylesheet" href="/static/lib/wangeditor/css/style.css">
- <style>
- .editor-toolbar { border: 1px solid #dcdfe6; border-bottom: none; border-radius: 4px 4px 0 0; }
- .editor-container { border: 1px solid #dcdfe6; border-radius: 0 0 4px 4px; min-height: 300px; }
- /* 手机模拟框 */
- .phone-frame {
- width: 375px; height: 720px; margin: 0 auto;
- border: 8px solid #1a1a1a; border-radius: 40px;
- background: #f5f5f5; position: relative; overflow: hidden;
- box-shadow: 0 8px 32px rgba(0,0,0,0.15);
- }
- .phone-notch {
- width: 120px; height: 24px; background: #1a1a1a;
- border-radius: 0 0 16px 16px; margin: 0 auto;
- position: relative; z-index: 2;
- }
- .phone-screen {
- height: calc(100% - 24px); overflow-y: auto; background: #fff;
- }
- .phone-screen::-webkit-scrollbar { width: 0; }
- /* 预览内容样式 */
- .preview-header {
- padding: 16px 16px 12px; border-bottom: 1px solid #f0f0f0;
- }
- .preview-title { font-size: 18px; font-weight: 600; color: #303133; line-height: 1.5; }
- .preview-body { padding: 16px; font-size: 14px; color: #606266; line-height: 1.8; }
- .preview-body img { max-width: 100% !important; height: auto !important; }
- .preview-body p { margin-bottom: 12px; }
- .preview-body p:last-child { margin-bottom: 0; }
- </style>
- {% endblock %}
-
- {% block content %}
- <div id="contentApp" v-cloak>
- <!-- 搜索栏 -->
- <el-card shadow="never" class="mb-4">
- <el-form :inline="true" @submit.prevent="loadList()" class="flex items-center flex-wrap gap-2">
- <el-form-item label="关键字" class="!mb-0">
- <el-input v-model="keyword" placeholder="标题/标识" clearable style="width:180px;" />
- </el-form-item>
- <el-form-item label="状态" class="!mb-0">
- <el-select v-model="statusFilter" placeholder="全部" clearable style="width:120px;">
- <el-option label="启用" :value="1"></el-option>
- <el-option label="禁用" :value="0"></el-option>
- </el-select>
- </el-form-item>
- <el-form-item class="!mb-0">
- <el-button type="primary" @click="loadList()">搜索</el-button>
- <el-button @click="resetFilter">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
-
- <!-- 列表 -->
- <el-card shadow="never">
- <template #header>
- <el-button v-if="canAdd" type="primary" @click="showAdd">+ 新增</el-button>
- </template>
-
- <el-table :data="tableData" v-loading="loading" stripe border>
- <el-table-column prop="content_key" label="内容标识" width="180"></el-table-column>
- <el-table-column prop="title" label="标题" min-width="200"></el-table-column>
- <el-table-column prop="remark" label="备注" min-width="160">
- <template #default="{ row }">${ row.remark || '-' }</template>
- </el-table-column>
- <el-table-column label="状态" width="80" align="center">
- <template #default="{ row }">
- <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">${ row.status === 1 ? '启用' : '禁用' }</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="sort" label="排序" width="80" align="center"></el-table-column>
- <el-table-column prop="update_time" label="更新时间" width="170">
- <template #default="{ row }">${ row.update_time || row.create_time || '-' }</template>
- </el-table-column>
- <el-table-column label="操作" width="200" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="showPreview(row)">预览</el-button>
- <el-button v-if="canEdit" type="primary" link @click="showEdit(row)">编辑</el-button>
- <el-button v-if="canDelete" type="danger" link @click="handleDelete(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="flex justify-end mt-4">
- <el-pagination
- v-model:current-page="pagination.page"
- :page-size="pagination.pageSize"
- :total="pagination.total"
- layout="total, prev, pager, next"
- @current-change="loadList"
- />
- </div>
- </el-card>
-
- <!-- 新增/编辑弹窗 -->
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="1000px" top="5vh" destroy-on-close draggable :close-on-click-modal="false" @closed="onDialogClosed">
- <div style="max-height: calc(90vh - 160px); overflow-y: auto; padding-right: 24px;">
- <el-form :model="form" label-width="90px">
- <el-form-item label="内容标识" required>
- <el-input v-model="form.content_key" placeholder="如 index_content" :disabled="!!form.id" />
- <div v-if="!form.id" class="text-xs text-gray-400 mt-1">创建后不可修改</div>
- </el-form-item>
- <el-form-item label="标题" required>
- <el-input v-model="form.title" placeholder="请输入标题" />
- </el-form-item>
- <el-form-item label="备注">
- <el-input v-model="form.remark" placeholder="请输入备注" />
- </el-form-item>
- <el-form-item label="富文本内容">
- <div style="width:780px; max-width:100%;">
- <div id="toolbar-container" class="editor-toolbar"></div>
- <div id="editor-container" class="editor-container"></div>
- </div>
- </el-form-item>
- <el-form-item label="排序">
- <el-input-number v-model="form.sort" :min="0" :precision="0" :step="1" />
- </el-form-item>
- <el-form-item label="状态">
- <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
- </el-form-item>
- </el-form>
- </div>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleSave" :loading="saving">确定</el-button>
- </template>
- </el-dialog>
-
- <!-- 预览抽屉 -->
- <el-drawer v-model="previewVisible" title="内容预览" size="480px" destroy-on-close>
- <div class="phone-frame">
- <div class="phone-notch"></div>
- <div class="phone-screen">
- <div class="preview-header">
- <div class="preview-title">${ previewData.title }</div>
- </div>
- <div class="preview-body" v-html="previewData.content"></div>
- </div>
- </div>
- </el-drawer>
- </div>
- {% endblock %}
-
- {% block js %}
- <script src="/static/lib/wangeditor/index.js"></script>
- <script>
- const canAdd = {{ canAdd | dump | safe }};
- const canEdit = {{ canEdit | dump | safe }};
- const canDelete = {{ canDelete | dump | safe }};
-
- const { createApp, ref, reactive, onMounted, nextTick } = Vue;
-
- const app = createApp({
- delimiters: ['${', '}'],
- setup() {
- const keyword = ref('');
- const statusFilter = ref('');
- const loading = ref(false);
- const tableData = ref([]);
- const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
-
- const dialogVisible = ref(false);
- const dialogTitle = ref('新增内容');
- const saving = ref(false);
- const form = reactive({ id: null, content_key: '', title: '', remark: '', content: '', sort: 0, status: 1 });
-
- // 预览
- const previewVisible = ref(false);
- const previewData = reactive({ title: '', content: '' });
-
- let editorInstance = null;
-
- async function loadList(page) {
- if (typeof page === 'number') pagination.page = page;
- loading.value = true;
- try {
- const params = new URLSearchParams({
- page: pagination.page, pageSize: pagination.pageSize,
- keyword: keyword.value,
- status: statusFilter.value !== '' ? statusFilter.value : ''
- });
- const res = await fetch('/admin/content/list?' + params).then(r => r.json());
- if (res.code === 0) {
- tableData.value = res.data.data || [];
- pagination.total = res.data.count || 0;
- } else {
- ElementPlus.ElMessage.error(res.msg || '加载失败');
- }
- } finally { loading.value = false; }
- }
-
- function resetFilter() {
- keyword.value = ''; statusFilter.value = ''; pagination.page = 1; loadList();
- }
-
- function showAdd() {
- dialogTitle.value = '新增内容';
- Object.assign(form, { id: null, content_key: '', title: '', remark: '', content: '', sort: 0, status: 1 });
- dialogVisible.value = true;
- nextTick(() => initEditor(''));
- }
-
- async function showEdit(row) {
- dialogTitle.value = '编辑内容';
- const res = await fetch('/admin/content/detail?id=' + row.id).then(r => r.json());
- if (res.code !== 0) { ElementPlus.ElMessage.error(res.msg || '获取详情失败'); return; }
- const item = res.data;
- Object.assign(form, {
- id: item.id, content_key: item.content_key, title: item.title,
- remark: item.remark || '', content: item.content || '',
- sort: item.sort || 0, status: item.status
- });
- dialogVisible.value = true;
- nextTick(() => initEditor(form.content));
- }
-
- async function showPreview(row) {
- const res = await fetch('/admin/content/detail?id=' + row.id).then(r => r.json());
- if (res.code !== 0) { ElementPlus.ElMessage.error(res.msg || '获取详情失败'); return; }
- previewData.title = res.data.title || '';
- previewData.content = res.data.content || '';
- previewVisible.value = true;
- }
-
- function initEditor(html) {
- destroyEditor();
- const { createEditor, createToolbar } = window.wangEditor;
- editorInstance = createEditor({
- selector: '#editor-container',
- html: html || '',
- config: {
- placeholder: '请输入内容...',
- MENU_CONF: {
- uploadImage: {
- server: '/admin/upload', fieldName: 'file', maxFileSize: 5 * 1024 * 1024,
- customInsert(res, insertFn) {
- if (res.code === 0 && res.data && res.data.url) { insertFn(res.data.url, res.data.name || '', ''); }
- else { ElementPlus.ElMessage.error(res.msg || '上传失败'); }
- }
- }
- },
- onChange(editor) { form.content = editor.getHtml(); }
- }
- });
- createToolbar({ editor: editorInstance, selector: '#toolbar-container', config: {} });
- }
-
- function destroyEditor() {
- if (editorInstance) { try { editorInstance.destroy(); } catch (e) {} editorInstance = null; }
- var toolbar = document.getElementById('toolbar-container');
- var editor = document.getElementById('editor-container');
- if (toolbar) toolbar.innerHTML = '';
- if (editor) editor.innerHTML = '';
- }
-
- function onDialogClosed() { destroyEditor(); }
-
- async function handleSave() {
- if (!form.content_key.trim()) { ElementPlus.ElMessage.warning('内容标识不能为空'); return; }
- if (!form.title.trim()) { ElementPlus.ElMessage.warning('标题不能为空'); return; }
- saving.value = true;
- try {
- const url = form.id ? '/admin/content/edit' : '/admin/content/add';
- const body = { title: form.title, remark: form.remark, content: form.content, sort: form.sort, status: form.status };
- if (form.id) { body.id = form.id; } else { body.content_key = form.content_key; }
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
- if (res.code === 0) { ElementPlus.ElMessage.success('保存成功'); dialogVisible.value = false; loadList(); }
- else { ElementPlus.ElMessage.error(res.msg || '保存失败'); }
- } finally { saving.value = false; }
- }
-
- async function handleDelete(row) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定要删除「' + row.title + '」吗?', '提示', { type: 'warning' });
- const res = await fetch('/admin/content/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: row.id }) }).then(r => r.json());
- if (res.code === 0) { ElementPlus.ElMessage.success('删除成功'); loadList(); }
- else { ElementPlus.ElMessage.error(res.msg || '删除失败'); }
- } catch {}
- }
-
- onMounted(() => loadList());
-
- return {
- keyword, statusFilter, loading, tableData, pagination,
- dialogVisible, dialogTitle, saving, form,
- previewVisible, previewData,
- canAdd, canEdit, canDelete,
- loadList, resetFilter, showAdd, showEdit, showPreview, handleSave, handleDelete, onDialogClosed
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#contentApp');
- </script>
- {% endblock %}
|