mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加元素选择面板
This commit is contained in:
parent
7b0f611edf
commit
f62e7da258
@ -10,7 +10,7 @@
|
|||||||
>
|
>
|
||||||
<div class="header" @mousedown="$event => startMove($event)">
|
<div class="header" @mousedown="$event => startMove($event)">
|
||||||
<div class="title">{{title}}</div>
|
<div class="title">{{title}}</div>
|
||||||
<div class="close-btn"><IconClose /></div>
|
<div class="close-btn" @click="emit('close')"><IconClose /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -45,6 +45,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
|
|
||||||
|
@ -4,10 +4,11 @@ import { useMainStore, useSlidesStore } from '@/store'
|
|||||||
export default () => {
|
export default () => {
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { currentSlide } = storeToRefs(useSlidesStore())
|
const { currentSlide } = storeToRefs(useSlidesStore())
|
||||||
|
const { hiddenElementIdList } = storeToRefs(mainStore)
|
||||||
|
|
||||||
// 将当前页面全部元素设置为被选择状态
|
// 将当前页面全部元素设置为被选择状态
|
||||||
const selectAllElement = () => {
|
const selectAllElement = () => {
|
||||||
const unlockedElements = currentSlide.value.elements.filter(el => !el.lock)
|
const unlockedElements = currentSlide.value.elements.filter(el => !el.lock && !hiddenElementIdList.value.includes(el.id))
|
||||||
const newActiveElementIdList = unlockedElements.map(el => el.id)
|
const newActiveElementIdList = unlockedElements.map(el => el.id)
|
||||||
mainStore.setActiveElementIdList(newActiveElementIdList)
|
mainStore.setActiveElementIdList(newActiveElementIdList)
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,8 @@ import {
|
|||||||
TextRotationNone,
|
TextRotationNone,
|
||||||
TextRotationDown,
|
TextRotationDown,
|
||||||
FormatBrush,
|
FormatBrush,
|
||||||
|
PreviewOpen,
|
||||||
|
PreviewClose,
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
@ -217,6 +219,8 @@ export const icons = {
|
|||||||
IconTextRotationNone: TextRotationNone,
|
IconTextRotationNone: TextRotationNone,
|
||||||
IconTextRotationDown: TextRotationDown,
|
IconTextRotationDown: TextRotationDown,
|
||||||
IconFormatBrush: FormatBrush,
|
IconFormatBrush: FormatBrush,
|
||||||
|
IconPreviewOpen: PreviewOpen,
|
||||||
|
IconPreviewClose: PreviewClose,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -13,6 +13,7 @@ export interface MainState {
|
|||||||
activeElementIdList: string[]
|
activeElementIdList: string[]
|
||||||
handleElementId: string
|
handleElementId: string
|
||||||
activeGroupElementId: string
|
activeGroupElementId: string
|
||||||
|
hiddenElementIdList: string[]
|
||||||
canvasPercentage: number
|
canvasPercentage: number
|
||||||
canvasScale: number
|
canvasScale: number
|
||||||
canvasDragged: boolean
|
canvasDragged: boolean
|
||||||
@ -32,6 +33,7 @@ export interface MainState {
|
|||||||
dialogForExport: DialogForExportTypes
|
dialogForExport: DialogForExportTypes
|
||||||
databaseId: string
|
databaseId: string
|
||||||
textFormatPainter: TextFormatPainter | null
|
textFormatPainter: TextFormatPainter | null
|
||||||
|
showSelectPanel: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
||||||
@ -42,6 +44,7 @@ export const useMainStore = defineStore('main', {
|
|||||||
activeElementIdList: [], // 被选中的元素ID集合,包含 handleElementId
|
activeElementIdList: [], // 被选中的元素ID集合,包含 handleElementId
|
||||||
handleElementId: '', // 正在操作的元素ID
|
handleElementId: '', // 正在操作的元素ID
|
||||||
activeGroupElementId: '', // 组合元素成员中,被选中可独立操作的元素ID
|
activeGroupElementId: '', // 组合元素成员中,被选中可独立操作的元素ID
|
||||||
|
hiddenElementIdList: [], // 被隐藏的元素ID集合
|
||||||
canvasPercentage: 90, // 画布可视区域百分比
|
canvasPercentage: 90, // 画布可视区域百分比
|
||||||
canvasScale: 1, // 画布缩放比例(基于宽度1000px)
|
canvasScale: 1, // 画布缩放比例(基于宽度1000px)
|
||||||
canvasDragged: false, // 画布被拖拽移动
|
canvasDragged: false, // 画布被拖拽移动
|
||||||
@ -61,6 +64,7 @@ export const useMainStore = defineStore('main', {
|
|||||||
dialogForExport: '', // 导出面板
|
dialogForExport: '', // 导出面板
|
||||||
databaseId, // 标识当前应用的indexedDB数据库ID
|
databaseId, // 标识当前应用的indexedDB数据库ID
|
||||||
textFormatPainter: null, // 文字格式刷
|
textFormatPainter: null, // 文字格式刷
|
||||||
|
showSelectPanel: false, // 打开选择面板
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -94,6 +98,10 @@ export const useMainStore = defineStore('main', {
|
|||||||
setActiveGroupElementId(activeGroupElementId: string) {
|
setActiveGroupElementId(activeGroupElementId: string) {
|
||||||
this.activeGroupElementId = activeGroupElementId
|
this.activeGroupElementId = activeGroupElementId
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setHiddenElementIdList(hiddenElementIdList: string[]) {
|
||||||
|
this.hiddenElementIdList = hiddenElementIdList
|
||||||
|
},
|
||||||
|
|
||||||
setCanvasPercentage(percentage: number) {
|
setCanvasPercentage(percentage: number) {
|
||||||
this.canvasPercentage = percentage
|
this.canvasPercentage = percentage
|
||||||
@ -166,5 +174,9 @@ export const useMainStore = defineStore('main', {
|
|||||||
setTextFormatPainter(textFormatPainter: TextFormatPainter | null) {
|
setTextFormatPainter(textFormatPainter: TextFormatPainter | null) {
|
||||||
this.textFormatPainter = textFormatPainter
|
this.textFormatPainter = textFormatPainter
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setSelectPanelState(show: boolean) {
|
||||||
|
this.showSelectPanel = show
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
@ -6,7 +6,7 @@ import { getElementRange } from '@/utils/element'
|
|||||||
|
|
||||||
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | undefined>) => {
|
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | undefined>) => {
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { canvasScale } = storeToRefs(mainStore)
|
const { canvasScale, hiddenElementIdList } = storeToRefs(mainStore)
|
||||||
|
|
||||||
const mouseSelectionVisible = ref(false)
|
const mouseSelectionVisible = ref(false)
|
||||||
const mouseSelectionQuadrant = ref(1)
|
const mouseSelectionQuadrant = ref(1)
|
||||||
@ -117,8 +117,8 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
|
|||||||
maxY < mouseSelectionTop + mouseSelectionHeight
|
maxY < mouseSelectionTop + mouseSelectionHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// 被锁定的元素即使在范围内,也不需要设置为选中状态
|
// 被锁定或被隐藏的元素即使在范围内,也不需要设置为选中状态
|
||||||
if (isInclude && !element.lock) inRangeElementList.push(element)
|
if (isInclude && !element.lock && !hiddenElementIdList.value.includes(element.id)) inRangeElementList.push(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果范围内有组合元素的成员,需要该组全部成员都处在范围内,才会被设置为选中状态
|
// 如果范围内有组合元素的成员,需要该组全部成员都处在范围内,才会被设置为选中状态
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
:openLinkDialog="openLinkDialog"
|
:openLinkDialog="openLinkDialog"
|
||||||
:dragLineElement="dragLineElement"
|
:dragLineElement="dragLineElement"
|
||||||
:moveShapeKeypoint="moveShapeKeypoint"
|
:moveShapeKeypoint="moveShapeKeypoint"
|
||||||
|
v-show="!hiddenElementIdList.includes(element.id)"
|
||||||
/>
|
/>
|
||||||
<ViewportBackground />
|
<ViewportBackground />
|
||||||
</div>
|
</div>
|
||||||
@ -72,6 +73,7 @@
|
|||||||
:isMultiSelect="activeElementIdList.length > 1"
|
:isMultiSelect="activeElementIdList.length > 1"
|
||||||
:selectElement="selectElement"
|
:selectElement="selectElement"
|
||||||
:openLinkDialog="openLinkDialog"
|
:openLinkDialog="openLinkDialog"
|
||||||
|
v-show="!hiddenElementIdList.includes(element.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -137,6 +139,7 @@ const {
|
|||||||
activeElementIdList,
|
activeElementIdList,
|
||||||
activeGroupElementId,
|
activeGroupElementId,
|
||||||
handleElementId,
|
handleElementId,
|
||||||
|
hiddenElementIdList,
|
||||||
editorAreaFocus,
|
editorAreaFocus,
|
||||||
gridLineSize,
|
gridLineSize,
|
||||||
showRuler,
|
showRuler,
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
<MenuItem @click="toggleGridLines()">{{ gridLineSize ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
<MenuItem @click="toggleGridLines()">{{ gridLineSize ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
||||||
<MenuItem @click="toggleRuler()">{{ showRuler ? '关闭标尺' : '打开标尺' }}</MenuItem>
|
<MenuItem @click="toggleRuler()">{{ showRuler ? '关闭标尺' : '打开标尺' }}</MenuItem>
|
||||||
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
||||||
|
<MenuItem @click="openSelectPanel()">打开选择面板</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@ -107,6 +108,10 @@ const toggleRuler = () => {
|
|||||||
mainStore.setRulerState(!showRuler.value)
|
mainStore.setRulerState(!showRuler.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSelectPanel = () => {
|
||||||
|
mainStore.setSelectPanelState(true)
|
||||||
|
}
|
||||||
|
|
||||||
const hotkeyDrawerVisible = ref(false)
|
const hotkeyDrawerVisible = ref(false)
|
||||||
|
|
||||||
const goIssues = () => {
|
const goIssues = () => {
|
||||||
|
181
src/views/Editor/SelectPanel.vue
Normal file
181
src/views/Editor/SelectPanel.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<MoveablePanel
|
||||||
|
class="select-panel"
|
||||||
|
:width="200"
|
||||||
|
:height="360"
|
||||||
|
:title="`选择(${activeElementIdList.length}/${currentSlide.elements.length})`"
|
||||||
|
:left="-270"
|
||||||
|
:top="90"
|
||||||
|
@close="close()"
|
||||||
|
>
|
||||||
|
<div class="btns" v-if="elements.length">
|
||||||
|
<Button size="small" style="margin-right: 5px;" @click="showAll()">全部显示</Button>
|
||||||
|
<Button size="small" @click="hideAll()">全部隐藏</Button>
|
||||||
|
</div>
|
||||||
|
<div class="element-list">
|
||||||
|
<template v-for="item in elements" :key="item.id">
|
||||||
|
<div class="group-els" v-if="item.type === 'group'">
|
||||||
|
<div class="group-title">组合</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{
|
||||||
|
'active': activeElementIdList.includes(groupItem.id),
|
||||||
|
'group-active': activeGroupElementId.includes(groupItem.id),
|
||||||
|
}"
|
||||||
|
v-for="groupItem in item.elements"
|
||||||
|
:key="groupItem.id"
|
||||||
|
@click="selectGroupEl(item, groupItem.id)"
|
||||||
|
>
|
||||||
|
<div class="name">{{ELEMENT_TYPE_ZH[groupItem.type]}}</div>
|
||||||
|
<div class="icons">
|
||||||
|
<IconPreviewClose style="font-size: 17px;" @click.stop="hideElement(groupItem.id)" v-if="hiddenElementIdList.includes(groupItem.id)" />
|
||||||
|
<IconPreviewOpen style="font-size: 17px;" @click.stop="hideElement(groupItem.id)" v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ 'active': activeElementIdList.includes(item.id) }"
|
||||||
|
v-else
|
||||||
|
@click="selectEl(item.id)"
|
||||||
|
>
|
||||||
|
<div class="name">{{ELEMENT_TYPE_ZH[item.type]}}</div>
|
||||||
|
<div class="icons">
|
||||||
|
<IconPreviewClose style="font-size: 17px;" @click.stop="hideElement(item.id)" v-if="hiddenElementIdList.includes(item.id)" />
|
||||||
|
<IconPreviewOpen style="font-size: 17px;" @click.stop="hideElement(item.id)" v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</MoveablePanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore, useMainStore } from '@/store'
|
||||||
|
import { PPTElement } from '@/types/slides'
|
||||||
|
import { ELEMENT_TYPE_ZH } from '@/configs/element'
|
||||||
|
|
||||||
|
const slidesStore = useSlidesStore()
|
||||||
|
const mainStore = useMainStore()
|
||||||
|
const { currentSlide } = storeToRefs(slidesStore)
|
||||||
|
const { handleElementId, activeElementIdList, activeGroupElementId, hiddenElementIdList } = storeToRefs(mainStore)
|
||||||
|
|
||||||
|
interface GroupElements {
|
||||||
|
type: 'group'
|
||||||
|
id: string
|
||||||
|
elements: PPTElement[]
|
||||||
|
}
|
||||||
|
type ElementItem = PPTElement | GroupElements
|
||||||
|
|
||||||
|
const elements = computed<ElementItem[]>(() => {
|
||||||
|
const _elements: ElementItem[] = []
|
||||||
|
|
||||||
|
for (const el of currentSlide.value.elements) {
|
||||||
|
if (el.groupId) {
|
||||||
|
const lastItem = _elements[_elements.length - 1]
|
||||||
|
|
||||||
|
if (lastItem && lastItem.type === 'group' && lastItem.id && lastItem.id === el.groupId) {
|
||||||
|
lastItem.elements.push(el)
|
||||||
|
}
|
||||||
|
else _elements.push({ type: 'group', id: el.groupId, elements: [el] })
|
||||||
|
}
|
||||||
|
else _elements.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _elements
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectGroupEl = (item: GroupElements, id: string) => {
|
||||||
|
if (handleElementId.value === id) return
|
||||||
|
if (hiddenElementIdList.value.includes(id)) return
|
||||||
|
|
||||||
|
const idList = item.elements.map(el => el.id)
|
||||||
|
mainStore.setActiveElementIdList(idList)
|
||||||
|
mainStore.setHandleElementId(id)
|
||||||
|
nextTick(() => mainStore.setActiveGroupElementId(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectEl = (id: string) => {
|
||||||
|
if (handleElementId.value === id) return
|
||||||
|
if (hiddenElementIdList.value.includes(id)) return
|
||||||
|
|
||||||
|
mainStore.setActiveElementIdList([id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideElement = (id: string) => {
|
||||||
|
if (hiddenElementIdList.value.includes(id)) {
|
||||||
|
mainStore.setHiddenElementIdList(hiddenElementIdList.value.filter(item => item !== id))
|
||||||
|
}
|
||||||
|
else mainStore.setHiddenElementIdList([...hiddenElementIdList.value, id])
|
||||||
|
|
||||||
|
if (activeElementIdList.value.includes(id)) mainStore.setActiveElementIdList([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAll = () => {
|
||||||
|
if (hiddenElementIdList.value.length) mainStore.setHiddenElementIdList([])
|
||||||
|
}
|
||||||
|
const hideAll = () => {
|
||||||
|
mainStore.setHiddenElementIdList(currentSlide.value.elements.map(item => item.id))
|
||||||
|
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
mainStore.setSelectPanelState(false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select-panel {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.btns {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.element-list {
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-right: -10px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: $borderRadius;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: rgba($color: $themeColor, $alpha: .1);
|
||||||
|
}
|
||||||
|
&.group-active {
|
||||||
|
background-color: rgba($color: $themeColor, $alpha: .2);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color: $themeColor, $alpha: .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
width: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.group-els {
|
||||||
|
padding: 5px 0;
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -16,6 +16,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectPanel v-if="showSelectPanel" />
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
:visible="!!dialogForExport"
|
:visible="!!dialogForExport"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
@ -43,9 +45,10 @@ import Thumbnails from './Thumbnails/index.vue'
|
|||||||
import Toolbar from './Toolbar/index.vue'
|
import Toolbar from './Toolbar/index.vue'
|
||||||
import Remark from './Remark/index.vue'
|
import Remark from './Remark/index.vue'
|
||||||
import ExportDialog from './ExportDialog/index.vue'
|
import ExportDialog from './ExportDialog/index.vue'
|
||||||
|
import SelectPanel from './SelectPanel.vue'
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { dialogForExport } = storeToRefs(mainStore)
|
const { dialogForExport, showSelectPanel } = storeToRefs(mainStore)
|
||||||
const closeExportDialog = () => mainStore.setDialogForExport('')
|
const closeExportDialog = () => mainStore.setDialogForExport('')
|
||||||
|
|
||||||
const remarkHeight = ref(40)
|
const remarkHeight = ref(40)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user