feat: 添加元素选择面板

This commit is contained in:
pipipi-pikachu 2022-09-27 20:28:51 +08:00
parent 7b0f611edf
commit f62e7da258
9 changed files with 219 additions and 6 deletions

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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: {
@ -94,6 +98,10 @@ export const useMainStore = defineStore('main', {
setActiveGroupElementId(activeGroupElementId: string) {
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
},
},
})

View File

@ -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)
}
// 如果范围内有组合元素的成员,需要该组全部成员都处在范围内,才会被设置为选中状态

View File

@ -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,

View File

@ -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 = () => {

View 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>

View File

@ -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)