jvs-ppt-ui 0.0.11 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/dist/lib/jvs-ppt-ui.es.js +21682 -21520
- package/dist/lib/jvs-ppt-ui.umd.js +85 -85
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/components/CanvasArea.vue +44 -21
- package/src/components/Header.vue +1592 -1564
- package/src/components/PptEditor.vue +5 -0
- package/src/components/PresentationMode.vue +2 -1
- package/src/components/SidebarLeft.vue +17 -5
- package/src/components/TemplateGallery.vue +217 -119
- package/src/components/canvas/elements/CanvasShapeElement.vue +3 -3
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -4
- package/src/composables/canvas/useCanvasShortcuts.ts +50 -1
- package/src/composables/canvas/useCanvasTable.ts +8 -3
- package/src/composables/canvas/useCanvasText.ts +7 -3
- package/src/composables/sidebar/useSidebarElementTransform.ts +17 -4
- package/src/composables/useSlides.ts +33 -14
- package/src/utils/groupFlip.ts +108 -0
|
@@ -4,7 +4,7 @@ import { getFontVariantStyle } from '../../../configs/fonts'
|
|
|
4
4
|
import { useSlides } from '../../../composables/useSlides'
|
|
5
5
|
import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
|
|
6
6
|
|
|
7
|
-
const { shapeFormatPainter } = useSlides()
|
|
7
|
+
const { shapeFormatPainter, isReadonly } = useSlides()
|
|
8
8
|
const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
|
|
9
9
|
const isTextSelectable = computed(() => (props.isEditingText && props.element.id === props.selectedElementId) || isTextPaintActive.value)
|
|
10
10
|
|
|
@@ -176,7 +176,7 @@ function handleMaskMouseDown(e: MouseEvent) {
|
|
|
176
176
|
>
|
|
177
177
|
<!-- Sibling Table Edit Mask -->
|
|
178
178
|
<div
|
|
179
|
-
v-if="element.id === selectedElementId && isTable(element) && !isEditingText"
|
|
179
|
+
v-if="!isReadonly && element.id === selectedElementId && isTable(element) && !isEditingText"
|
|
180
180
|
class="table-edit-mask"
|
|
181
181
|
@mousemove="handleMaskMouseMove"
|
|
182
182
|
@mousedown="handleMaskMouseDown"
|
|
@@ -187,7 +187,7 @@ function handleMaskMouseDown(e: MouseEvent) {
|
|
|
187
187
|
|
|
188
188
|
<!-- Sibling Formula Edit Mask -->
|
|
189
189
|
<div
|
|
190
|
-
v-if="element.id === selectedElementId && element.latex && !isEditingText"
|
|
190
|
+
v-if="!isReadonly && element.id === selectedElementId && element.latex && !isEditingText"
|
|
191
191
|
class="formula-edit-mask"
|
|
192
192
|
@dblclick.stop="emit('formulaDblClick')"
|
|
193
193
|
></div>
|
|
@@ -203,7 +203,7 @@ function handleMaskMouseDown(e: MouseEvent) {
|
|
|
203
203
|
@focusout="emit('focusout', $event)"
|
|
204
204
|
@input="emit('input', $event)"
|
|
205
205
|
@keydown="emit('keydown', $event)"
|
|
206
|
-
@dblclick.stop="isTextSelectable ? null : emit('dblclick', $event)"
|
|
206
|
+
@dblclick.stop="isReadonly ? null : (isTextSelectable ? null : emit('dblclick', $event))"
|
|
207
207
|
@mousemove="emit('mousemove', $event)"
|
|
208
208
|
@mouseleave="emit('mouseleave', $event)"
|
|
209
209
|
:class="{
|
|
@@ -36,6 +36,7 @@ export interface ShortcutsOptions {
|
|
|
36
36
|
shapeFormatPainter?: Ref<any>
|
|
37
37
|
notify: (msg: string) => void
|
|
38
38
|
addHistorySnapshot?: () => void
|
|
39
|
+
isReadonly?: Ref<boolean>
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function useCanvasShortcuts(options: ShortcutsOptions) {
|
|
@@ -73,7 +74,8 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
|
|
|
73
74
|
getElementPixels,
|
|
74
75
|
shapeFormatPainter,
|
|
75
76
|
notify,
|
|
76
|
-
addHistorySnapshot
|
|
77
|
+
addHistorySnapshot,
|
|
78
|
+
isReadonly
|
|
77
79
|
} = options
|
|
78
80
|
|
|
79
81
|
let historyDebounceTimer: any = null
|
|
@@ -89,6 +91,53 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
|
|
|
89
91
|
function handleGlobalKeyDown(e: KeyboardEvent) {
|
|
90
92
|
if (e.defaultPrevented) return
|
|
91
93
|
|
|
94
|
+
if (isReadonly?.value) {
|
|
95
|
+
if (e.key === 'F5') {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
if (!isPresenting.value) {
|
|
98
|
+
if (e.shiftKey) {
|
|
99
|
+
startPresentation()
|
|
100
|
+
} else {
|
|
101
|
+
activeIndex.value = 0
|
|
102
|
+
startPresentation()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
|
|
108
|
+
showSearchPanel.value = !showSearchPanel.value
|
|
109
|
+
if (showSearchPanel.value) {
|
|
110
|
+
activeSearchTab.value = 'find'
|
|
111
|
+
}
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (e.key === 'Escape') {
|
|
116
|
+
selectedElementId.value = null
|
|
117
|
+
selectedElementIds.value = []
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
121
|
+
if (activeIndex.value > 0) activeIndex.value--
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
|
126
|
+
if (activeIndex.value < slides.value.length - 1) activeIndex.value++
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'C')) {
|
|
131
|
+
copySelectedSlides()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (['Delete', 'Backspace'].includes(e.key) || ((e.ctrlKey || e.metaKey) && ['d', 'g', 'z', 'y', 'v', 'x'].includes(e.key.toLowerCase()))) {
|
|
135
|
+
e.preventDefault()
|
|
136
|
+
e.stopPropagation()
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
92
141
|
// Ctrl + D (Duplicate / 快速复制副本) - 拦截防止浏览器添加书签
|
|
93
142
|
if ((e.ctrlKey || e.metaKey) && (e.key === 'd' || e.key === 'D')) {
|
|
94
143
|
e.preventDefault()
|
|
@@ -12,6 +12,7 @@ export interface TableOptions {
|
|
|
12
12
|
getOverlayHeight?: (el: any) => string
|
|
13
13
|
selectedElementWidth?: Ref<string>
|
|
14
14
|
selectedElementHeight?: Ref<string>
|
|
15
|
+
isReadonly?: Ref<boolean>
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface TableMeasure {
|
|
@@ -57,7 +58,8 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
57
58
|
getOverlayWidth = (el: any) => el?.style?.width || '500px',
|
|
58
59
|
getOverlayHeight = (el: any) => el?.style?.height || '200px',
|
|
59
60
|
selectedElementWidth,
|
|
60
|
-
selectedElementHeight
|
|
61
|
+
selectedElementHeight,
|
|
62
|
+
isReadonly
|
|
61
63
|
} = options
|
|
62
64
|
|
|
63
65
|
const selectedCellCount = ref(0)
|
|
@@ -461,7 +463,7 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
461
463
|
}
|
|
462
464
|
|
|
463
465
|
function selectColumnCells(el: any, colIdx: number) {
|
|
464
|
-
if (!el || !el.id) return
|
|
466
|
+
if (isReadonly?.value || !el || !el.id) return
|
|
465
467
|
selectedElementId.value = el.id
|
|
466
468
|
if (!selectedElementIds.value.includes(el.id)) {
|
|
467
469
|
selectedElementIds.value = [el.id]
|
|
@@ -508,7 +510,7 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
508
510
|
}
|
|
509
511
|
|
|
510
512
|
function selectRowCells(el: any, rowIdx: number) {
|
|
511
|
-
if (!el || !el.id) return
|
|
513
|
+
if (isReadonly?.value || !el || !el.id) return
|
|
512
514
|
selectedElementId.value = el.id
|
|
513
515
|
if (!selectedElementIds.value.includes(el.id)) {
|
|
514
516
|
selectedElementIds.value = [el.id]
|
|
@@ -1297,6 +1299,7 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
1297
1299
|
}
|
|
1298
1300
|
|
|
1299
1301
|
function startTableColResize(e: MouseEvent, el: any, targetColIdx: number) {
|
|
1302
|
+
if (isReadonly?.value) return
|
|
1300
1303
|
e.preventDefault()
|
|
1301
1304
|
e.stopPropagation()
|
|
1302
1305
|
|
|
@@ -1401,6 +1404,7 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
1401
1404
|
}
|
|
1402
1405
|
|
|
1403
1406
|
function startTableRowResize(e: MouseEvent, el: any, targetRowIdx: number) {
|
|
1407
|
+
if (isReadonly?.value) return
|
|
1404
1408
|
e.preventDefault()
|
|
1405
1409
|
e.stopPropagation()
|
|
1406
1410
|
|
|
@@ -1613,6 +1617,7 @@ export function useCanvasTable(options: TableOptions) {
|
|
|
1613
1617
|
}
|
|
1614
1618
|
|
|
1615
1619
|
function handleTableDblClick(e: MouseEvent, el: any) {
|
|
1620
|
+
if (isReadonly?.value) return
|
|
1616
1621
|
e.stopPropagation()
|
|
1617
1622
|
const targetEl = el || selectedElement.value
|
|
1618
1623
|
if (!targetEl) return
|
|
@@ -25,6 +25,7 @@ export interface TextOptions {
|
|
|
25
25
|
renderLatexToSvg: (latex: string) => string
|
|
26
26
|
sanitizeNbspInHtml: (html: string) => string
|
|
27
27
|
updateTextFontScale: (el: any) => void
|
|
28
|
+
isReadonly?: Ref<boolean>
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function useCanvasText(options: TextOptions) {
|
|
@@ -48,7 +49,8 @@ export function useCanvasText(options: TextOptions) {
|
|
|
48
49
|
applyTextStyleToHtml,
|
|
49
50
|
renderLatexToSvg,
|
|
50
51
|
sanitizeNbspInHtml,
|
|
51
|
-
updateTextFontScale
|
|
52
|
+
updateTextFontScale,
|
|
53
|
+
isReadonly
|
|
52
54
|
} = options
|
|
53
55
|
|
|
54
56
|
const isEditingText = ref(false)
|
|
@@ -56,7 +58,7 @@ export function useCanvasText(options: TextOptions) {
|
|
|
56
58
|
const editingPlaceholderOriginalValue = ref<{ [key: string]: string }>({})
|
|
57
59
|
|
|
58
60
|
function updateElementContent(id: string, text: string) {
|
|
59
|
-
if (!activeSlide.value) return
|
|
61
|
+
if (isReadonly?.value || !activeSlide.value) return
|
|
60
62
|
const el = activeSlide.value.elements.find((item: any) => item.id === id)
|
|
61
63
|
if (el) {
|
|
62
64
|
if (el.type === 'shape') {
|
|
@@ -198,6 +200,7 @@ export function useCanvasText(options: TextOptions) {
|
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
function startEditingShapeText(elOrId: any, mouseEvent?: MouseEvent) {
|
|
203
|
+
if (isReadonly?.value) return
|
|
201
204
|
const elId = typeof elOrId === 'string' ? elOrId : elOrId?.id
|
|
202
205
|
const el = typeof elOrId === 'string'
|
|
203
206
|
? (findElementInSlide(elOrId) || activeSlide.value?.elements.find((x: any) => x.id === elOrId))
|
|
@@ -255,6 +258,7 @@ export function useCanvasText(options: TextOptions) {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
function startEditingTextElement(elId: string, mouseEvent?: MouseEvent) {
|
|
261
|
+
if (isReadonly?.value || !elId) return
|
|
258
262
|
selectedElementId.value = elId
|
|
259
263
|
selectedElementIds.value = [elId]
|
|
260
264
|
isEditingText.value = true
|
|
@@ -406,7 +410,7 @@ export function useCanvasText(options: TextOptions) {
|
|
|
406
410
|
}
|
|
407
411
|
|
|
408
412
|
function handleFormulaDblClick(el: any) {
|
|
409
|
-
if (!el.latex) return
|
|
413
|
+
if (isReadonly?.value || !el.latex) return
|
|
410
414
|
currentEditingFormulaEl.value = el
|
|
411
415
|
formulaDialogVisible.value = true
|
|
412
416
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import { shapeFormulas } from '../../utils/pptxShapesLazy'
|
|
3
3
|
import { useSlides } from '../useSlides'
|
|
4
|
+
import { applyGroupGeometryFlip } from '../../utils/groupFlip'
|
|
4
5
|
|
|
5
6
|
export function useSidebarElementTransform(
|
|
6
7
|
selectedElement: ComputedRef<any>,
|
|
@@ -65,10 +66,16 @@ export function useSidebarElementTransform(
|
|
|
65
66
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
66
67
|
activeSlide.value.elements.forEach((el: any) => {
|
|
67
68
|
if (selectedElementIds.value.includes(el.id)) {
|
|
68
|
-
if (
|
|
69
|
-
|
|
69
|
+
if (el.type === 'group') {
|
|
70
|
+
applyGroupGeometryFlip(el, true, false)
|
|
71
|
+
} else {
|
|
72
|
+
if (!el.style) el.style = {}
|
|
73
|
+
el.style.flipH = !el.style.flipH
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
})
|
|
77
|
+
} else if (selectedElement.value && selectedElement.value.type === 'group') {
|
|
78
|
+
applyGroupGeometryFlip(selectedElement.value, true, false)
|
|
72
79
|
} else {
|
|
73
80
|
getStyle().flipH = !getStyle().flipH
|
|
74
81
|
}
|
|
@@ -79,10 +86,16 @@ export function useSidebarElementTransform(
|
|
|
79
86
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
80
87
|
activeSlide.value.elements.forEach((el: any) => {
|
|
81
88
|
if (selectedElementIds.value.includes(el.id)) {
|
|
82
|
-
if (
|
|
83
|
-
|
|
89
|
+
if (el.type === 'group') {
|
|
90
|
+
applyGroupGeometryFlip(el, false, true)
|
|
91
|
+
} else {
|
|
92
|
+
if (!el.style) el.style = {}
|
|
93
|
+
el.style.flipV = !el.style.flipV
|
|
94
|
+
}
|
|
84
95
|
}
|
|
85
96
|
})
|
|
97
|
+
} else if (selectedElement.value && selectedElement.value.type === 'group') {
|
|
98
|
+
applyGroupGeometryFlip(selectedElement.value, false, true)
|
|
86
99
|
} else {
|
|
87
100
|
getStyle().flipV = !getStyle().flipV
|
|
88
101
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
inferElementTextType,
|
|
13
13
|
normalizeTextType
|
|
14
14
|
} from '../utils/textTypes'
|
|
15
|
+
import { applyGroupGeometryFlip } from '../utils/groupFlip'
|
|
15
16
|
import type { CustomUploadHandler, UploadResourceContext, UploadResourceType } from '../types'
|
|
16
17
|
|
|
17
18
|
export type { SlideTextType }
|
|
@@ -340,15 +341,15 @@ function setAiConfig(config?: { apiUrl?: string; headers?: Record<string, string
|
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
//
|
|
344
|
-
export const DEFAULT_MATERIAL_API_URL = '/jvs-dam/
|
|
345
|
-
export const DEFAULT_MATERIAL_PAGE_API_URL = '/jvs-dam/
|
|
344
|
+
// 素材库配置状态(公网免版权素材检索 stock/search、单页模板、素材保存 API)
|
|
345
|
+
export const DEFAULT_MATERIAL_API_URL = '/jvs-dam/stock/search'
|
|
346
|
+
export const DEFAULT_MATERIAL_PAGE_API_URL = '/jvs-dam/stock/search'
|
|
346
347
|
|
|
347
348
|
const defaultEnvMaterialUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env)
|
|
348
|
-
? String((import.meta as any).env?.
|
|
349
|
+
? String((import.meta as any).env?.VITE_MATERIAL_STOCK_API_URL || (import.meta as any).env?.VITE_MATERIAL_API_URL || (import.meta as any).env?.VITE_MATERIAL_LIST_API_URL || (import.meta as any).env?.VITE_MATERIAL_TREE_API_URL || DEFAULT_MATERIAL_API_URL)
|
|
349
350
|
: DEFAULT_MATERIAL_API_URL
|
|
350
351
|
const defaultEnvMaterialPageUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env)
|
|
351
|
-
? String((import.meta as any).env?.VITE_MATERIAL_PAGE_API_URL || DEFAULT_MATERIAL_PAGE_API_URL)
|
|
352
|
+
? String((import.meta as any).env?.VITE_MATERIAL_PAGE_API_URL || (import.meta as any).env?.VITE_MATERIAL_STOCK_API_URL || DEFAULT_MATERIAL_PAGE_API_URL)
|
|
352
353
|
: DEFAULT_MATERIAL_PAGE_API_URL
|
|
353
354
|
const defaultEnvMaterialTypeUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_MATERIAL_TYPE_API_URL)
|
|
354
355
|
? String((import.meta as any).env.VITE_MATERIAL_TYPE_API_URL)
|
|
@@ -357,11 +358,11 @@ const defaultEnvMaterialSaveUrl = (typeof import.meta !== 'undefined' && (import
|
|
|
357
358
|
? String((import.meta as any).env.VITE_MATERIAL_SAVE_API_URL)
|
|
358
359
|
: ''
|
|
359
360
|
|
|
360
|
-
/**
|
|
361
|
+
/** 素材库检索公网素材/所有内容的 API 地址 */
|
|
361
362
|
const materialApiUrl = ref<string>(defaultEnvMaterialUrl)
|
|
362
|
-
/**
|
|
363
|
+
/** 素材库首页/分类检索 API 地址 */
|
|
363
364
|
const materialTreeApiUrl = ref<string>(defaultEnvMaterialUrl)
|
|
364
|
-
/**
|
|
365
|
+
/** 素材库分页/关键词检索 API 地址 */
|
|
365
366
|
const materialPageApiUrl = ref<string>(defaultEnvMaterialPageUrl)
|
|
366
367
|
/** 素材库按类型获取单独分类素材的 API 地址 */
|
|
367
368
|
const materialTypeApiUrl = ref<string>(defaultEnvMaterialTypeUrl)
|
|
@@ -2497,6 +2498,7 @@ watch(formulaDialogVisible, (visible) => {
|
|
|
2497
2498
|
}
|
|
2498
2499
|
})
|
|
2499
2500
|
const isPresenting = ref(false)
|
|
2501
|
+
const isReadonly = ref(false)
|
|
2500
2502
|
const showNotification = ref(false)
|
|
2501
2503
|
const notificationMsg = ref('')
|
|
2502
2504
|
const shapeFormatPainter = ref<any>(null)
|
|
@@ -2958,6 +2960,7 @@ function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list:
|
|
|
2958
2960
|
}
|
|
2959
2961
|
|
|
2960
2962
|
function undo() {
|
|
2963
|
+
if (isReadonly.value) return
|
|
2961
2964
|
if (isMasterEditingView.value) {
|
|
2962
2965
|
// 先落盘防抖中的快照:编辑后 350ms 内撤销不再丢最后一步编辑
|
|
2963
2966
|
flushPendingMasterHistorySnapshot()
|
|
@@ -3009,6 +3012,7 @@ function undo() {
|
|
|
3009
3012
|
}
|
|
3010
3013
|
|
|
3011
3014
|
function redo() {
|
|
3015
|
+
if (isReadonly.value) return
|
|
3012
3016
|
if (isMasterEditingView.value) {
|
|
3013
3017
|
flushPendingMasterHistorySnapshot()
|
|
3014
3018
|
if (masterHistoryIndex.value < masterHistoryStack.value.length - 1) {
|
|
@@ -3183,6 +3187,7 @@ function escapeRegExp(string: string) {
|
|
|
3183
3187
|
}
|
|
3184
3188
|
|
|
3185
3189
|
function addSlide() {
|
|
3190
|
+
if (isReadonly.value) return
|
|
3186
3191
|
const newId = Date.now().toString()
|
|
3187
3192
|
const insertIndex = slides.value.length > 0 ? activeIndex.value + 1 : 0
|
|
3188
3193
|
const curSlide = slides.value[activeIndex.value]
|
|
@@ -3202,6 +3207,7 @@ function addSlide() {
|
|
|
3202
3207
|
}
|
|
3203
3208
|
|
|
3204
3209
|
function deleteSlide(index: number) {
|
|
3210
|
+
if (isReadonly.value) return
|
|
3205
3211
|
const isLastSlide = slides.value.length === 1
|
|
3206
3212
|
if (isLastSlide) {
|
|
3207
3213
|
const newSlide = {
|
|
@@ -3232,7 +3238,7 @@ function deleteSlide(index: number) {
|
|
|
3232
3238
|
}
|
|
3233
3239
|
|
|
3234
3240
|
function pasteSlideData(targetSlideData: any, afterIndex = activeIndex.value, isCut = false) {
|
|
3235
|
-
if (!targetSlideData) return
|
|
3241
|
+
if (isReadonly.value || !targetSlideData) return
|
|
3236
3242
|
|
|
3237
3243
|
if (Array.isArray(targetSlideData)) {
|
|
3238
3244
|
if (targetSlideData.length === 0) return
|
|
@@ -3445,7 +3451,7 @@ function scaleElementIfNeeded(el: any, sX: number, sY: number) {
|
|
|
3445
3451
|
}
|
|
3446
3452
|
|
|
3447
3453
|
function pasteElementsData(elementsArray: any[], options?: { sourceWidth?: number; sourceHeight?: number }) {
|
|
3448
|
-
if (!activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
|
|
3454
|
+
if (isReadonly.value || !activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
|
|
3449
3455
|
const pastedIds: string[] = []
|
|
3450
3456
|
const oldIdToNewId = new Map<string, string>()
|
|
3451
3457
|
const oldGroupIdToNewGroupId = new Map<string, string>()
|
|
@@ -3889,7 +3895,7 @@ function selectAllSlides() {
|
|
|
3889
3895
|
}
|
|
3890
3896
|
|
|
3891
3897
|
function deleteSelectedSlides() {
|
|
3892
|
-
if (!slides.value || slides.value.length === 0) return
|
|
3898
|
+
if (isReadonly.value || !slides.value || slides.value.length === 0) return
|
|
3893
3899
|
const sortedIndexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => b - a)
|
|
3894
3900
|
if (sortedIndexes.length === 0) {
|
|
3895
3901
|
deleteSlide(activeIndex.value)
|
|
@@ -3942,12 +3948,14 @@ function copySelectedSlides() {
|
|
|
3942
3948
|
}
|
|
3943
3949
|
|
|
3944
3950
|
function cutSelectedSlides() {
|
|
3951
|
+
if (isReadonly.value) return
|
|
3945
3952
|
copySelectedSlides()
|
|
3946
3953
|
deleteSelectedSlides()
|
|
3947
3954
|
notify('已剪切选中的幻灯片')
|
|
3948
3955
|
}
|
|
3949
3956
|
|
|
3950
3957
|
function duplicateSelectedSlides() {
|
|
3958
|
+
if (isReadonly.value) return
|
|
3951
3959
|
const indexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => a - b)
|
|
3952
3960
|
if (indexes.length === 0) indexes.push(activeIndex.value)
|
|
3953
3961
|
const lastIndex = indexes[indexes.length - 1]
|
|
@@ -3976,6 +3984,7 @@ function duplicateSelectedSlides() {
|
|
|
3976
3984
|
}
|
|
3977
3985
|
|
|
3978
3986
|
function applyBackground(bg: string) {
|
|
3987
|
+
if (isReadonly.value) return
|
|
3979
3988
|
if (activeSlide.value) {
|
|
3980
3989
|
activeSlide.value.background = bg
|
|
3981
3990
|
activeSlide.value.bgType = 'gradient'
|
|
@@ -4009,7 +4018,7 @@ function addElement(
|
|
|
4009
4018
|
initialDimensions?: { width?: number; height?: number; left?: number; top?: number },
|
|
4010
4019
|
extraOptions?: { textType?: SlideTextType; name?: string; [key: string]: any }
|
|
4011
4020
|
) {
|
|
4012
|
-
if (!activeSlide.value) return
|
|
4021
|
+
if (isReadonly.value || !activeSlide.value) return
|
|
4013
4022
|
|
|
4014
4023
|
const recipe = getActiveSlideThemeRecipe(activeSlide.value)
|
|
4015
4024
|
|
|
@@ -4824,7 +4833,7 @@ function updateGroupBounds(groupEl: SlideElement) {
|
|
|
4824
4833
|
|
|
4825
4834
|
// Delete Element from current slide
|
|
4826
4835
|
function deleteElement(id: string, silent = false) {
|
|
4827
|
-
if (!activeSlide.value) return
|
|
4836
|
+
if (isReadonly.value || !activeSlide.value) return
|
|
4828
4837
|
const hasTopLevel = activeSlide.value.elements.some(item => item.id === id)
|
|
4829
4838
|
if (hasTopLevel) {
|
|
4830
4839
|
activeSlide.value.elements = activeSlide.value.elements.filter(item => item.id !== id)
|
|
@@ -6582,7 +6591,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
|
|
|
6582
6591
|
const grpFlipH = !!(el.flipH || el.isFlipH || el.style?.flipH)
|
|
6583
6592
|
const grpFlipV = !!(el.flipV || el.isFlipV || el.style?.flipV)
|
|
6584
6593
|
|
|
6585
|
-
|
|
6594
|
+
const groupObj: any = {
|
|
6586
6595
|
id: el.id || 'group_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
|
|
6587
6596
|
type: 'group',
|
|
6588
6597
|
name: el.name || '组合',
|
|
@@ -6601,6 +6610,12 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
|
|
|
6601
6610
|
isFlipV: grpFlipV,
|
|
6602
6611
|
elements: el.elements.map((child: any) => convertChildRecursive(child, topW, topH))
|
|
6603
6612
|
}
|
|
6613
|
+
|
|
6614
|
+
if (grpFlipH || grpFlipV) {
|
|
6615
|
+
applyGroupGeometryFlip(groupObj, grpFlipH, grpFlipV)
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6618
|
+
return groupObj
|
|
6604
6619
|
}
|
|
6605
6620
|
|
|
6606
6621
|
const elemPattern = extractPatternFromFill(el)
|
|
@@ -7283,6 +7298,9 @@ function preprocessImportedSlides(slides: any[]) {
|
|
|
7283
7298
|
}
|
|
7284
7299
|
processEl(child, grpW, grpH)
|
|
7285
7300
|
})
|
|
7301
|
+
if (el.flipH || el.isFlipH || el.style?.flipH || el.flipV || el.isFlipV || el.style?.flipV) {
|
|
7302
|
+
applyGroupGeometryFlip(el)
|
|
7303
|
+
}
|
|
7286
7304
|
}
|
|
7287
7305
|
}
|
|
7288
7306
|
for (const el of slide.elements) {
|
|
@@ -9255,6 +9273,7 @@ export function useSlides() {
|
|
|
9255
9273
|
selectedElementIds,
|
|
9256
9274
|
croppingElementId,
|
|
9257
9275
|
isPresenting,
|
|
9276
|
+
isReadonly,
|
|
9258
9277
|
showNotification,
|
|
9259
9278
|
notificationMsg,
|
|
9260
9279
|
notify,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 组合 (Group) 翻转与几何镜像计算工具函数
|
|
3
|
+
*
|
|
4
|
+
* 遵循微软 PowerPoint / DrawingML 规范标准:
|
|
5
|
+
* 当对组合执行水平/垂直翻转时:
|
|
6
|
+
* 1. 组合内部子元素的相对几何位置镜像对调(left / top 沿中心线镜像);
|
|
7
|
+
* 2. 子元素若是形状/图片,其自身的翻转状态联动反转;
|
|
8
|
+
* 3. 子元素若是纯文本(Text),文字字形永远保持正向可读(Text is never mirrored),禁止倒影;
|
|
9
|
+
* 4. 几何镜像处理完成后,Group 容器自身不再需要使用 CSS scale(-1) 进行整体物理镜像。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function applyGroupGeometryFlip(group: any, forceFlipH?: boolean, forceFlipV?: boolean) {
|
|
13
|
+
if (!group || !Array.isArray(group.elements) || group.elements.length === 0) return
|
|
14
|
+
|
|
15
|
+
const flipH = forceFlipH !== undefined ? forceFlipH : Boolean(group.flipH || group.isFlipH || group.style?.flipH)
|
|
16
|
+
const flipV = forceFlipV !== undefined ? forceFlipV : Boolean(group.flipV || group.isFlipV || group.style?.flipV)
|
|
17
|
+
|
|
18
|
+
if (!flipH && !flipV) return
|
|
19
|
+
|
|
20
|
+
const grpW = typeof group.width === 'number' && group.width > 0 ? group.width : (parseFloat(group.style?.width) || 0)
|
|
21
|
+
const grpH = typeof group.height === 'number' && group.height > 0 ? group.height : (parseFloat(group.style?.height) || 0)
|
|
22
|
+
|
|
23
|
+
for (const child of group.elements) {
|
|
24
|
+
if (!child) continue
|
|
25
|
+
|
|
26
|
+
// 1. 水平翻转:镜像相对水平位置
|
|
27
|
+
if (flipH) {
|
|
28
|
+
if (typeof child.style?.left === 'string' && child.style.left.endsWith('%')) {
|
|
29
|
+
const lPct = parseFloat(child.style.left) || 0
|
|
30
|
+
const wPct = parseFloat(child.style.width) || 0
|
|
31
|
+
child.style.left = `${+(100 - lPct - wPct).toFixed(4)}%`
|
|
32
|
+
} else if (typeof child.left === 'number' && grpW > 0) {
|
|
33
|
+
const cW = typeof child.width === 'number' ? child.width : (parseFloat(child.style?.width) || 0)
|
|
34
|
+
child.left = +(grpW - child.left - cW).toFixed(4)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. 垂直翻转:镜像相对垂直位置
|
|
39
|
+
if (flipV) {
|
|
40
|
+
if (typeof child.style?.top === 'string' && child.style.top.endsWith('%')) {
|
|
41
|
+
const tPct = parseFloat(child.style.top) || 0
|
|
42
|
+
const hPct = parseFloat(child.style.height) || 0
|
|
43
|
+
child.style.top = `${+(100 - tPct - hPct).toFixed(4)}%`
|
|
44
|
+
} else if (typeof child.top === 'number' && grpH > 0) {
|
|
45
|
+
const cH = typeof child.height === 'number' ? child.height : (parseFloat(child.style?.height) || 0)
|
|
46
|
+
child.top = +(grpH - child.top - cH).toFixed(4)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 子元素自身翻转状态更新:纯文本和连线永不翻转,形状/图片等反转
|
|
51
|
+
const is2DArrow = /^(?:rightArrow|leftArrow|upArrow|downArrow|leftRightArrow|upDownArrow|quadArrow|leftRightUpArrow|bentArrow|uturnArrow|leftUpArrow|bentUpArrow|curvedRightArrow|curvedLeftArrow|curvedUpArrow|curvedDownArrow|stripedRightArrow|notchedRightArrow|pentagon|chevron|circularArrow|swooshArrow|.*ArrowCallout)$/i.test(String(child.shapType || ''))
|
|
52
|
+
const isConnector = !is2DArrow && (
|
|
53
|
+
child.type === 'line' ||
|
|
54
|
+
Boolean(child.startConnection || child.endConnection) ||
|
|
55
|
+
Boolean(child.borderTailEnd || child.borderHeadEnd) ||
|
|
56
|
+
/^(?:line|straightConnector\d*|bentConnector\d*|curvedConnector\d*)$/i.test(String(child.shapType || '')) ||
|
|
57
|
+
(/连接符/i.test(String(child.name || '')) && !is2DArrow)
|
|
58
|
+
)
|
|
59
|
+
const isPureText = child.type === 'text' || isConnector
|
|
60
|
+
|
|
61
|
+
if (isPureText) {
|
|
62
|
+
child.flipH = false
|
|
63
|
+
child.isFlipH = false
|
|
64
|
+
if (child.style) child.style.flipH = false
|
|
65
|
+
child.flipV = false
|
|
66
|
+
child.isFlipV = false
|
|
67
|
+
if (child.style) child.style.flipV = false
|
|
68
|
+
} else if (child.type === 'group') {
|
|
69
|
+
applyGroupGeometryFlip(child, flipH, flipV)
|
|
70
|
+
} else {
|
|
71
|
+
if (flipH) {
|
|
72
|
+
const curH = Boolean(child.flipH || child.isFlipH || child.style?.flipH)
|
|
73
|
+
child.flipH = !curH
|
|
74
|
+
child.isFlipH = !curH
|
|
75
|
+
if (child.style) child.style.flipH = !curH
|
|
76
|
+
}
|
|
77
|
+
if (flipV) {
|
|
78
|
+
const curV = Boolean(child.flipV || child.isFlipV || child.style?.flipV)
|
|
79
|
+
child.flipV = !curV
|
|
80
|
+
child.isFlipV = !curV
|
|
81
|
+
if (child.style) child.style.flipV = !curV
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. 子元素全部完成几何镜像后,Group 容器自身无需再 scale 镜像,避免将内部文本整体反转为倒影字
|
|
87
|
+
group.flipH = false
|
|
88
|
+
group.isFlipH = false
|
|
89
|
+
if (group.style) {
|
|
90
|
+
group.style.flipH = false
|
|
91
|
+
group.style.flipV = false
|
|
92
|
+
}
|
|
93
|
+
group.flipV = false
|
|
94
|
+
group.isFlipV = false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 判断一个元素是否为包含文本的组合元素(或纯文本元素),
|
|
99
|
+
* 渲染层用此判断避免在外层容器施加 scale(-1) 导致内部文字倒影。
|
|
100
|
+
*/
|
|
101
|
+
export function isTextContainingElement(el: any): boolean {
|
|
102
|
+
if (!el) return false
|
|
103
|
+
if (el.type === 'text' || el.type === 'line') return true
|
|
104
|
+
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
105
|
+
return el.elements.some((child: any) => isTextContainingElement(child))
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|