|
|
@ -24,33 +24,42 @@ |
|
|
<!-- 上方:课程页面列表(横向滑动) --> |
|
|
<!-- 上方:课程页面列表(横向滑动) --> |
|
|
<div class="page-list-container"> |
|
|
<div class="page-list-container"> |
|
|
<div class="page-list-scroll"> |
|
|
<div class="page-list-scroll"> |
|
|
<div class="page-list"> |
|
|
|
|
|
<div v-for="(page, index) in pageList" :key="page.id" |
|
|
|
|
|
:class="['page-item', { active: currentPageId === page.id }]" @click="selectPage(page)"> |
|
|
|
|
|
<!-- <div class="page-thumbnail"> |
|
|
|
|
|
<Icon :icon="getPageTypeIcon(page.type)" class="page-icon" /> |
|
|
|
|
|
</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> |
|
|
|
|
|
<!-- <span class="page-sort">排序: {{ page.sort || 0 }}</span> --> |
|
|
|
|
|
|
|
|
<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> |
|
|
</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> |
|
|
|
|
|
<!-- 空状态 --> |
|
|
|
|
|
<div v-if="pageList.length === 0" class="empty-page-list"> |
|
|
|
|
|
<Icon icon="ant-design:file-add-outlined" class="empty-icon" /> |
|
|
|
|
|
<p>暂无页面,点击右上角"新增页面"开始创建</p> |
|
|
|
|
|
</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> |
|
|
</div> |
|
|
</div> |
|
|
@ -85,65 +94,98 @@ |
|
|
|
|
|
|
|
|
<!-- 内容组件列表 --> |
|
|
<!-- 内容组件列表 --> |
|
|
<div class="content-components"> |
|
|
<div class="content-components"> |
|
|
<div v-for="(component, index) in contentComponents" :key="index" class="content-component"> |
|
|
|
|
|
<!-- 文本组件 --> |
|
|
|
|
|
<div v-if="component.type === 'text'" class="text-component"> |
|
|
|
|
|
<div class="component-header"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
<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 === '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"> |
|
|
|
|
|
<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 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> |
|
|
</div> |
|
|
<JUpload |
|
|
|
|
|
v-model:value="component.url" |
|
|
|
|
|
bizPath="course" |
|
|
|
|
|
text="上传视频" |
|
|
|
|
|
@change="handleVideoChange($event, component)" |
|
|
|
|
|
style="margin-top: 8px;" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
</draggable> |
|
|
|
|
|
|
|
|
<!-- 空状态 --> |
|
|
<!-- 空状态 --> |
|
|
<div v-if="contentComponents.length === 0" class="empty-content"> |
|
|
<div v-if="contentComponents.length === 0" class="empty-content"> |
|
|
@ -171,9 +213,6 @@ |
|
|
<a-form-item label="页面标题" name="title"> |
|
|
<a-form-item label="页面标题" name="title"> |
|
|
<a-input v-model:value="currentPage.title" placeholder="请输入页面标题" /> |
|
|
<a-input v-model:value="currentPage.title" placeholder="请输入页面标题" /> |
|
|
</a-form-item> |
|
|
</a-form-item> |
|
|
<a-form-item label="排序" name="sort"> |
|
|
|
|
|
<a-input-number v-model:value="currentPage.sort" :min="0" placeholder="排序号" style="width: 100%" /> |
|
|
|
|
|
</a-form-item> |
|
|
|
|
|
<a-form-item label="页面类型" name="type"> |
|
|
<a-form-item label="页面类型" name="type"> |
|
|
<a-select v-model:value="currentPage.type" placeholder="选择类型"> |
|
|
<a-select v-model:value="currentPage.type" placeholder="选择类型"> |
|
|
<a-select-option |
|
|
<a-select-option |
|
|
@ -225,8 +264,16 @@ |
|
|
</div> |
|
|
</div> |
|
|
<!-- 视频预览 --> |
|
|
<!-- 视频预览 --> |
|
|
<div v-else-if="component.type === 'video'" class="video-preview"> |
|
|
<div v-else-if="component.type === 'video'" class="video-preview"> |
|
|
<video v-if="component.url" :src="component.url" controls></video> |
|
|
|
|
|
<div v-else class="placeholder-video">视频预览</div> |
|
|
|
|
|
|
|
|
<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> |
|
|
@ -249,6 +296,7 @@ import { Icon } from '/@/components/Icon'; |
|
|
import { JImageUpload, JUpload } from '/@/components/Form'; |
|
|
import { JImageUpload, JUpload } from '/@/components/Form'; |
|
|
import { saveOrUpdate, getById, list, deleteOne, add, edit } from './AppletCoursePage.api'; |
|
|
import { saveOrUpdate, getById, list, deleteOne, add, edit } from './AppletCoursePage.api'; |
|
|
import { initDictOptions } from '/@/utils/dict/JDictSelectUtil'; |
|
|
import { initDictOptions } from '/@/utils/dict/JDictSelectUtil'; |
|
|
|
|
|
import draggable from 'vuedraggable'; |
|
|
|
|
|
|
|
|
const route = useRoute(); |
|
|
const route = useRoute(); |
|
|
const router = useRouter(); |
|
|
const router = useRouter(); |
|
|
@ -439,7 +487,7 @@ async function handleAddPage() { |
|
|
courseId: currentPage.courseId, |
|
|
courseId: currentPage.courseId, |
|
|
title: `新页面 ${pageList.value.length + 1}`, |
|
|
title: `新页面 ${pageList.value.length + 1}`, |
|
|
type: '0', |
|
|
type: '0', |
|
|
sort: pageList.value.length, |
|
|
|
|
|
|
|
|
sort: pageList.value.length + 1, // 自动设置为最后一个位置 |
|
|
pay: 'N', |
|
|
pay: 'N', |
|
|
status: 'N', |
|
|
status: 'N', |
|
|
content: '[]' // 默认空内容 |
|
|
content: '[]' // 默认空内容 |
|
|
@ -454,6 +502,9 @@ async function handleAddPage() { |
|
|
// 将新页面添加到页面列表中 |
|
|
// 将新页面添加到页面列表中 |
|
|
pageList.value.push(newPage); |
|
|
pageList.value.push(newPage); |
|
|
|
|
|
|
|
|
|
|
|
// 重新设置所有页面的排序值 |
|
|
|
|
|
updatePageSortOrder(); |
|
|
|
|
|
|
|
|
// 设置为当前编辑页面 |
|
|
// 设置为当前编辑页面 |
|
|
Object.assign(currentPage, newPage); |
|
|
Object.assign(currentPage, newPage); |
|
|
contentComponents.value = []; |
|
|
contentComponents.value = []; |
|
|
@ -485,6 +536,8 @@ async function deletePage(page: any) { |
|
|
const index = pageList.value.findIndex(p => p === page); |
|
|
const index = pageList.value.findIndex(p => p === page); |
|
|
if (index > -1) { |
|
|
if (index > -1) { |
|
|
pageList.value.splice(index, 1); |
|
|
pageList.value.splice(index, 1); |
|
|
|
|
|
// 重新设置排序值 |
|
|
|
|
|
updatePageSortOrder(); |
|
|
// 如果删除的是当前页面,选择其他页面 |
|
|
// 如果删除的是当前页面,选择其他页面 |
|
|
if (currentPageId.value === page.id || currentPageId.value === '') { |
|
|
if (currentPageId.value === page.id || currentPageId.value === '') { |
|
|
if (pageList.value.length > 0) { |
|
|
if (pageList.value.length > 0) { |
|
|
@ -521,6 +574,53 @@ async function deletePage(page: any) { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 拖拽开始事件 |
|
|
|
|
|
*/ |
|
|
|
|
|
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('保存排序失败'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 保存页面数据 |
|
|
* 保存页面数据 |
|
|
*/ |
|
|
*/ |
|
|
@ -580,6 +680,7 @@ function refreshPreview() { |
|
|
*/ |
|
|
*/ |
|
|
function addTextContent() { |
|
|
function addTextContent() { |
|
|
contentComponents.value.push({ |
|
|
contentComponents.value.push({ |
|
|
|
|
|
id: Date.now() + Math.random(), // 生成唯一ID |
|
|
type: 'text', |
|
|
type: 'text', |
|
|
content: '', |
|
|
content: '', |
|
|
language: 'zh' // 默认选择中文 |
|
|
language: 'zh' // 默认选择中文 |
|
|
@ -591,6 +692,7 @@ function addTextContent() { |
|
|
*/ |
|
|
*/ |
|
|
function addImageContent() { |
|
|
function addImageContent() { |
|
|
contentComponents.value.push({ |
|
|
contentComponents.value.push({ |
|
|
|
|
|
id: Date.now() + Math.random(), // 生成唯一ID |
|
|
type: 'image', |
|
|
type: 'image', |
|
|
imageUrl: '', |
|
|
imageUrl: '', |
|
|
alt: '' |
|
|
alt: '' |
|
|
@ -602,8 +704,10 @@ function addImageContent() { |
|
|
*/ |
|
|
*/ |
|
|
function addVideoContent() { |
|
|
function addVideoContent() { |
|
|
contentComponents.value.push({ |
|
|
contentComponents.value.push({ |
|
|
|
|
|
id: Date.now() + Math.random(), // 生成唯一ID |
|
|
type: 'video', |
|
|
type: 'video', |
|
|
url: '' |
|
|
|
|
|
|
|
|
url: '', |
|
|
|
|
|
coverUrl: '' |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -629,6 +733,30 @@ function handleVideoChange(value: string, component: any) { |
|
|
component.url = value; |
|
|
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('组件顺序已更新'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 获取页面类型图标 |
|
|
* 获取页面类型图标 |
|
|
*/ |
|
|
*/ |
|
|
@ -746,9 +874,51 @@ function getPageTypeName(type: string) { |
|
|
height: 6px; |
|
|
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 { |
|
|
.page-item { |
|
|
min-width: 140px; |
|
|
|
|
|
width: 140px; |
|
|
|
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
min-width: 160px; |
|
|
|
|
|
width: 160px; |
|
|
padding: 8px; |
|
|
padding: 8px; |
|
|
background: #fff; |
|
|
background: #fff; |
|
|
border: 1px solid #d9d9d9; |
|
|
border: 1px solid #d9d9d9; |
|
|
@ -1002,6 +1172,34 @@ function getPageTypeName(type: string) { |
|
|
border-radius: 6px; |
|
|
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, |
|
|
.settings-panel, |
|
|
.preview-panel { |
|
|
.preview-panel { |
|
|
display: flex; |
|
|
display: flex; |
|
|
@ -1084,6 +1282,73 @@ function getPageTypeName(type: string) { |
|
|
flex: 1; |
|
|
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 { |
|
|
.language-selector { |
|
|
margin-bottom: 12px; |
|
|
margin-bottom: 12px; |
|
|
|