|
|
- <template>
- <div class="course-page-editor">
- <!-- 顶部导航栏 -->
- <div class="editor-header">
- <div class="header-left">
- <a-button type="text" @click="goBack" class="back-btn">
- <Icon icon="ant-design:arrow-left-outlined" />
- 返回课程列表
- </a-button>
- <a-divider type="vertical" />
- </div>
- <div class="header-right">
- <a-button @click="handleAddPage" class="add-btn">
- <Icon icon="ant-design:plus-outlined" />
- 新增页面
- </a-button>
- <a-button type="primary" @click="handleSave" :loading="saving">
- <Icon icon="ant-design:save-outlined" />
- 保存
- </a-button>
- </div>
- </div>
-
- <!-- 上方:课程页面列表(横向滑动) -->
- <div class="page-list-container">
- <div class="page-list-scroll">
- <draggable
- v-model="pageList"
- class="page-list"
- item-key="id"
- handle=".drag-handle"
- :animation="200"
- @start="onPageDragStart"
- @end="onPageDragEnd"
- >
- <template #item="{ element: page, index }">
- <div :class="['page-item', { active: currentPageId === page.id }]" @click="selectPage(page)">
- <!-- 拖拽手柄 -->
- <div class="drag-handle">
- <Icon icon="ant-design:drag-outlined" />
- </div>
- <div class="page-info">
- <div class="page-title">{{ page.title || `${index + 1}` }}</div>
- <div class="page-meta">
- <span class="page-type">{{ getPageTypeName(page.type) }}</span>
- </div>
- </div>
- <div class="page-actions">
- <a-button size="small" type="text" @click.stop="editPage(page)">
- <Icon icon="ant-design:edit-outlined" />
- </a-button>
- <a-button size="small" type="text" danger @click.stop="deletePage(page)">
- <Icon icon="ant-design:delete-outlined" />
- </a-button>
- </div>
- </div>
- </template>
- </draggable>
- <!-- 空状态 -->
- <div v-if="pageList.length === 0" class="empty-page-list">
- <Icon icon="ant-design:file-add-outlined" class="empty-icon" />
- <p>暂无页面,点击右上角"新增页面"开始创建</p>
- </div>
- </div>
- </div>
-
- <!-- 下方:主体编辑区域(左右分栏) -->
- <div class="editor-main">
- <!-- 左侧:内容编辑区域 -->
- <div class="editor-left">
- <div class="editor-panel">
- <div class="panel-content">
- <a-form :model="currentPage" layout="vertical" class="page-form">
-
- <!-- 内容编辑区域 -->
- <div class="content-editor-section">
- <div class="section-header">
- <span>页面内容</span>
- <a-button-group size="small">
- <a-button @click="addTextContent">
- <Icon icon="ant-design:font-size-outlined" />
- 文本
- </a-button>
- <a-button @click="addImageContent">
- <Icon icon="ant-design:picture-outlined" />
- 图片
- </a-button>
- <a-button @click="addVideoContent">
- <Icon icon="ant-design:video-camera-outlined" />
- 视频
- </a-button>
- </a-button-group>
- </div>
-
- <!-- 内容组件列表 -->
- <div class="content-components">
- <draggable
- v-model="contentComponents"
- item-key="id"
- handle=".drag-handle"
- animation="200"
- class="draggable-list"
- @start="onDragStart"
- @end="onDragEnd"
- >
- <template #item="{ element: component, index }">
- <div class="content-component">
- <!-- 文本组件 -->
- <div v-if="component.type === 'text'" class="text-component">
- <div class="component-header">
- <div class="drag-handle">
- <Icon icon="ant-design:drag-outlined" />
- </div>
- <Icon icon="ant-design:font-size-outlined" />
- <span>文本内容</span>
- <a-button size="small" type="text" danger @click="removeComponent(index)">
- <Icon icon="ant-design:delete-outlined" />
- </a-button>
- </div>
- <!-- 语言选择 -->
- <div class="language-selector">
- <a-radio-group v-model:value="component.language" size="small">
- <a-radio value="zh">中文</a-radio>
- <a-radio value="en">英文</a-radio>
- </a-radio-group>
- </div>
- <a-textarea v-model:value="component.content" :rows="4" placeholder="请输入文本内容" />
- </div>
-
- <!-- 图片组件 -->
- <div v-else-if="component.type === 'image'" class="image-component">
- <div class="component-header">
- <div class="drag-handle">
- <Icon icon="ant-design:drag-outlined" />
- </div>
- <Icon icon="ant-design:picture-outlined" />
- <span>图片内容</span>
- <a-button size="small" type="text" danger @click="removeComponent(index)">
- <Icon icon="ant-design:delete-outlined" />
- </a-button>
- </div>
- <JImageUpload
- v-model:value="component.imageUrl"
- :fileMax="1"
- listType="picture-card"
- text="上传图片"
- bizPath="course"
- :accept="['image/*']"
- @change="handleImageChange($event, component)"
- />
- <a-input v-model:value="component.alt" placeholder="图片描述(可选)" style="margin-top: 8px;" />
- </div>
-
- <!-- 视频组件 -->
- <div v-else-if="component.type === 'video'" class="video-component">
- <div class="component-header">
- <div class="drag-handle">
- <Icon icon="ant-design:drag-outlined" />
- </div>
- <Icon icon="ant-design:video-camera-outlined" />
- <span>视频内容</span>
- <a-button size="small" type="text" danger @click="removeComponent(index)">
- <Icon icon="ant-design:delete-outlined" />
- </a-button>
- </div>
- <JUpload
- v-model:value="component.url"
- bizPath="course"
- text="上传视频"
- @change="handleVideoChange($event, component)"
- style="margin-top: 8px;"
- />
- <div style="margin-top: 12px;">
- <div style="margin-bottom: 8px; font-size: 14px; color: #666;">视频封面</div>
- <JImageUpload
- v-model:value="component.coverUrl"
- :fileMax="1"
- listType="picture-card"
- text="上传封面"
- bizPath="course"
- :accept="['image/*']"
- @change="handleVideoCoverChange($event, component)"
- />
- </div>
- </div>
- </div>
- </template>
- </draggable>
-
- <!-- 空状态 -->
- <div v-if="contentComponents.length === 0" class="empty-content">
- <Icon icon="ant-design:plus-circle-outlined" class="empty-icon" />
- <p>点击上方按钮添加内容组件</p>
- </div>
- </div>
- </div>
- </a-form>
- </div>
- </div>
- </div>
-
- <!-- 右侧:设置和预览区域 -->
- <div class="editor-right">
- <!-- 设置和预览区域(左右布局) -->
- <div class="settings-preview-container">
- <!-- 左侧:页面设置 -->
- <div class="settings-section">
- <div class="panel-header">
- <span>页面设置</span>
- </div>
- <div class="panel-content">
- <a-form :model="currentPage" layout="vertical" class="settings-form">
- <a-form-item label="页面标题" name="title">
- <a-input v-model:value="currentPage.title" placeholder="请输入页面标题" />
- </a-form-item>
- <a-form-item label="页面类型" name="type">
- <a-select v-model:value="currentPage.type" placeholder="选择类型">
- <a-select-option
- v-for="option in pageTypeOptions"
- :key="option.value"
- :value="option.value"
- >
- {{ option.label }}
- </a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="付费" name="pay">
- <a-switch v-model:checked="payChecked" checked-children="是" un-checked-children="否" />
- </a-form-item>
- <a-form-item label="上架" name="status">
- <a-switch v-model:checked="statusChecked" checked-children="是" un-checked-children="否" />
- </a-form-item>
- </a-form>
- </div>
- </div>
-
- <!-- 右侧:手机预览区域 -->
- <div class="mobile-preview-section">
- <div class="panel-header">
- <span>手机预览<span style="font-size: 12px; color: #999;">(该预览经供参考,请以实际手机为准)</span></span>
- <a-button size="small" @click="refreshPreview">
- <Icon icon="ant-design:reload-outlined" />
- </a-button>
- </div>
- <div class="panel-content">
- <div class="mobile-preview-container">
- <div class="mobile-screen">
- <div class="preview-content">
- <div v-if="contentComponents.length === 0" class="empty-preview">
- <p>暂无内容,请在左侧添加内容组件</p>
- </div>
- <div v-else class="content-preview">
- <div v-for="(component, index) in contentComponents" :key="index" class="preview-component">
- <!-- 文本预览 -->
- <div v-if="component.type === 'text'" class="text-preview" :class="{ 'text-english': component.language === 'en' }">
- <div class="text-language-tag">{{ component.language === 'en' ? 'EN' : '中' }}</div>
- <pre>{{ component.content || '文本内容...' }}</pre>
- </div>
- <!-- 图片预览 -->
- <div v-else-if="component.type === 'image'" class="image-preview">
- <img v-if="component.imageUrl" :src="component.imageUrl" :alt="component.alt" />
- <div v-else class="placeholder-image">图片预览</div>
- <p v-if="component.alt" class="image-caption">{{ component.alt }}</p>
- </div>
- <!-- 视频预览 -->
- <div v-else-if="component.type === 'video'" class="video-preview">
- <div v-if="component.url" class="video-container">
- <video :src="component.url" :poster="component.coverUrl" controls></video>
- </div>
- <div v-else class="placeholder-video">
- <div v-if="component.coverUrl" class="video-cover-preview">
- <img :src="component.coverUrl" alt="视频封面" />
- <div class="play-icon">▶</div>
- </div>
- <div v-else>视频预览</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
-
- <script lang="ts" name="appletCoursePage-appletCoursePage" setup>
- import { ref, reactive, computed, unref, onMounted, watch } from 'vue';
- import { useRoute, useRouter } from 'vue-router';
- import { useMessage } from '/@/hooks/web/useMessage';
- import { Icon } from '/@/components/Icon';
- import { JImageUpload, JUpload } from '/@/components/Form';
- import { saveOrUpdate, getById, list, deleteOne, add, edit } from './AppletCoursePage.api';
- import { initDictOptions } from '/@/utils/dict/JDictSelectUtil';
- import draggable from 'vuedraggable';
-
- const route = useRoute();
- const router = useRouter();
- const { createMessage } = useMessage();
-
- // 课程页面列表
- const pageList = ref([]);
- const currentPageId = ref('');
-
- // 页面类型字典选项
- const pageTypeOptions = ref([]);
-
- // 当前编辑的页面数据
- const currentPage = reactive({
- id: '',
- courseId: '',
- title: '',
- type: '0',
- sort: 0,
- pay: 'N',
- status: 'Y',
- });
-
- // 内容组件列表
- const contentComponents = ref([]);
-
- // 计算属性:处理付费开关的Y/N字符串与布尔值转换
- const payChecked = computed({
- get: () => currentPage.pay === 'Y',
- set: (value: boolean) => {
- currentPage.pay = value ? 'Y' : 'N';
- }
- });
-
- // 计算属性:处理上架开关的Y/N字符串与布尔值转换
- const statusChecked = computed({
- get: () => currentPage.status === 'Y',
- set: (value: boolean) => {
- currentPage.status = value ? 'Y' : 'N';
- }
- });
-
- // 保存状态
- const saving = ref(false);
-
- onMounted(async () => {
- // 获取课程ID
- if (route.query.courseId) {
- currentPage.courseId = route.query.courseId as string;
- }
-
- // 加载页面类型字典数据
- await loadPageTypeOptions();
-
- // 加载课程页面列表
- await loadPageList();
-
- // 如果有页面ID,选择对应页面
- if (route.query.id) {
- currentPageId.value = route.query.id as string;
- await loadPageData(route.query.id as string);
- } else if (pageList.value.length > 0) {
- // 默认选择第一个页面
- selectPage(pageList.value[0]);
- }
- // 移除自动创建页面的逻辑,因为现在新增页面会立即保存到数据库
- });
-
- /**
- * 加载页面类型字典数据
- */
- async function loadPageTypeOptions() {
- try {
- const dictData = await initDictOptions('applet_course_page_type');
- pageTypeOptions.value = dictData || [];
- } catch (error) {
- createMessage.error('加载页面类型字典失败');
- // 如果字典加载失败,使用默认选项
- pageTypeOptions.value = [
- { label: '纯文本', value: 'text' },
- { label: '图文混合', value: 'image' },
- { label: '视频', value: 'video' }
- ];
- }
- }
-
- /**
- * 加载课程页面列表
- */
- async function loadPageList() {
- try {
- const params = {
- courseId: currentPage.courseId,
- pageNo: 1,
- pageSize: 9999999
- };
- const result = await list(params);
- const records = result.records || [];
- pageList.value = records
- } catch (error) {
- createMessage.error('加载页面列表失败', error);
- }
- }
-
- /**
- * 选择页面
- */
- function selectPage(page: any) {
- currentPageId.value = page.id;
-
- // 加载页面数据
- loadPageData(page.id);
- }
-
- /**
- * 加载页面数据
- */
- async function loadPageData(id: string) {
- if (!id) {
- return;
- }
-
- try {
- const result = await getById({ id });
- Object.assign(currentPage, result);
- // 解析content字段为组件列表
- parseContentToComponents(result.content);
- } catch (error) {
- createMessage.error('加载页面数据失败');
- }
- }
-
- /**
- * 解析content字段为组件列表
- */
- function parseContentToComponents(content: string) {
- contentComponents.value = [];
- if (!content) return;
-
- try {
- const parsed = JSON.parse(content);
- if (Array.isArray(parsed)) {
- // 确保每个文本组件都有language字段
- contentComponents.value = parsed.map(component => {
- if (component.type === 'text' && !component.language) {
- return { ...component, language: 'zh' }; // 默认中文
- }
- return component;
- });
- } else {
- // 如果是字符串,创建一个文本组件
- contentComponents.value = [{
- type: 'text',
- content: content,
- language: 'zh' // 默认中文
- }];
- }
- } catch (error) {
- // 如果解析失败,作为纯文本处理
- contentComponents.value = [{
- type: 'text',
- content: content,
- language: 'zh' // 默认中文
- }];
- }
- }
-
- /**
- * 将组件列表转换为content字段
- */
- function componentsToContent() {
- return JSON.stringify(contentComponents.value);
- }
-
- /**
- * 返回课程列表
- */
- function goBack() {
- router.back();
- }
-
- /**
- * 新增页面
- */
- async function handleAddPage() {
- try {
- const newPage = {
- courseId: currentPage.courseId,
- title: `新页面 ${pageList.value.length + 1}`,
- type: '0',
- sort: pageList.value.length + 1, // 自动设置为最后一个位置
- pay: 'N',
- status: 'N',
- content: '[]' // 默认空内容
- };
-
- // 调用API添加到数据库
- const result = await add(newPage);
- if (result.id) {
- // 设置新页面的ID
- newPage.id = result.id;
-
- // 将新页面添加到页面列表中
- pageList.value.push(newPage);
-
- // 重新设置所有页面的排序值
- updatePageSortOrder();
-
- // 设置为当前编辑页面
- Object.assign(currentPage, newPage);
- contentComponents.value = [];
- currentPageId.value = newPage.id;
-
- createMessage.success('新增页面成功');
- } else {
- createMessage.error(result.message || '新增页面失败');
- }
- } catch (error) {
- console.error('新增页面失败:', error);
- createMessage.error('新增页面失败');
- }
- }
-
- /**
- * 编辑页面
- */
- function editPage(page: any) {
- selectPage(page);
- }
-
- /**
- * 删除页面
- */
- async function deletePage(page: any) {
- if (!page.id) {
- // 如果是新页面(未保存),直接从列表中移除
- const index = pageList.value.findIndex(p => p === page);
- if (index > -1) {
- pageList.value.splice(index, 1);
- // 重新设置排序值
- updatePageSortOrder();
- // 如果删除的是当前页面,选择其他页面
- if (currentPageId.value === page.id || currentPageId.value === '') {
- if (pageList.value.length > 0) {
- selectPage(pageList.value[0]);
- } else {
- // 没有页面了,创建新页面
- handleAddPage();
- }
- }
- }
- return;
- }
-
- try {
- await deleteOne({ id: page.id }, () => {
- createMessage.success('删除成功');
- // 重新加载页面列表
- loadPageList();
- // 如果删除的是当前页面,选择其他页面
- if (currentPageId.value === page.id) {
- if (pageList.value.length > 1) {
- const index = pageList.value.findIndex(p => p.id === page.id);
- const nextPage = pageList.value[index === 0 ? 1 : index - 1];
- selectPage(nextPage);
- } else {
- // 没有其他页面了,创建新页面
- handleAddPage();
- }
- }
- });
- } catch (error) {
- console.error('删除页面失败:', error);
- createMessage.error('删除失败');
- }
- }
-
- /**
- * 拖拽开始事件
- */
- function onPageDragStart(evt: any) {
- console.log('开始拖拽页面:', evt);
- }
-
- /**
- * 拖拽结束事件
- */
- function onPageDragEnd(evt: any) {
- console.log('拖拽结束:', evt);
- // 更新排序值
- updatePageSortOrder();
- // 保存排序更改
- savePageOrder();
- createMessage.success('页面排序已更新');
- }
-
- /**
- * 更新页面排序值
- */
- function updatePageSortOrder() {
- pageList.value.forEach((page, index) => {
- page.sort = index + 1;
- });
- }
-
- /**
- * 保存页面排序
- */
- async function savePageOrder() {
- try {
- // 批量更新页面排序
- const updatePromises = pageList.value.map(page => {
- if (page.id) {
- return edit({ id: page.id, sort: page.sort });
- }
- }).filter(Boolean);
-
- await Promise.all(updatePromises);
- } catch (error) {
- console.error('保存页面排序失败:', error);
- createMessage.error('保存排序失败');
- }
- }
-
- /**
- * 保存页面数据
- */
- async function handleSave() {
- if (!currentPage.title) {
- createMessage.warning('请输入页面标题');
- return;
- }
-
- if (!currentPage.id) {
- createMessage.warning('页面ID不存在,无法保存');
- return;
- }
-
- saving.value = true;
- try {
- // 将组件列表转换为content字段
- const pageData = {
- ...currentPage,
- content: componentsToContent()
- };
-
- // 只处理编辑操作,因为新增已在handleAddPage中处理
- const result = await edit(pageData);
- if (result) {
- // createMessage.success('保存成功');
-
- // 重新加载页面列表以确保数据同步
- await loadPageList();
-
- // 保持当前页面选中状态
- if (currentPageId.value) {
- const currentPageInList = pageList.value.find(p => p.id === currentPageId.value);
- if (currentPageInList) {
- selectPage(currentPageInList);
- }
- }
- } else {
- createMessage.error(result.message || '保存失败');
- }
- } catch (error) {
- createMessage.error('保存失败');
- } finally {
- saving.value = false;
- }
- }
-
- /**
- * 刷新预览
- */
- function refreshPreview() {
- createMessage.info('预览已刷新');
- }
-
- /**
- * 添加文本内容
- */
- function addTextContent() {
- contentComponents.value.push({
- id: Date.now() + Math.random(), // 生成唯一ID
- type: 'text',
- content: '',
- language: 'zh' // 默认选择中文
- });
- }
-
- /**
- * 添加图片内容
- */
- function addImageContent() {
- contentComponents.value.push({
- id: Date.now() + Math.random(), // 生成唯一ID
- type: 'image',
- imageUrl: '',
- alt: ''
- });
- }
-
- /**
- * 添加视频内容
- */
- function addVideoContent() {
- contentComponents.value.push({
- id: Date.now() + Math.random(), // 生成唯一ID
- type: 'video',
- url: '',
- coverUrl: ''
- });
- }
-
- /**
- * 移除组件
- */
- function removeComponent(index: number) {
- contentComponents.value.splice(index, 1);
- }
-
- /**
- * 图片上传变化处理
- */
- function handleImageChange(value: string, component: any) {
- // JImageUpload组件直接返回图片URL字符串
- component.imageUrl = value;
- }
-
- /**
- * 视频上传变化处理
- */
- function handleVideoChange(value: string, component: any) {
- component.url = value;
- }
-
- /**
- * 视频封面上传变化处理
- */
- function handleVideoCoverChange(value: string, component: any) {
- component.coverUrl = value;
- }
-
- /**
- * 拖拽开始事件
- */
- function onDragStart(evt: any) {
- console.log('拖拽开始:', evt);
- }
-
- /**
- * 拖拽结束事件
- */
- function onDragEnd(evt: any) {
- console.log('拖拽结束:', evt);
- // Vue Draggable 会自动更新 v-model 绑定的数组
- // 这里可以添加额外的处理逻辑,比如保存排序状态
- createMessage.success('组件顺序已更新');
- }
-
- /**
- * 获取页面类型图标
- */
- function getPageTypeIcon(type: string) {
- const iconMap = {
- 0: 'ant-design:font-size-outlined',
- 1: 'ant-design:picture-outlined',
- 2: 'ant-design:video-camera-outlined'
- };
- return iconMap[type] || 'ant-design:file-outlined';
- }
-
- /**
- * 获取页面类型名称
- */
- function getPageTypeName(type: string) {
- // 优先从字典数据中获取名称
- const dictOption = pageTypeOptions.value.find(option => option.value === type);
- if (dictOption) {
- return dictOption.label;
- }
- }
-
-
- </script>
-
- <style scoped>
- /* 主容器 */
- .course-page-editor {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- background: #f5f5f5;
- }
-
- /* 顶部导航栏 */
- .editor-header {
- padding: 16px 24px;
- background: #fff;
- border-bottom: 1px solid #e8e8e8;
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
-
- .header-left {
- display: flex;
- align-items: center;
- gap: 16px;
- }
-
- .back-btn {
- display: flex;
- align-items: center;
- gap: 8px;
- color: #666;
- }
-
- .page-title {
- font-size: 18px;
- font-weight: 600;
- color: #262626;
- }
-
- .header-right {
- display: flex;
- gap: 12px;
- }
-
- .add-btn {
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- /* 页面列表容器 */
- .page-list-container {
- padding: 8px;
- background: #fff;
- border-bottom: 1px solid #e8e8e8;
- flex-shrink: 0;
- }
-
- .page-list-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- }
-
- .list-title {
- font-size: 16px;
- font-weight: 500;
- color: #262626;
- }
-
- .page-count {
- font-size: 14px;
- color: #8c8c8c;
- }
-
- .page-list-scroll {
- overflow-x: auto;
- padding-bottom: 8px;
- }
-
- .page-list {
- display: flex;
- gap: 12px;
- min-width: max-content;
- }
-
- .page-list-container::-webkit-scrollbar {
- height: 6px;
- }
-
- /* 拖拽手柄样式 */
- .drag-handle {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- margin-right: 8px;
- cursor: grab;
- color: #8c8c8c;
- border-radius: 2px;
- transition: all 0.2s;
- }
-
- .drag-handle:hover {
- color: #1890ff;
- background: #f0f0f0;
- }
-
- .drag-handle:active {
- cursor: grabbing;
- }
-
- /* 拖拽状态样式 */
- .sortable-ghost {
- opacity: 0.5;
- background: #f0f7ff;
- border: 2px dashed #1890ff;
- }
-
- .sortable-chosen {
- transform: scale(1.02);
- box-shadow: 0 6px 16px rgba(24, 144, 255, 0.3);
- }
-
- .sortable-drag {
- opacity: 0.8;
- transform: rotate(2deg);
- }
-
- .page-item {
- display: flex;
- align-items: center;
- min-width: 160px;
- width: 160px;
- padding: 8px;
- background: #fff;
- border: 1px solid #d9d9d9;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
- }
-
- .page-item:hover {
- border-color: #1890ff;
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
- }
-
- .page-item.active {
- border-color: #1890ff;
- background: #e6f7ff;
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
- }
-
- .page-thumbnail {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 40px;
- background: #f5f5f5;
- border-radius: 4px;
- margin-bottom: 6px;
- }
-
- .page-icon {
- font-size: 20px;
- color: #1890ff;
- }
-
- .page-info {
- margin-bottom: 6px;
- }
-
- .page-title {
- font-size: 13px;
- font-weight: 500;
- color: #262626;
- margin-bottom: 6px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .page-meta {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .page-type,
- .page-sort {
- font-size: 12px;
- color: #8c8c8c;
- }
-
- .page-actions {
- display: flex;
- justify-content: center;
- gap: 8px;
- }
-
- .empty-page-list {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px;
- color: #8c8c8c;
- }
-
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- color: #d9d9d9;
- }
-
- /* 主编辑区域 */
- .editor-main {
- flex: 1;
- display: flex;
- overflow: hidden;
- }
-
- /* 左侧编辑区 */
- .editor-left {
- width: 50%;
- background: #fff;
- border-right: 1px solid #e8e8e8;
- display: flex;
- flex-direction: column;
- }
-
- /* 右侧预览区 */
- .editor-right {
- width: 50%;
- background: #fff;
- display: flex;
- flex-direction: column;
- }
-
- /* 面板通用样式 */
- .editor-panel,
- /* 设置和预览容器 - 左右布局 */
- .settings-preview-container {
- display: flex;
- height: 100%;
- gap: 24px;
- }
-
- .settings-section {
- flex: 0 0 280px;
- display: flex;
- flex-direction: column;
- border: 1px solid #e8e8e8;
- border-radius: 6px;
- background: #fff;
- }
-
- .mobile-preview-section {
- flex: 1;
- display: flex;
- flex-direction: column;
- border: 1px solid #e8e8e8;
- border-radius: 6px;
- background: #fff;
- }
-
- /* 手机预览样式 */
- .mobile-preview-container {
- display: flex;
- justify-content: center;
- align-items: flex-start;
- padding: 20px;
- background: #f5f5f5;
- min-height: 500px;
- }
-
- .mobile-screen {
- width: 375px;
- height: 667px;
- background: #fff;
- border-radius: 20px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
- overflow: hidden;
- position: relative;
- border: 8px solid #333;
- }
-
- .mobile-screen::before {
- content: '';
- position: absolute;
- top: 10px;
- left: 50%;
- transform: translateX(-50%);
- width: 60px;
- height: 4px;
- background: #333;
- border-radius: 2px;
- z-index: 10;
- }
-
- .mobile-screen .preview-header {
- padding: 20px 16px 12px;
- border-bottom: 1px solid #f0f0f0;
- background: #fff;
- }
-
- .mobile-screen .preview-header h3 {
- font-size: 16px;
- margin: 0 0 8px 0;
- color: #262626;
- font-weight: 600;
- }
-
- .mobile-screen .preview-meta {
- display: flex;
- gap: 6px;
- flex-wrap: wrap;
- }
-
- .mobile-screen .preview-content {
- padding: 16px;
- padding-top: 50px;
- height: calc(100% - 30px);
- overflow-y: auto;
- }
-
- .mobile-screen .empty-preview {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 200px;
- color: #8c8c8c;
- font-size: 14px;
- }
-
- .mobile-screen .content-preview {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .mobile-screen .preview-component {
- background: #f9f9f9;
- border-radius: 8px;
- padding: 12px;
- }
-
- .mobile-screen .text-preview pre {
- font-size: 14px;
- line-height: 1.5;
- margin: 0;
- white-space: pre-wrap;
- word-break: break-word;
- color: #262626;
- }
-
- .mobile-screen .image-preview img {
- width: 100%;
- height: auto;
- border-radius: 6px;
- }
-
- .mobile-screen .placeholder-image,
- .mobile-screen .placeholder-video {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 120px;
- background: #f0f0f0;
- border-radius: 6px;
- color: #8c8c8c;
- font-size: 14px;
- }
-
- .mobile-screen .image-caption {
- margin: 8px 0 0 0;
- font-size: 12px;
- color: #8c8c8c;
- }
-
- .mobile-screen .video-preview video {
- width: 100%;
- height: auto;
- border-radius: 6px;
- }
-
- .mobile-screen .video-cover-preview {
- position: relative;
- display: inline-block;
- width: 100%;
- }
-
- .mobile-screen .video-cover-preview img {
- width: 100%;
- height: auto;
- border-radius: 6px;
- }
-
- .mobile-screen .video-cover-preview .play-icon {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 40px;
- height: 40px;
- background: rgba(0, 0, 0, 0.6);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- font-size: 16px;
- }
-
- .settings-panel,
- .preview-panel {
- display: flex;
- flex-direction: column;
- height: 100%;
- }
-
- .panel-header {
- padding: 16px 24px;
- border-bottom: 1px solid #e8e8e8;
- background: #fafafa;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .panel-header span {
- font-size: 16px;
- font-weight: 500;
- color: #262626;
- }
-
- .panel-content {
- flex: 1;
- padding: 24px;
- overflow-y: auto;
- }
-
- /* 设置面板特殊样式 */
- .settings-panel {
- max-height: 300px;
- }
-
- .settings-form {
- margin-bottom: 0;
- }
-
- /* 内容编辑区域 */
- .content-editor-section {
- margin-top: 24px;
- }
-
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- }
-
- .section-header span {
- font-size: 14px;
- font-weight: 500;
- color: #262626;
- }
-
- .content-components {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- .content-component {
- padding: 16px;
- border: 1px solid #e8e8e8;
- border-radius: 6px;
- background: #fafafa;
- }
-
- .component-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 12px;
- }
-
- .component-header span {
- font-size: 14px;
- font-weight: 500;
- color: #262626;
- flex: 1;
- }
-
- /* 拖拽手柄样式 */
- .drag-handle {
- cursor: move;
- padding: 4px;
- color: #8c8c8c;
- transition: color 0.3s;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border-radius: 4px;
- }
-
- .drag-handle:hover {
- color: #1890ff;
- background-color: #f0f8ff;
- }
-
- /* 拖拽列表样式 */
- .draggable-list {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- /* 拖拽时的样式 */
- .sortable-ghost {
- opacity: 0.5;
- background-color: #f5f5f5;
- border: 2px dashed #d9d9d9;
- }
-
- .sortable-chosen {
- transform: scale(1.02);
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
- border: 2px solid #1890ff;
- background-color: #f0f8ff;
- }
-
- .sortable-drag {
- opacity: 0.8;
- transform: rotate(5deg);
- }
-
- /* 组件头部样式增强 */
- .component-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- background-color: #fafafa;
- border-radius: 6px 6px 0 0;
- border-bottom: 1px solid #f0f0f0;
- transition: all 0.3s;
- }
-
- .content-component:hover .component-header {
- background-color: #f0f8ff;
- }
-
- .content-component {
- transition: all 0.3s;
- border-radius: 6px;
- overflow: hidden;
- }
-
- /* 语言选择器样式 */
- .language-selector {
- margin-bottom: 12px;
- padding: 8px 12px;
- background: #f0f0f0;
- border-radius: 4px;
- border: 1px solid #d9d9d9;
- }
-
- .language-selector .ant-radio-group {
- display: flex;
- gap: 16px;
- }
-
- .empty-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px;
- color: #8c8c8c;
- border: 2px dashed #d9d9d9;
- border-radius: 6px;
- }
-
- /* 上传相关样式 */
- .image-uploader,
- .uploaded-image {
- width: 100%;
- max-width: 200px;
- }
-
- .uploaded-image img {
- width: 100%;
- height: auto;
- border-radius: 6px;
- }
-
- .upload-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 20px;
- border: 2px dashed #d9d9d9;
- border-radius: 6px;
- color: #8c8c8c;
- }
-
- /* 预览区域样式 */
- .preview-container {
- border: 1px solid #e8e8e8;
- border-radius: 6px;
- overflow: hidden;
- }
-
- .preview-header {
- padding: 16px;
- background: #fafafa;
- border-bottom: 1px solid #e8e8e8;
- }
-
- .preview-header h3 {
- margin: 0 0 8px 0;
- font-size: 16px;
- color: #262626;
- }
-
- .preview-meta {
- display: flex;
- gap: 8px;
- }
-
- .preview-content {
- padding: 16px;
- min-height: 200px;
- }
-
- .empty-preview {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 200px;
- color: #8c8c8c;
- }
-
- .content-preview {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- .preview-component {
- padding: 12px;
- border: 1px solid #f0f0f0;
- border-radius: 4px;
- background: #fafafa;
- }
-
- .text-preview pre {
- margin: 0;
- white-space: pre-wrap;
- word-break: break-word;
- font-family: inherit;
- }
-
- /* 文本语言标签样式 */
- .text-language-tag {
- position: absolute;
- top: 4px;
- right: 4px;
- background: #1890ff;
- color: white;
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 2px;
- font-weight: bold;
- }
-
- .text-preview {
- position: relative;
- }
-
- .text-preview.text-english pre {
- font-family: 'Arial', 'Helvetica', sans-serif;
- font-style: italic;
- }
-
- .text-preview.text-english .text-language-tag {
- background: #52c41a;
- }
-
- .image-preview img {
- max-width: 100%;
- height: auto;
- border-radius: 4px;
- }
-
- .image-caption {
- margin-top: 8px;
- font-size: 12px;
- color: #8c8c8c;
- text-align: center;
- }
-
- .placeholder-image,
- .placeholder-video {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 120px;
- background: #f5f5f5;
- border: 2px dashed #d9d9d9;
- border-radius: 4px;
- color: #8c8c8c;
- }
-
- .video-preview video {
- width: 100%;
- max-height: 200px;
- }
-
- /* 响应式设计 */
- @media (max-width: 1200px) {
-
- .editor-left,
- .editor-right {
- width: 50%;
- }
- }
-
- @media (max-width: 768px) {
- .editor-main {
- flex-direction: column;
- }
-
- .editor-left,
- .editor-right {
- width: 100%;
- }
-
- .editor-right {
- border-right: none;
- border-top: 1px solid #e8e8e8;
- }
-
- .page-list {
- flex-direction: column;
- }
-
- .page-item {
- min-width: auto;
- }
- }
- </style>
|