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="title">{{title}}</div>
|
||||
<div class="close-btn"><IconClose /></div>
|
||||
<div class="close-btn" @click="emit('close')"><IconClose /></div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@ -45,6 +45,10 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'close'): void
|
||||
}>()
|
||||
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
|
||||
|
@ -4,10 +4,11 @@ import { useMainStore, useSlidesStore } from '@/store'
|
||||
export default () => {
|
||||
const mainStore = useMainStore()
|
||||
const { currentSlide } = storeToRefs(useSlidesStore())
|
||||
const { hiddenElementIdList } = storeToRefs(mainStore)
|
||||
|
||||
// 将当前页面全部元素设置为被选择状态
|
||||
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)
|
||||
mainStore.setActiveElementIdList(newActiveElementIdList)
|
||||
}
|
||||
|
@ -108,6 +108,8 @@ import {
|
||||
TextRotationNone,
|
||||
TextRotationDown,
|
||||
FormatBrush,
|
||||
PreviewOpen,
|
||||
PreviewClose,
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
export const icons = {
|
||||
@ -217,6 +219,8 @@ export const icons = {
|
||||
IconTextRotationNone: TextRotationNone,
|
||||
IconTextRotationDown: TextRotationDown,
|
||||
IconFormatBrush: FormatBrush,
|
||||
IconPreviewOpen: PreviewOpen,
|
||||
IconPreviewClose: PreviewClose,
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -13,6 +13,7 @@ export interface MainState {
|
||||
activeElementIdList: string[]
|
||||
handleElementId: string
|
||||
activeGroupElementId: string
|
||||
hiddenElementIdList: string[]
|
||||
canvasPercentage: number
|
||||
canvasScale: number
|
||||
canvasDragged: boolean
|
||||
@ -32,6 +33,7 @@ export interface MainState {
|
||||
dialogForExport: DialogForExportTypes
|
||||
databaseId: string
|
||||
textFormatPainter: TextFormatPainter | null
|
||||
showSelectPanel: boolean
|
||||
}
|
||||
|
||||
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
||||
@ -42,6 +44,7 @@ export const useMainStore = defineStore('main', {
|
||||
activeElementIdList: [], // 被选中的元素ID集合,包含 handleElementId
|
||||
handleElementId: '', // 正在操作的元素ID
|
||||
activeGroupElementId: '', // 组合元素成员中,被选中可独立操作的元素ID
|
||||
hiddenElementIdList: [], // 被隐藏的元素ID集合
|
||||
canvasPercentage: 90, // 画布可视区域百分比
|
||||
canvasScale: 1, // 画布缩放比例(基于宽度1000px)
|
||||
canvasDragged: false, // 画布被拖拽移动
|
||||
@ -61,6 +64,7 @@ export const useMainStore = defineStore('main', {
|
||||
dialogForExport: '', // 导出面板
|
||||
databaseId, // 标识当前应用的indexedDB数据库ID
|
||||
textFormatPainter: null, // 文字格式刷
|
||||
showSelectPanel: false, // 打开选择面板
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -95,6 +99,10 @@ export const useMainStore = defineStore('main', {
|
||||
this.activeGroupElementId = activeGroupElementId
|
||||
},
|
||||
|
||||
setHiddenElementIdList(hiddenElementIdList: string[]) {
|
||||
this.hiddenElementIdList = hiddenElementIdList
|
||||
},
|
||||
|
||||
setCanvasPercentage(percentage: number) {
|
||||
this.canvasPercentage = percentage
|
||||
},
|
||||
@ -166,5 +174,9 @@ export const useMainStore = defineStore('main', {
|
||||
setTextFormatPainter(textFormatPainter: TextFormatPainter | null) {
|
||||
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>) => {
|
||||
const mainStore = useMainStore()
|
||||
const { canvasScale } = storeToRefs(mainStore)
|
||||
const { canvasScale, hiddenElementIdList } = storeToRefs(mainStore)
|
||||
|
||||
const mouseSelectionVisible = ref(false)
|
||||
const mouseSelectionQuadrant = ref(1)
|
||||
@ -117,8 +117,8 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
|
||||
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"
|
||||
:dragLineElement="dragLineElement"
|
||||
:moveShapeKeypoint="moveShapeKeypoint"
|
||||
v-show="!hiddenElementIdList.includes(element.id)"
|
||||
/>
|
||||
<ViewportBackground />
|
||||
</div>
|
||||
@ -72,6 +73,7 @@
|
||||
:isMultiSelect="activeElementIdList.length > 1"
|
||||
:selectElement="selectElement"
|
||||
:openLinkDialog="openLinkDialog"
|
||||
v-show="!hiddenElementIdList.includes(element.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,6 +139,7 @@ const {
|
||||
activeElementIdList,
|
||||
activeGroupElementId,
|
||||
handleElementId,
|
||||
hiddenElementIdList,
|
||||
editorAreaFocus,
|
||||
gridLineSize,
|
||||
showRuler,
|
||||
|
@ -27,6 +27,7 @@
|
||||
<MenuItem @click="toggleGridLines()">{{ gridLineSize ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
||||
<MenuItem @click="toggleRuler()">{{ showRuler ? '关闭标尺' : '打开标尺' }}</MenuItem>
|
||||
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
||||
<MenuItem @click="openSelectPanel()">打开选择面板</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
@ -107,6 +108,10 @@ const toggleRuler = () => {
|
||||
mainStore.setRulerState(!showRuler.value)
|
||||
}
|
||||
|
||||
const openSelectPanel = () => {
|
||||
mainStore.setSelectPanelState(true)
|
||||
}
|
||||
|
||||
const hotkeyDrawerVisible = ref(false)
|
||||
|
||||
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>
|
||||
|
||||
<SelectPanel v-if="showSelectPanel" />
|
||||
|
||||
<Modal
|
||||
:visible="!!dialogForExport"
|
||||
:footer="null"
|
||||
@ -43,9 +45,10 @@ import Thumbnails from './Thumbnails/index.vue'
|
||||
import Toolbar from './Toolbar/index.vue'
|
||||
import Remark from './Remark/index.vue'
|
||||
import ExportDialog from './ExportDialog/index.vue'
|
||||
import SelectPanel from './SelectPanel.vue'
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const { dialogForExport } = storeToRefs(mainStore)
|
||||
const { dialogForExport, showSelectPanel } = storeToRefs(mainStore)
|
||||
const closeExportDialog = () => mainStore.setDialogForExport('')
|
||||
|
||||
const remarkHeight = ref(40)
|
||||
|
Loading…
x
Reference in New Issue
Block a user