Spaces:
Running
Running
File size: 11,862 Bytes
108f409 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
## 如何自定义一个元素
我们以【网页元素】为例,来梳理下自定义一个元素的过程。
> 完整代码在 https://github.com/pipipi-pikachu/PPTist/tree/document-demo
> 注意:由于版本更新,该文档和仓库中的代码并不是直接复制粘贴就可以使用,这里仅提供思路。
### 编写新元素的结构与配置
首先需要定义这个元素的结构,并添加该元素类型
```typescript
// types/slides.ts
export const enum ElementTypes {
TEXT = 'text',
IMAGE = 'image',
SHAPE = 'shape',
LINE = 'line',
CHART = 'chart',
TABLE = 'table',
LATEX = 'latex',
VIDEO = 'video',
AUDIO = 'audio',
FRAME = 'frame', // add
}
// add
export interface PPTFrameElement extends PPTBaseElement {
type: 'frame'
id: string;
left: number;
top: number;
width: number;
height: number;
url: string; // 网页链接地址
}
// 修改 PPTElement Type
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement | PPTAudioElement | PPTFrameElement
```
在配置文件中添加新元素的中文名,以及最小尺寸:
```typescript
// configs/element
export const ELEMENT_TYPE_ZH = {
text: '文本',
image: '图片',
shape: '形状',
line: '线条',
chart: '图表',
table: '表格',
video: '视频',
audio: '音频',
frame: '网页', // add
}
export const MIN_SIZE = {
text: 20,
image: 20,
shape: 15,
chart: 200,
table: 20,
video: 250,
audio: 20,
frame: 200, // add
}
```
### 编写新元素组件
然后开始编写该元素的组件:
```html
<!-- views/components/element/FrameElement/index.vue -->
<template>
<div class="editable-element-frame"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
height: elementInfo.height + 'px',
}"
>
<div
class="rotate-wrapper"
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
>
<div
class="element-content"
v-contextmenu="contextmenus"
@mousedown="$event => handleSelectElement($event)"
@touchstart="$event => handleSelectElement($event)"
>
<iframe
:src="elementInfo.url"
:width="elementInfo.width"
:height="elementInfo.height"
:frameborder="0"
:allowfullscreen="true"
></iframe>
<div class="drag-handler top"></div>
<div class="drag-handler bottom"></div>
<div class="drag-handler left"></div>
<div class="drag-handler right"></div>
<div class="mask"
v-if="handleElementId !== elementInfo.id"
@mousedown="$event => handleSelectElement($event, false)"
@touchstart="$event => handleSelectElement($event, false)"
></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTFrameElement } from '@/types/slides'
import { ContextmenuItem } from '@/components/Contextmenu/types'
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTFrameElement>,
required: true,
},
selectElement: {
type: Function as PropType<(e: MouseEvent | TouchEvent, element: PPTFrameElement, canMove?: boolean) => void>,
required: true,
},
contextmenus: {
type: Function as PropType<() => ContextmenuItem[] | null>,
},
})
const { handleElementId } = storeToRefs(useMainStore())
const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
e.stopPropagation()
props.selectElement(e, props.elementInfo, canMove)
}
</script>
<style lang="scss" scoped>
.editable-element-frame {
position: absolute;
}
.element-content {
width: 100%;
height: 100%;
cursor: move;
}
.drag-handler {
position: absolute;
&.top {
height: 20px;
left: 0;
right: 0;
top: 0;
}
&.bottom {
height: 20px;
left: 0;
right: 0;
bottom: 0;
}
&.left {
width: 20px;
top: 0;
bottom: 0;
left: 0;
}
&.right {
width: 20px;
top: 0;
bottom: 0;
right: 0;
}
}
.mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
```
此外我们需要另一个不带编辑功能的基础版组件,用于缩略图/放映模式下显示:
```html
<!-- views/components/element/FrameElement/BaseFrameElement.vue -->
<template>
<div class="base-element-frame"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
height: elementInfo.height + 'px',
}"
>
<div
class="rotate-wrapper"
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
>
<div class="element-content">
<iframe
:src="elementInfo.url"
:width="elementInfo.width"
:height="elementInfo.height"
:frameborder="0"
:allowfullscreen="true"
></iframe>
<div class="mask"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { PPTFrameElement } from '@/types/slides'
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTFrameElement>,
required: true,
},
})
</script>
<style lang="scss" scoped>
.base-element-frame {
position: absolute;
}
.element-content {
width: 100%;
height: 100%;
}
.mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
```
在这里你可能会发现,这两个组件非常相似,确实如此,对于比较简单的元素组件来说,可编辑版和不可编辑版是高度一致的,不可编辑版可能仅仅是少了一些方法而已。但是对于比较复杂的元素组件,两者的差异就会比较大了(具体可以比较文本元素和图片元素的两版),因此,你可以自行判断是否将二者合并抽象为一个组件,这里不过多展开。
编写完元素组件,我们需要把它用在需要的地方,具体可能包括:
- 缩略图元素组件 `views/components/ThumbnailSlide/ThumbnailElement.vue`
- 放映元素组件 `views/Screen/ScreenElement.vue`
- 可编辑元素组件 `views/Editor/Canvas/EditableElement.vue`
- 移动端可编辑元素组件 `views/Mobile/MobileEditor/MobileEditableElement.vue`
一般来说,前两者使用不可编辑版,后两者使用可编辑版。
这里仅以画布中的可编辑元素组件为例:
```html
<!-- views/Editor/Canvas/EditableElement.vue -->
<script lang="ts" setup>
import FrameElement from '@/views/components/element/FrameElement/index.vue'
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
[ElementTypes.FRAME]: FrameElement, // add
}
return elementTypeMap[props.elementInfo.type] || null
})
</script>
```
在画布的可编辑元素中,还需要为元素添加操作节点 `Operate`(一般包括八个缩放点、四条边线、一个旋转点),对于特殊的元素(如线条的操作节点明显与其他不同)你可以自己编写该组件,但是一般情况下可以直接使用已经编写好的通用操作节点:
```html
<!-- src\views\Editor\Canvas\Operate\index.vue -->
<script lang="ts" setup>
const currentOperateComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElementOperate,
[ElementTypes.TEXT]: TextElementOperate,
[ElementTypes.SHAPE]: ShapeElementOperate,
[ElementTypes.LINE]: LineElementOperate,
[ElementTypes.TABLE]: TableElementOperate,
[ElementTypes.CHART]: CommonElementOperate,
[ElementTypes.LATEX]: CommonElementOperate,
[ElementTypes.VIDEO]: CommonElementOperate,
[ElementTypes.AUDIO]: CommonElementOperate,
[ElementTypes.FRAME]: CommonElementOperate, // add
}
return elementTypeMap[props.elementInfo.type] || null
})
</script>
```
### 编写右侧元素编辑面板
接下来需要为元素添加一个样式面板。当选中元素时,右侧工具栏会自动聚焦到该面板,你需要在这里添加一些你认为需要的设置项来操作元素本身,只需要记住一点:修改元素实际是修改元素的数据,也就是最开始定义的结构中的各个字段。
另外,修改元素后不要忘了将操作添加到历史记录。
```html
<!-- src\views\Editor\Toolbar\ElementStylePanel\FrameStylePanel.vue -->
<template>
<div class="frame-style-panel">
<div class="row">
<div>网页链接:</div>
<Input v-model:value="url" placeholder="请输入网页链接" />
<Button @click="updateURL()">确定</Button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
const slidesStore = useSlidesStore()
const { handleElementId } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const url = ref('')
const updateURL = () => {
if (!handleElementId.value) return
slidesStore.updateElement({ id: handleElementId.value, props: { url: url.value } })
addHistorySnapshot()
}
</script>
```
```html
<script lang="ts" setup>
import FrameStylePanel from './FrameStylePanel.vue'
const panelMap = {
[ElementTypes.TEXT]: TextStylePanel,
[ElementTypes.IMAGE]: ImageStylePanel,
[ElementTypes.SHAPE]: ShapeStylePanel,
[ElementTypes.LINE]: LineStylePanel,
[ElementTypes.CHART]: ChartStylePanel,
[ElementTypes.TABLE]: TableStylePanel,
[ElementTypes.LATEX]: LatexStylePanel,
[ElementTypes.VIDEO]: VideoStylePanel,
[ElementTypes.AUDIO]: AudioStylePanel,
[ElementTypes.FRAME]: FrameStylePanel, // add
}
</script>
```
### 创建元素
这是自定义一个新元素的最后一步。首先编写一个创建元素的方法:
```typescript
// src\hooks\useCreateElement.ts
const createFrameElement = (url: string) => {
createElement({
type: 'frame',
id: nanoid(10),
width: 800,
height: 480,
rotate: 0,
left: (VIEWPORT_SIZE - 800) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - 480) / 2,
url,
})
}
```
然后在插入工具栏中使用:
```html
<!-- src\views\Editor\CanvasTool\index.vue -->
<template>
<div class="canvas-tool">
<div class="add-element-handler">
<!-- add -->
<span class="handler-item" @click="createFrameElement('https://v3.cn.vuejs.org/')">插入网页</span>
</div>
</div>
</template>
<script lang="ts" setup>
const {
createImageElement,
createChartElement,
createTableElement,
createLatexElement,
createVideoElement,
createAudioElement,
createFrameElement, // add
} = useCreateElement()
</script>
```
点击【插入网页】按钮,你就会看到一个网页元素被添加到画布中了。
### 总结
至此就是自定义一个元素的基本流程了。整个过程比较繁琐,但并不复杂,重点在于元素结构的定义与元素组件的编写,这决定了新元素将具备怎样的能力与外表。而其他的部分仅依葫芦画瓢即可。
除此之外,还有一些非必须的调整:比如你希望导出能够支持新元素,则需要在导出相关的方法中进行扩展;比如你希望主题功能能够应用在新元素上,则需要在主题相关的方法中进行扩展,以此类推。
|