CatPtain commited on
Commit
7d93f04
·
verified ·
1 Parent(s): 989a2fe

Upload 168 files

Browse files
frontend/src/views/Editor/Canvas/Operate/ImageElementOperate.vue CHANGED
@@ -1,67 +1,67 @@
1
- <template>
2
- <div class="image-element-operate" :class="{ 'cliping': isCliping }">
3
- <BorderLine
4
- class="operate-border-line"
5
- v-for="line in borderLines"
6
- :key="line.type"
7
- :type="line.type"
8
- :style="line.style"
9
- />
10
- <template v-if="handlerVisible">
11
- <ResizeHandler
12
- class="operate-resize-handler"
13
- v-for="point in resizeHandlers"
14
- :key="point.direction"
15
- :type="point.direction"
16
- :rotate="elementInfo.rotate"
17
- :style="point.style"
18
- @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
19
- />
20
- <RotateHandler
21
- class="operate-rotate-handler"
22
- :style="{ left: scaleWidth / 2 + 'px' }"
23
- @mousedown.stop="$event => rotateElement($event, elementInfo)"
24
- />
25
- </template>
26
- </div>
27
- </template>
28
-
29
- <script lang="ts">
30
- export default {
31
- inheritAttrs: false,
32
- }
33
- </script>
34
-
35
- <script lang="ts" setup>
36
- import { computed } from 'vue'
37
- import { storeToRefs } from 'pinia'
38
- import { useMainStore } from '@/store'
39
- import type { PPTImageElement } from '@/types/slides'
40
- import type { OperateResizeHandlers } from '@/types/edit'
41
- import useCommonOperate from '../hooks/useCommonOperate'
42
-
43
- import RotateHandler from './RotateHandler.vue'
44
- import ResizeHandler from './ResizeHandler.vue'
45
- import BorderLine from './BorderLine.vue'
46
-
47
- const props = defineProps<{
48
- elementInfo: PPTImageElement
49
- handlerVisible: boolean
50
- rotateElement: (e: MouseEvent, element: PPTImageElement) => void
51
- scaleElement: (e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void
52
- }>()
53
-
54
- const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
55
-
56
- const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
57
-
58
- const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
59
- const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
60
- const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
61
- </script>
62
-
63
- <style lang="scss" scoped>
64
- .image-element-operate.cliping {
65
- visibility: hidden;
66
- }
67
  </style>
 
1
+ <template>
2
+ <div class="image-element-operate" :class="{ 'cliping': isCliping }">
3
+ <BorderLine
4
+ class="operate-border-line"
5
+ v-for="line in borderLines"
6
+ :key="line.type"
7
+ :type="line.type"
8
+ :style="line.style"
9
+ />
10
+ <template v-if="handlerVisible">
11
+ <ResizeHandler
12
+ class="operate-resize-handler"
13
+ v-for="point in resizeHandlers"
14
+ :key="point.direction"
15
+ :type="point.direction"
16
+ :rotate="elementInfo.rotate"
17
+ :style="point.style"
18
+ @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
19
+ />
20
+ <RotateHandler
21
+ class="operate-rotate-handler"
22
+ :style="{ left: scaleWidth / 2 + 'px' }"
23
+ @mousedown.stop="$event => rotateElement($event, elementInfo)"
24
+ />
25
+ </template>
26
+ </div>
27
+ </template>
28
+
29
+ <script lang="ts">
30
+ export default {
31
+ inheritAttrs: false,
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts" setup>
36
+ import { computed } from 'vue'
37
+ import { storeToRefs } from 'pinia'
38
+ import { useMainStore } from '@/store'
39
+ import type { PPTImageElement } from '@/types/slides'
40
+ import type { OperateResizeHandlers } from '@/types/edit'
41
+ import useCommonOperate from '../hooks/useCommonOperate'
42
+
43
+ import RotateHandler from './RotateHandler.vue'
44
+ import ResizeHandler from './ResizeHandler.vue'
45
+ import BorderLine from './BorderLine.vue'
46
+
47
+ const props = defineProps<{
48
+ elementInfo: PPTImageElement
49
+ handlerVisible: boolean
50
+ rotateElement: (e: MouseEvent, element: PPTImageElement) => void
51
+ scaleElement: (e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void
52
+ }>()
53
+
54
+ const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
55
+
56
+ const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
57
+
58
+ const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
59
+ const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
60
+ const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
61
+ </script>
62
+
63
+ <style lang="scss" scoped>
64
+ .image-element-operate.cliping {
65
+ visibility: hidden;
66
+ }
67
  </style>
frontend/src/views/Editor/Canvas/Operate/TextElementOperate.vue CHANGED
@@ -1,61 +1,61 @@
1
- <template>
2
- <div class="text-element-operate">
3
- <BorderLine
4
- class="operate-border-line"
5
- v-for="line in borderLines"
6
- :key="line.type"
7
- :type="line.type"
8
- :style="line.style"
9
- />
10
- <template v-if="handlerVisible">
11
- <ResizeHandler
12
- class="operate-resize-handler"
13
- v-for="point in resizeHandlers"
14
- :key="point.direction"
15
- :type="point.direction"
16
- :rotate="elementInfo.rotate"
17
- :style="point.style"
18
- @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
19
- />
20
- <RotateHandler
21
- class="operate-rotate-handler"
22
- :style="{ left: scaleWidth / 2 + 'px' }"
23
- @mousedown.stop="$event => rotateElement($event, elementInfo)"
24
- />
25
- </template>
26
- </div>
27
- </template>
28
-
29
- <script lang="ts">
30
- export default {
31
- inheritAttrs: false,
32
- }
33
- </script>
34
-
35
- <script lang="ts" setup>
36
- import { computed } from 'vue'
37
- import { storeToRefs } from 'pinia'
38
- import { useMainStore } from '@/store'
39
- import type { PPTTextElement } from '@/types/slides'
40
- import type { OperateResizeHandlers } from '@/types/edit'
41
- import useCommonOperate from '../hooks/useCommonOperate'
42
-
43
- import RotateHandler from './RotateHandler.vue'
44
- import ResizeHandler from './ResizeHandler.vue'
45
- import BorderLine from './BorderLine.vue'
46
-
47
- const props = defineProps<{
48
- elementInfo: PPTTextElement
49
- handlerVisible: boolean
50
- rotateElement: (e: MouseEvent, element: PPTTextElement) => void
51
- scaleElement: (e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void
52
- }>()
53
-
54
- const { canvasScale } = storeToRefs(useMainStore())
55
-
56
- const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
57
- const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
58
-
59
- const { textElementResizeHandlers, verticalTextElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
60
- const resizeHandlers = computed(() => props.elementInfo.vertical ? verticalTextElementResizeHandlers.value : textElementResizeHandlers.value)
61
  </script>
 
1
+ <template>
2
+ <div class="text-element-operate">
3
+ <BorderLine
4
+ class="operate-border-line"
5
+ v-for="line in borderLines"
6
+ :key="line.type"
7
+ :type="line.type"
8
+ :style="line.style"
9
+ />
10
+ <template v-if="handlerVisible">
11
+ <ResizeHandler
12
+ class="operate-resize-handler"
13
+ v-for="point in resizeHandlers"
14
+ :key="point.direction"
15
+ :type="point.direction"
16
+ :rotate="elementInfo.rotate"
17
+ :style="point.style"
18
+ @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
19
+ />
20
+ <RotateHandler
21
+ class="operate-rotate-handler"
22
+ :style="{ left: scaleWidth / 2 + 'px' }"
23
+ @mousedown.stop="$event => rotateElement($event, elementInfo)"
24
+ />
25
+ </template>
26
+ </div>
27
+ </template>
28
+
29
+ <script lang="ts">
30
+ export default {
31
+ inheritAttrs: false,
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts" setup>
36
+ import { computed } from 'vue'
37
+ import { storeToRefs } from 'pinia'
38
+ import { useMainStore } from '@/store'
39
+ import type { PPTTextElement } from '@/types/slides'
40
+ import type { OperateResizeHandlers } from '@/types/edit'
41
+ import useCommonOperate from '../hooks/useCommonOperate'
42
+
43
+ import RotateHandler from './RotateHandler.vue'
44
+ import ResizeHandler from './ResizeHandler.vue'
45
+ import BorderLine from './BorderLine.vue'
46
+
47
+ const props = defineProps<{
48
+ elementInfo: PPTTextElement
49
+ handlerVisible: boolean
50
+ rotateElement: (e: MouseEvent, element: PPTTextElement) => void
51
+ scaleElement: (e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void
52
+ }>()
53
+
54
+ const { canvasScale } = storeToRefs(useMainStore())
55
+
56
+ const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
57
+ const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
58
+
59
+ const { textElementResizeHandlers, verticalTextElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
60
+ const resizeHandlers = computed(() => props.elementInfo.vertical ? verticalTextElementResizeHandlers.value : textElementResizeHandlers.value)
61
  </script>
frontend/src/views/Editor/Canvas/Operate/index.vue CHANGED
@@ -1,138 +1,138 @@
1
- <template>
2
- <div
3
- class="operate"
4
- :class="{ 'multi-select': isMultiSelect && !isActive }"
5
- :style="{
6
- top: elementInfo.top * canvasScale + 'px',
7
- left: elementInfo.left * canvasScale + 'px',
8
- transform: `rotate(${rotate}deg)`,
9
- transformOrigin: `${elementInfo.width * canvasScale / 2}px ${height * canvasScale / 2}px`,
10
- }"
11
- >
12
- <component
13
- v-if="isSelected"
14
- :is="currentOperateComponent"
15
- :elementInfo="elementInfo"
16
- :handlerVisible="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)"
17
- :rotateElement="rotateElement"
18
- :scaleElement="scaleElement"
19
- :dragLineElement="dragLineElement"
20
- :moveShapeKeypoint="moveShapeKeypoint"
21
- ></component>
22
-
23
- <div
24
- class="animation-index"
25
- v-if="toolbarState === 'elAnimation' && elementIndexListInAnimation.length"
26
- >
27
- <div class="index-item" v-for="index in elementIndexListInAnimation" :key="index">{{index + 1}}</div>
28
- </div>
29
-
30
- <LinkHandler
31
- :elementInfo="elementInfo"
32
- :link="elementInfo.link"
33
- :openLinkDialog="openLinkDialog"
34
- v-if="isActive && elementInfo.link"
35
- @mousedown.stop=""
36
- />
37
- </div>
38
- </template>
39
-
40
- <script lang="ts" setup>
41
- import { computed } from 'vue'
42
- import { storeToRefs } from 'pinia'
43
- import { useMainStore, useSlidesStore } from '@/store'
44
- import {
45
- ElementTypes,
46
- type PPTElement,
47
- type PPTLineElement,
48
- type PPTVideoElement,
49
- type PPTAudioElement,
50
- type PPTShapeElement,
51
- type PPTChartElement,
52
- } from '@/types/slides'
53
- import type { OperateLineHandlers, OperateResizeHandlers } from '@/types/edit'
54
-
55
- import ImageElementOperate from './ImageElementOperate.vue'
56
- import TextElementOperate from './TextElementOperate.vue'
57
- import ShapeElementOperate from './ShapeElementOperate.vue'
58
- import LineElementOperate from './LineElementOperate.vue'
59
- import TableElementOperate from './TableElementOperate.vue'
60
- import CommonElementOperate from './CommonElementOperate.vue'
61
- import LinkHandler from './LinkHandler.vue'
62
-
63
- const props = defineProps<{
64
- elementInfo: PPTElement
65
- isSelected: boolean
66
- isActive: boolean
67
- isActiveGroupElement: boolean
68
- isMultiSelect: boolean
69
- rotateElement: (e: MouseEvent, element: Exclude<PPTElement, PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement>) => void
70
- scaleElement: (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void
71
- dragLineElement: (e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void
72
- moveShapeKeypoint: (e: MouseEvent, element: PPTShapeElement, index: number) => void
73
- openLinkDialog: () => void
74
- }>()
75
-
76
- const { canvasScale, toolbarState } = storeToRefs(useMainStore())
77
- const { formatedAnimations } = storeToRefs(useSlidesStore())
78
-
79
- const currentOperateComponent = computed<unknown>(() => {
80
- const elementTypeMap = {
81
- [ElementTypes.IMAGE]: ImageElementOperate,
82
- [ElementTypes.TEXT]: TextElementOperate,
83
- [ElementTypes.SHAPE]: ShapeElementOperate,
84
- [ElementTypes.LINE]: LineElementOperate,
85
- [ElementTypes.TABLE]: TableElementOperate,
86
- [ElementTypes.CHART]: CommonElementOperate,
87
- [ElementTypes.LATEX]: CommonElementOperate,
88
- [ElementTypes.VIDEO]: CommonElementOperate,
89
- [ElementTypes.AUDIO]: CommonElementOperate,
90
- }
91
- return elementTypeMap[props.elementInfo.type] || null
92
- })
93
-
94
- const elementIndexListInAnimation = computed(() => {
95
- const indexList = []
96
- for (let i = 0; i < formatedAnimations.value.length; i++) {
97
- const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
98
- if (elIds.includes(props.elementInfo.id)) indexList.push(i)
99
- }
100
- return indexList
101
- })
102
-
103
- const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
104
- const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
105
- </script>
106
-
107
- <style lang="scss" scoped>
108
- .operate {
109
- position: absolute;
110
- z-index: 100;
111
- user-select: none;
112
-
113
- &.multi-select {
114
- opacity: 0.2;
115
- }
116
- }
117
- .animation-index {
118
- position: absolute;
119
- top: 0;
120
- left: -24px;
121
- font-size: 12px;
122
-
123
- .index-item {
124
- width: 18px;
125
- height: 18px;
126
- background-color: #fff;
127
- color: $themeColor;
128
- border: 1px solid $themeColor;
129
- display: flex;
130
- justify-content: center;
131
- align-items: center;
132
-
133
- & + .index-item {
134
- margin-top: 5px;
135
- }
136
- }
137
- }
138
  </style>
 
1
+ <template>
2
+ <div
3
+ class="operate"
4
+ :class="{ 'multi-select': isMultiSelect && !isActive }"
5
+ :style="{
6
+ top: elementInfo.top * canvasScale + 'px',
7
+ left: elementInfo.left * canvasScale + 'px',
8
+ transform: `rotate(${rotate}deg)`,
9
+ transformOrigin: `${elementInfo.width * canvasScale / 2}px ${height * canvasScale / 2}px`,
10
+ }"
11
+ >
12
+ <component
13
+ v-if="isSelected"
14
+ :is="currentOperateComponent"
15
+ :elementInfo="elementInfo"
16
+ :handlerVisible="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)"
17
+ :rotateElement="rotateElement"
18
+ :scaleElement="scaleElement"
19
+ :dragLineElement="dragLineElement"
20
+ :moveShapeKeypoint="moveShapeKeypoint"
21
+ ></component>
22
+
23
+ <div
24
+ class="animation-index"
25
+ v-if="toolbarState === 'elAnimation' && elementIndexListInAnimation.length"
26
+ >
27
+ <div class="index-item" v-for="index in elementIndexListInAnimation" :key="index">{{index + 1}}</div>
28
+ </div>
29
+
30
+ <LinkHandler
31
+ :elementInfo="elementInfo"
32
+ :link="elementInfo.link"
33
+ :openLinkDialog="openLinkDialog"
34
+ v-if="isActive && elementInfo.link"
35
+ @mousedown.stop=""
36
+ />
37
+ </div>
38
+ </template>
39
+
40
+ <script lang="ts" setup>
41
+ import { computed } from 'vue'
42
+ import { storeToRefs } from 'pinia'
43
+ import { useMainStore, useSlidesStore } from '@/store'
44
+ import {
45
+ ElementTypes,
46
+ type PPTElement,
47
+ type PPTLineElement,
48
+ type PPTVideoElement,
49
+ type PPTAudioElement,
50
+ type PPTShapeElement,
51
+ type PPTChartElement,
52
+ } from '@/types/slides'
53
+ import type { OperateLineHandlers, OperateResizeHandlers } from '@/types/edit'
54
+
55
+ import ImageElementOperate from './ImageElementOperate.vue'
56
+ import TextElementOperate from './TextElementOperate.vue'
57
+ import ShapeElementOperate from './ShapeElementOperate.vue'
58
+ import LineElementOperate from './LineElementOperate.vue'
59
+ import TableElementOperate from './TableElementOperate.vue'
60
+ import CommonElementOperate from './CommonElementOperate.vue'
61
+ import LinkHandler from './LinkHandler.vue'
62
+
63
+ const props = defineProps<{
64
+ elementInfo: PPTElement
65
+ isSelected: boolean
66
+ isActive: boolean
67
+ isActiveGroupElement: boolean
68
+ isMultiSelect: boolean
69
+ rotateElement: (e: MouseEvent, element: Exclude<PPTElement, PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement>) => void
70
+ scaleElement: (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void
71
+ dragLineElement: (e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void
72
+ moveShapeKeypoint: (e: MouseEvent, element: PPTShapeElement, index: number) => void
73
+ openLinkDialog: () => void
74
+ }>()
75
+
76
+ const { canvasScale, toolbarState } = storeToRefs(useMainStore())
77
+ const { formatedAnimations } = storeToRefs(useSlidesStore())
78
+
79
+ const currentOperateComponent = computed<unknown>(() => {
80
+ const elementTypeMap = {
81
+ [ElementTypes.IMAGE]: ImageElementOperate,
82
+ [ElementTypes.TEXT]: TextElementOperate,
83
+ [ElementTypes.SHAPE]: ShapeElementOperate,
84
+ [ElementTypes.LINE]: LineElementOperate,
85
+ [ElementTypes.TABLE]: TableElementOperate,
86
+ [ElementTypes.CHART]: CommonElementOperate,
87
+ [ElementTypes.LATEX]: CommonElementOperate,
88
+ [ElementTypes.VIDEO]: CommonElementOperate,
89
+ [ElementTypes.AUDIO]: CommonElementOperate,
90
+ }
91
+ return elementTypeMap[props.elementInfo.type] || null
92
+ })
93
+
94
+ const elementIndexListInAnimation = computed(() => {
95
+ const indexList = []
96
+ for (let i = 0; i < formatedAnimations.value.length; i++) {
97
+ const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
98
+ if (elIds.includes(props.elementInfo.id)) indexList.push(i)
99
+ }
100
+ return indexList
101
+ })
102
+
103
+ const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
104
+ const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
105
+ </script>
106
+
107
+ <style lang="scss" scoped>
108
+ .operate {
109
+ position: absolute;
110
+ z-index: 100;
111
+ user-select: none;
112
+
113
+ &.multi-select {
114
+ opacity: 0.2;
115
+ }
116
+ }
117
+ .animation-index {
118
+ position: absolute;
119
+ top: 0;
120
+ left: -24px;
121
+ font-size: 12px;
122
+
123
+ .index-item {
124
+ width: 18px;
125
+ height: 18px;
126
+ background-color: #fff;
127
+ color: $themeColor;
128
+ border: 1px solid $themeColor;
129
+ display: flex;
130
+ justify-content: center;
131
+ align-items: center;
132
+
133
+ & + .index-item {
134
+ margin-top: 5px;
135
+ }
136
+ }
137
+ }
138
  </style>
frontend/src/views/Editor/ExportDialog/ExportHTML.vue ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="export-html-dialog">
3
+ <div class="configs">
4
+ <div class="row">
5
+ <div class="title">导出范围:</div>
6
+ <RadioGroup
7
+ class="config-item"
8
+ v-model:value="rangeType"
9
+ >
10
+ <RadioButton style="width: 33.33%;" value="all">全部</RadioButton>
11
+ <RadioButton style="width: 33.33%;" value="current">当前页</RadioButton>
12
+ <RadioButton style="width: 33.33%;" value="custom">自定义</RadioButton>
13
+ </RadioGroup>
14
+ </div>
15
+ <div class="row" v-if="rangeType === 'custom'">
16
+ <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">自定义范围:</div>
17
+ <Slider
18
+ class="config-item"
19
+ range
20
+ :min="1"
21
+ :max="slides.length"
22
+ :step="1"
23
+ v-model:value="range"
24
+ />
25
+ </div>
26
+ <div class="row">
27
+ <div class="title">交互功能:</div>
28
+ <Switch class="config-item" v-model:value="includeInteractivity" />
29
+ </div>
30
+ <div class="row">
31
+ <div class="title">包含样式:</div>
32
+ <Switch class="config-item" v-model:value="includeCSS" />
33
+ </div>
34
+ <div class="row">
35
+ <div class="title">独立文件:</div>
36
+ <Switch class="config-item" v-model:value="standalone" />
37
+ </div>
38
+ <div class="tips">
39
+ <div class="tip-item">
40
+ <strong>交互功能:</strong>启用后可以使用键盘导航(方向键、空格键等)和全屏功能
41
+ </div>
42
+ <div class="tip-item">
43
+ <strong>包含样式:</strong>启用后HTML文件包含完整的CSS样式,否则只有基本结构
44
+ </div>
45
+ <div class="tip-item">
46
+ <strong>独立文件:</strong>启用后生成的HTML文件可以在任何环境中独立运行
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="btns">
51
+ <Button class="btn export" type="primary" @click="handleExport">导出 HTML</Button>
52
+ <Button class="btn close" @click="emit('close')">关闭</Button>
53
+ </div>
54
+
55
+ <FullscreenSpin :loading="exporting" tip="正在导出HTML网页..." />
56
+ </div>
57
+ </template>
58
+
59
+ <script lang="ts" setup>
60
+ import { computed, ref } from 'vue'
61
+ import { storeToRefs } from 'pinia'
62
+ import { useSlidesStore } from '@/store'
63
+ import useExport from '@/hooks/useExport'
64
+
65
+ import FullscreenSpin from '@/components/FullscreenSpin.vue'
66
+ import Switch from '@/components/Switch.vue'
67
+ import Slider from '@/components/Slider.vue'
68
+ import Button from '@/components/Button.vue'
69
+ import RadioButton from '@/components/RadioButton.vue'
70
+ import RadioGroup from '@/components/RadioGroup.vue'
71
+
72
+ const emit = defineEmits<{
73
+ (event: 'close'): void
74
+ }>()
75
+
76
+ const { slides, currentSlide } = storeToRefs(useSlidesStore())
77
+ const { exportHTML, exporting } = useExport()
78
+
79
+ const rangeType = ref<'all' | 'current' | 'custom'>('all')
80
+ const range = ref<[number, number]>([1, slides.value.length])
81
+ const includeInteractivity = ref(true)
82
+ const includeCSS = ref(true)
83
+ const standalone = ref(true)
84
+
85
+ const selectedSlides = computed(() => {
86
+ if (rangeType.value === 'all') return slides.value
87
+ if (rangeType.value === 'current') return [currentSlide.value]
88
+ return slides.value.filter((item, index) => {
89
+ const [min, max] = range.value
90
+ return index >= min - 1 && index <= max - 1
91
+ })
92
+ })
93
+
94
+ const handleExport = () => {
95
+ const options = {
96
+ includeInteractivity: includeInteractivity.value,
97
+ includeCSS: includeCSS.value,
98
+ standalone: standalone.value
99
+ }
100
+
101
+ exportHTML(selectedSlides.value, options)
102
+ }
103
+ </script>
104
+
105
+ <style lang="scss" scoped>
106
+ .export-html-dialog {
107
+ height: 100%;
108
+ display: flex;
109
+ justify-content: center;
110
+ align-items: center;
111
+ flex-direction: column;
112
+ position: relative;
113
+ overflow: hidden;
114
+ }
115
+ .configs {
116
+ width: 350px;
117
+ height: calc(100% - 100px);
118
+ display: flex;
119
+ flex-direction: column;
120
+ justify-content: center;
121
+
122
+ .row {
123
+ display: flex;
124
+ justify-content: center;
125
+ align-items: center;
126
+ margin-bottom: 25px;
127
+ }
128
+
129
+ .title {
130
+ width: 100px;
131
+ position: relative;
132
+
133
+ &::after {
134
+ content: attr(data-range);
135
+ position: absolute;
136
+ top: 20px;
137
+ left: 0;
138
+ }
139
+ }
140
+ .config-item {
141
+ flex: 1;
142
+ }
143
+
144
+ .tips {
145
+ margin-top: 20px;
146
+ padding: 15px;
147
+ background: #f8f9fa;
148
+ border-radius: 8px;
149
+ border-left: 4px solid #007bff;
150
+
151
+ .tip-item {
152
+ font-size: 12px;
153
+ color: #666;
154
+ line-height: 1.6;
155
+ margin-bottom: 8px;
156
+
157
+ &:last-child {
158
+ margin-bottom: 0;
159
+ }
160
+
161
+ strong {
162
+ color: #333;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ .btns {
168
+ width: 300px;
169
+ height: 100px;
170
+ display: flex;
171
+ justify-content: center;
172
+ align-items: center;
173
+
174
+ .export {
175
+ flex: 1;
176
+ }
177
+ .close {
178
+ width: 100px;
179
+ margin-left: 10px;
180
+ }
181
+ }
182
+ </style>
frontend/src/views/Editor/ExportDialog/index.vue CHANGED
@@ -23,6 +23,7 @@ import ExportJSON from './ExportJSON.vue'
23
  import ExportPDF from './ExportPDF.vue'
24
  import ExportPPTX from './ExportPPTX.vue'
25
  import ExportSpecificFile from './ExportSpecificFile.vue'
 
26
  import Tabs from '@/components/Tabs.vue'
27
 
28
  interface TabItem {
@@ -38,6 +39,7 @@ const setDialogForExport = mainStore.setDialogForExport
38
  const tabs: TabItem[] = [
39
  { key: 'pptist', label: '导出 pptist 文件' },
40
  { key: 'pptx', label: '导出 PPTX' },
 
41
  { key: 'image', label: '导出图片' },
42
  { key: 'json', label: '导出 JSON' },
43
  { key: 'pdf', label: '打印 / 导出 PDF' },
@@ -50,6 +52,7 @@ const currentDialogComponent = computed<unknown>(() => {
50
  'pdf': ExportPDF,
51
  'pptx': ExportPPTX,
52
  'pptist': ExportSpecificFile,
 
53
  }
54
  if (dialogForExport.value) return dialogMap[dialogForExport.value] || null
55
  return null
 
23
  import ExportPDF from './ExportPDF.vue'
24
  import ExportPPTX from './ExportPPTX.vue'
25
  import ExportSpecificFile from './ExportSpecificFile.vue'
26
+ import ExportHTML from './ExportHTML.vue'
27
  import Tabs from '@/components/Tabs.vue'
28
 
29
  interface TabItem {
 
39
  const tabs: TabItem[] = [
40
  { key: 'pptist', label: '导出 pptist 文件' },
41
  { key: 'pptx', label: '导出 PPTX' },
42
+ { key: 'html', label: '导出 HTML' },
43
  { key: 'image', label: '导出图片' },
44
  { key: 'json', label: '导出 JSON' },
45
  { key: 'pdf', label: '打印 / 导出 PDF' },
 
52
  'pdf': ExportPDF,
53
  'pptx': ExportPPTX,
54
  'pptist': ExportSpecificFile,
55
+ 'html': ExportHTML,
56
  }
57
  if (dialogForExport.value) return dialogMap[dialogForExport.value] || null
58
  return null
frontend/src/views/Login.vue CHANGED
@@ -1,122 +1,122 @@
1
- <template>
2
- <div class="login-container">
3
- <div class="login-form">
4
- <h2>PPTist 登录</h2>
5
- <form @submit.prevent="handleLogin">
6
- <div class="form-group">
7
- <label>用户名</label>
8
- <input
9
- v-model="username"
10
- type="text"
11
- placeholder="请输入用户名"
12
- required
13
- />
14
- </div>
15
- <div class="form-group">
16
- <label>密码</label>
17
- <input
18
- v-model="password"
19
- type="password"
20
- placeholder="请输入密码"
21
- required
22
- />
23
- </div>
24
- <button type="submit" :disabled="loading">
25
- {{ loading ? '登录中...' : '登录' }}
26
- </button>
27
- </form>
28
- </div>
29
- </div>
30
- </template>
31
-
32
- <script setup lang="ts">
33
- import { ref } from 'vue'
34
- import { useAuthStore } from '@/store'
35
- import message from '@/utils/message'
36
-
37
- const authStore = useAuthStore()
38
-
39
- const username = ref('')
40
- const password = ref('')
41
- const loading = ref(false)
42
-
43
- const handleLogin = async () => {
44
- if (!username.value || !password.value) {
45
- message.error('请输入用户名和密码')
46
- return
47
- }
48
-
49
- loading.value = true
50
- try {
51
- await authStore.login(username.value, password.value)
52
- message.success('登录成功')
53
- // 登录成功后,App.vue会自动检测到认证状态变化并显示主界面
54
- } catch (error) {
55
- console.error('登录失败:', error)
56
- } finally {
57
- loading.value = false
58
- }
59
- }
60
- </script>
61
-
62
- <style scoped>
63
- .login-container {
64
- display: flex;
65
- justify-content: center;
66
- align-items: center;
67
- min-height: 100vh;
68
- background: #f5f5f5;
69
- }
70
-
71
- .login-form {
72
- background: white;
73
- padding: 40px;
74
- border-radius: 8px;
75
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
76
- width: 400px;
77
- }
78
-
79
- .login-form h2 {
80
- text-align: center;
81
- margin-bottom: 30px;
82
- color: #333;
83
- }
84
-
85
- .form-group {
86
- margin-bottom: 20px;
87
- }
88
-
89
- .form-group label {
90
- display: block;
91
- margin-bottom: 5px;
92
- color: #555;
93
- }
94
-
95
- .form-group input {
96
- width: 100%;
97
- padding: 10px;
98
- border: 1px solid #ddd;
99
- border-radius: 4px;
100
- font-size: 14px;
101
- }
102
-
103
- button {
104
- width: 100%;
105
- padding: 12px;
106
- background: #5b9bd5;
107
- color: white;
108
- border: none;
109
- border-radius: 4px;
110
- font-size: 16px;
111
- cursor: pointer;
112
- }
113
-
114
- button:hover {
115
- background: #4a8bc2;
116
- }
117
-
118
- button:disabled {
119
- background: #ccc;
120
- cursor: not-allowed;
121
- }
122
  </style>
 
1
+ <template>
2
+ <div class="login-container">
3
+ <div class="login-form">
4
+ <h2>PPTist 登录</h2>
5
+ <form @submit.prevent="handleLogin">
6
+ <div class="form-group">
7
+ <label>用户名</label>
8
+ <input
9
+ v-model="username"
10
+ type="text"
11
+ placeholder="请输入用户名"
12
+ required
13
+ />
14
+ </div>
15
+ <div class="form-group">
16
+ <label>密码</label>
17
+ <input
18
+ v-model="password"
19
+ type="password"
20
+ placeholder="请输入密码"
21
+ required
22
+ />
23
+ </div>
24
+ <button type="submit" :disabled="loading">
25
+ {{ loading ? '登录中...' : '登录' }}
26
+ </button>
27
+ </form>
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { ref } from 'vue'
34
+ import { useAuthStore } from '@/store'
35
+ import message from '@/utils/message'
36
+
37
+ const authStore = useAuthStore()
38
+
39
+ const username = ref('')
40
+ const password = ref('')
41
+ const loading = ref(false)
42
+
43
+ const handleLogin = async () => {
44
+ if (!username.value || !password.value) {
45
+ message.error('请输入用户名和密码')
46
+ return
47
+ }
48
+
49
+ loading.value = true
50
+ try {
51
+ await authStore.login(username.value, password.value)
52
+ message.success('登录成功')
53
+ // 登录成功后,App.vue会自动检测到认证状态变化并显示主界面
54
+ } catch (error) {
55
+ console.error('登录失败:', error)
56
+ } finally {
57
+ loading.value = false
58
+ }
59
+ }
60
+ </script>
61
+
62
+ <style scoped>
63
+ .login-container {
64
+ display: flex;
65
+ justify-content: center;
66
+ align-items: center;
67
+ min-height: 100vh;
68
+ background: #f5f5f5;
69
+ }
70
+
71
+ .login-form {
72
+ background: white;
73
+ padding: 40px;
74
+ border-radius: 8px;
75
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
76
+ width: 400px;
77
+ }
78
+
79
+ .login-form h2 {
80
+ text-align: center;
81
+ margin-bottom: 30px;
82
+ color: #333;
83
+ }
84
+
85
+ .form-group {
86
+ margin-bottom: 20px;
87
+ }
88
+
89
+ .form-group label {
90
+ display: block;
91
+ margin-bottom: 5px;
92
+ color: #555;
93
+ }
94
+
95
+ .form-group input {
96
+ width: 100%;
97
+ padding: 10px;
98
+ border: 1px solid #ddd;
99
+ border-radius: 4px;
100
+ font-size: 14px;
101
+ }
102
+
103
+ button {
104
+ width: 100%;
105
+ padding: 12px;
106
+ background: #5b9bd5;
107
+ color: white;
108
+ border: none;
109
+ border-radius: 4px;
110
+ font-size: 16px;
111
+ cursor: pointer;
112
+ }
113
+
114
+ button:hover {
115
+ background: #4a8bc2;
116
+ }
117
+
118
+ button:disabled {
119
+ background: #ccc;
120
+ cursor: not-allowed;
121
+ }
122
  </style>
frontend/src/views/PublicViewer.vue CHANGED
@@ -1,497 +1,497 @@
1
- <template>
2
- <div class="public-viewer">
3
- <FullscreenSpin v-if="loading" tip="正在加载演示文稿..." loading :mask="false" />
4
-
5
- <div v-else-if="error" class="error-container">
6
- <div class="error-message">
7
- <h3>{{ error }}</h3>
8
- <p>请检查分享链接是否正确或联系分享者</p>
9
- </div>
10
- </div>
11
-
12
- <div v-else class="presentation-container">
13
- <!-- 演示文稿标题 -->
14
- <div class="presentation-header">
15
- <h1>{{ presentationTitle }}</h1>
16
- <div class="controls">
17
- <button @click="toggleFullscreen" class="control-btn">
18
- {{ isFullscreen ? '退出全屏' : '全屏查看' }}
19
- </button>
20
- <button @click="startSlideshow" class="control-btn primary">
21
- 开始演示
22
- </button>
23
- </div>
24
- </div>
25
-
26
- <!-- 幻灯片预览 -->
27
- <div class="slides-container" ref="slidesContainer">
28
- <div
29
- v-for="(slide, index) in slides"
30
- :key="slide.id"
31
- class="slide-preview"
32
- :class="{ active: currentSlideIndex === index }"
33
- @click="currentSlideIndex = index"
34
- >
35
- <div class="slide-number">{{ index + 1 }}</div>
36
- <div class="slide-content">
37
- <SlideThumbnail :slide="slide" />
38
- </div>
39
- </div>
40
- </div>
41
-
42
- <!-- 当前幻灯片显示 -->
43
- <div class="current-slide-container">
44
- <div class="slide-navigation">
45
- <button
46
- @click="previousSlide"
47
- :disabled="currentSlideIndex === 0"
48
- class="nav-btn"
49
- >
50
- 上一页
51
- </button>
52
- <span class="slide-counter">
53
- {{ currentSlideIndex + 1 }} / {{ slides.length }}
54
- </span>
55
- <button
56
- @click="nextSlide"
57
- :disabled="currentSlideIndex === slides.length - 1"
58
- class="nav-btn"
59
- >
60
- 下一页
61
- </button>
62
- </div>
63
-
64
- <div class="current-slide" ref="currentSlideRef">
65
- <Slide
66
- v-if="currentSlide"
67
- :slide="currentSlide"
68
- :editable="false"
69
- class="slide-display"
70
- />
71
- </div>
72
- </div>
73
- </div>
74
-
75
- <!-- 全屏演示模式 -->
76
- <div v-if="isSlideshow" class="slideshow-mode" @keydown="handleKeydown">
77
- <div class="slideshow-container">
78
- <Slide
79
- v-if="currentSlide"
80
- :slide="currentSlide"
81
- :editable="false"
82
- class="slideshow-slide"
83
- />
84
-
85
- <div class="slideshow-controls">
86
- <button @click="exitSlideshow" class="exit-btn">退出演示</button>
87
- <div class="slideshow-navigation">
88
- <button @click="previousSlide" :disabled="currentSlideIndex === 0">←</button>
89
- <span>{{ currentSlideIndex + 1 }} / {{ slides.length }}</span>
90
- <button @click="nextSlide" :disabled="currentSlideIndex === slides.length - 1">→</button>
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
- </template>
97
-
98
- <script lang="ts" setup>
99
- import { ref, onMounted, computed, onUnmounted } from 'vue'
100
- import { useMessage } from '@/hooks/useMessage'
101
- import api from '@/services'
102
- import type { Slide as SlideType } from '@/types/slides'
103
-
104
- import FullscreenSpin from '@/components/FullscreenSpin.vue'
105
- import Slide from '@/views/components/Slide/index.vue'
106
- import SlideThumbnail from '@/views/components/SlideThumbnail/index.vue'
107
-
108
- const { message } = useMessage()
109
-
110
- const loading = ref(true)
111
- const error = ref('')
112
- const slides = ref<SlideType[]>([])
113
- const presentationTitle = ref('')
114
- const currentSlideIndex = ref(0)
115
- const isFullscreen = ref(false)
116
- const isSlideshow = ref(false)
117
- const slidesContainer = ref<HTMLElement>()
118
- const currentSlideRef = ref<HTMLElement>()
119
-
120
- const currentSlide = computed(() => slides.value[currentSlideIndex.value])
121
-
122
- // 从URL获取分享ID
123
- const getShareIdFromUrl = () => {
124
- const url = window.location.href
125
- const match = url.match(/\/public\/view\/([^/?]+)/)
126
- return match ? match[1] : null
127
- }
128
-
129
- // 加载公共演示文稿
130
- const loadPublicPresentation = async () => {
131
- try {
132
- loading.value = true
133
- const shareId = getShareIdFromUrl()
134
-
135
- if (!shareId) {
136
- error.value = '无效的分享链接'
137
- return
138
- }
139
-
140
- const response = await api.get(`/api/public/presentation/${shareId}`)
141
-
142
- if (response.data.success) {
143
- slides.value = response.data.slides
144
- presentationTitle.value = response.data.title || '未命名演示文稿'
145
- } else {
146
- error.value = response.data.message || '加载失败'
147
- }
148
- } catch (err) {
149
- console.error('加载公共演示文稿失败:', err)
150
- error.value = '加载演示文稿时出错,请稍后重试'
151
- } finally {
152
- loading.value = false
153
- }
154
- }
155
-
156
- // 幻灯片导航
157
- const previousSlide = () => {
158
- if (currentSlideIndex.value > 0) {
159
- currentSlideIndex.value--
160
- }
161
- }
162
-
163
- const nextSlide = () => {
164
- if (currentSlideIndex.value < slides.value.length - 1) {
165
- currentSlideIndex.value++
166
- }
167
- }
168
-
169
- // 全屏功能
170
- const toggleFullscreen = () => {
171
- if (!document.fullscreenElement) {
172
- currentSlideRef.value?.requestFullscreen()
173
- isFullscreen.value = true
174
- } else {
175
- document.exitFullscreen()
176
- isFullscreen.value = false
177
- }
178
- }
179
-
180
- // 演示模式
181
- const startSlideshow = () => {
182
- isSlideshow.value = true
183
- currentSlideIndex.value = 0
184
- document.addEventListener('keydown', handleKeydown)
185
- }
186
-
187
- const exitSlideshow = () => {
188
- isSlideshow.value = false
189
- document.removeEventListener('keydown', handleKeydown)
190
- }
191
-
192
- // 键盘事件处理
193
- const handleKeydown = (event: KeyboardEvent) => {
194
- switch (event.key) {
195
- case 'ArrowLeft':
196
- case 'ArrowUp':
197
- previousSlide()
198
- break
199
- case 'ArrowRight':
200
- case 'ArrowDown':
201
- case ' ':
202
- nextSlide()
203
- break
204
- case 'Escape':
205
- exitSlideshow()
206
- break
207
- }
208
- }
209
-
210
- // 全屏状态监听
211
- const handleFullscreenChange = () => {
212
- isFullscreen.value = !!document.fullscreenElement
213
- }
214
-
215
- onMounted(() => {
216
- loadPublicPresentation()
217
- document.addEventListener('fullscreenchange', handleFullscreenChange)
218
- })
219
-
220
- onUnmounted(() => {
221
- document.removeEventListener('keydown', handleKeydown)
222
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
223
- })
224
- </script>
225
-
226
- <style lang="scss" scoped>
227
- .public-viewer {
228
- width: 100%;
229
- height: 100vh;
230
- background: #f5f5f5;
231
- }
232
-
233
- .error-container {
234
- display: flex;
235
- align-items: center;
236
- justify-content: center;
237
- height: 100vh;
238
-
239
- .error-message {
240
- text-align: center;
241
- padding: 2rem;
242
- background: white;
243
- border-radius: 8px;
244
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
245
-
246
- h3 {
247
- color: #ff4757;
248
- margin-bottom: 1rem;
249
- }
250
-
251
- p {
252
- color: #666;
253
- }
254
- }
255
- }
256
-
257
- .presentation-container {
258
- display: flex;
259
- flex-direction: column;
260
- height: 100vh;
261
- }
262
-
263
- .presentation-header {
264
- display: flex;
265
- justify-content: space-between;
266
- align-items: center;
267
- padding: 1rem 2rem;
268
- background: white;
269
- border-bottom: 1px solid #e0e0e0;
270
-
271
- h1 {
272
- margin: 0;
273
- color: #333;
274
- font-size: 1.5rem;
275
- }
276
-
277
- .controls {
278
- display: flex;
279
- gap: 1rem;
280
- }
281
-
282
- .control-btn {
283
- padding: 0.5rem 1rem;
284
- border: 1px solid #ddd;
285
- border-radius: 4px;
286
- background: white;
287
- cursor: pointer;
288
- transition: all 0.2s;
289
-
290
- &:hover {
291
- background: #f8f9fa;
292
- }
293
-
294
- &.primary {
295
- background: #007bff;
296
- color: white;
297
- border-color: #007bff;
298
-
299
- &:hover {
300
- background: #0056b3;
301
- }
302
- }
303
- }
304
- }
305
-
306
- .slides-container {
307
- flex: 1;
308
- display: flex;
309
- flex-wrap: wrap;
310
- gap: 1rem;
311
- padding: 1rem;
312
- overflow-y: auto;
313
- max-height: 200px;
314
- }
315
-
316
- .slide-preview {
317
- position: relative;
318
- width: 200px;
319
- height: 150px;
320
- border: 2px solid #ddd;
321
- border-radius: 8px;
322
- cursor: pointer;
323
- transition: all 0.2s;
324
- background: white;
325
-
326
- &:hover {
327
- border-color: #007bff;
328
- box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
329
- }
330
-
331
- &.active {
332
- border-color: #007bff;
333
- box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
334
- }
335
-
336
- .slide-number {
337
- position: absolute;
338
- top: 8px;
339
- left: 8px;
340
- background: rgba(0, 0, 0, 0.7);
341
- color: white;
342
- padding: 2px 8px;
343
- border-radius: 4px;
344
- font-size: 0.8rem;
345
- z-index: 10;
346
- }
347
-
348
- .slide-content {
349
- width: 100%;
350
- height: 100%;
351
- padding: 8px;
352
- }
353
- }
354
-
355
- .current-slide-container {
356
- flex: 1;
357
- display: flex;
358
- flex-direction: column;
359
- padding: 1rem;
360
- background: white;
361
- }
362
-
363
- .slide-navigation {
364
- display: flex;
365
- justify-content: center;
366
- align-items: center;
367
- gap: 1rem;
368
- margin-bottom: 1rem;
369
-
370
- .nav-btn {
371
- padding: 0.5rem 1rem;
372
- border: 1px solid #ddd;
373
- border-radius: 4px;
374
- background: white;
375
- cursor: pointer;
376
-
377
- &:disabled {
378
- opacity: 0.5;
379
- cursor: not-allowed;
380
- }
381
-
382
- &:not(:disabled):hover {
383
- background: #f8f9fa;
384
- }
385
- }
386
-
387
- .slide-counter {
388
- font-weight: 500;
389
- color: #666;
390
- }
391
- }
392
-
393
- .current-slide {
394
- flex: 1;
395
- display: flex;
396
- justify-content: center;
397
- align-items: center;
398
-
399
- .slide-display {
400
- max-width: 90%;
401
- max-height: 90%;
402
- }
403
- }
404
-
405
- .slideshow-mode {
406
- position: fixed;
407
- top: 0;
408
- left: 0;
409
- width: 100vw;
410
- height: 100vh;
411
- background: black;
412
- z-index: 9999;
413
- }
414
-
415
- .slideshow-container {
416
- position: relative;
417
- width: 100%;
418
- height: 100%;
419
- display: flex;
420
- justify-content: center;
421
- align-items: center;
422
- }
423
-
424
- .slideshow-slide {
425
- max-width: 95%;
426
- max-height: 95%;
427
- }
428
-
429
- .slideshow-controls {
430
- position: absolute;
431
- bottom: 2rem;
432
- left: 50%;
433
- transform: translateX(-50%);
434
- display: flex;
435
- align-items: center;
436
- gap: 2rem;
437
-
438
- .exit-btn {
439
- padding: 0.5rem 1rem;
440
- background: rgba(255, 255, 255, 0.1);
441
- color: white;
442
- border: 1px solid rgba(255, 255, 255, 0.3);
443
- border-radius: 4px;
444
- cursor: pointer;
445
-
446
- &:hover {
447
- background: rgba(255, 255, 255, 0.2);
448
- }
449
- }
450
-
451
- .slideshow-navigation {
452
- display: flex;
453
- align-items: center;
454
- gap: 1rem;
455
- color: white;
456
-
457
- button {
458
- padding: 0.5rem;
459
- background: rgba(255, 255, 255, 0.1);
460
- color: white;
461
- border: 1px solid rgba(255, 255, 255, 0.3);
462
- border-radius: 4px;
463
- cursor: pointer;
464
-
465
- &:disabled {
466
- opacity: 0.5;
467
- cursor: not-allowed;
468
- }
469
-
470
- &:not(:disabled):hover {
471
- background: rgba(255, 255, 255, 0.2);
472
- }
473
- }
474
- }
475
- }
476
-
477
- @media (max-width: 768px) {
478
- .presentation-header {
479
- flex-direction: column;
480
- gap: 1rem;
481
-
482
- .controls {
483
- width: 100%;
484
- justify-content: center;
485
- }
486
- }
487
-
488
- .slides-container {
489
- max-height: 120px;
490
- }
491
-
492
- .slide-preview {
493
- width: 120px;
494
- height: 90px;
495
- }
496
- }
497
  </style>
 
1
+ <template>
2
+ <div class="public-viewer">
3
+ <FullscreenSpin v-if="loading" tip="正在加载演示文稿..." loading :mask="false" />
4
+
5
+ <div v-else-if="error" class="error-container">
6
+ <div class="error-message">
7
+ <h3>{{ error }}</h3>
8
+ <p>请检查分享链接是否正确或联系分享者</p>
9
+ </div>
10
+ </div>
11
+
12
+ <div v-else class="presentation-container">
13
+ <!-- 演示文稿标题 -->
14
+ <div class="presentation-header">
15
+ <h1>{{ presentationTitle }}</h1>
16
+ <div class="controls">
17
+ <button @click="toggleFullscreen" class="control-btn">
18
+ {{ isFullscreen ? '退出全屏' : '全屏查看' }}
19
+ </button>
20
+ <button @click="startSlideshow" class="control-btn primary">
21
+ 开始演示
22
+ </button>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- 幻灯片预览 -->
27
+ <div class="slides-container" ref="slidesContainer">
28
+ <div
29
+ v-for="(slide, index) in slides"
30
+ :key="slide.id"
31
+ class="slide-preview"
32
+ :class="{ active: currentSlideIndex === index }"
33
+ @click="currentSlideIndex = index"
34
+ >
35
+ <div class="slide-number">{{ index + 1 }}</div>
36
+ <div class="slide-content">
37
+ <SlideThumbnail :slide="slide" />
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- 当前幻灯片显示 -->
43
+ <div class="current-slide-container">
44
+ <div class="slide-navigation">
45
+ <button
46
+ @click="previousSlide"
47
+ :disabled="currentSlideIndex === 0"
48
+ class="nav-btn"
49
+ >
50
+ 上一页
51
+ </button>
52
+ <span class="slide-counter">
53
+ {{ currentSlideIndex + 1 }} / {{ slides.length }}
54
+ </span>
55
+ <button
56
+ @click="nextSlide"
57
+ :disabled="currentSlideIndex === slides.length - 1"
58
+ class="nav-btn"
59
+ >
60
+ 下一页
61
+ </button>
62
+ </div>
63
+
64
+ <div class="current-slide" ref="currentSlideRef">
65
+ <Slide
66
+ v-if="currentSlide"
67
+ :slide="currentSlide"
68
+ :editable="false"
69
+ class="slide-display"
70
+ />
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- 全屏演示模式 -->
76
+ <div v-if="isSlideshow" class="slideshow-mode" @keydown="handleKeydown">
77
+ <div class="slideshow-container">
78
+ <Slide
79
+ v-if="currentSlide"
80
+ :slide="currentSlide"
81
+ :editable="false"
82
+ class="slideshow-slide"
83
+ />
84
+
85
+ <div class="slideshow-controls">
86
+ <button @click="exitSlideshow" class="exit-btn">退出演示</button>
87
+ <div class="slideshow-navigation">
88
+ <button @click="previousSlide" :disabled="currentSlideIndex === 0">←</button>
89
+ <span>{{ currentSlideIndex + 1 }} / {{ slides.length }}</span>
90
+ <button @click="nextSlide" :disabled="currentSlideIndex === slides.length - 1">→</button>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </template>
97
+
98
+ <script lang="ts" setup>
99
+ import { ref, onMounted, computed, onUnmounted } from 'vue'
100
+ import { useMessage } from '@/hooks/useMessage'
101
+ import api from '@/services'
102
+ import type { Slide as SlideType } from '@/types/slides'
103
+
104
+ import FullscreenSpin from '@/components/FullscreenSpin.vue'
105
+ import Slide from '@/views/components/Slide/index.vue'
106
+ import SlideThumbnail from '@/views/components/SlideThumbnail/index.vue'
107
+
108
+ const { message } = useMessage()
109
+
110
+ const loading = ref(true)
111
+ const error = ref('')
112
+ const slides = ref<SlideType[]>([])
113
+ const presentationTitle = ref('')
114
+ const currentSlideIndex = ref(0)
115
+ const isFullscreen = ref(false)
116
+ const isSlideshow = ref(false)
117
+ const slidesContainer = ref<HTMLElement>()
118
+ const currentSlideRef = ref<HTMLElement>()
119
+
120
+ const currentSlide = computed(() => slides.value[currentSlideIndex.value])
121
+
122
+ // 从URL获取分享ID
123
+ const getShareIdFromUrl = () => {
124
+ const url = window.location.href
125
+ const match = url.match(/\/public\/view\/([^/?]+)/)
126
+ return match ? match[1] : null
127
+ }
128
+
129
+ // 加载公共演示文稿
130
+ const loadPublicPresentation = async () => {
131
+ try {
132
+ loading.value = true
133
+ const shareId = getShareIdFromUrl()
134
+
135
+ if (!shareId) {
136
+ error.value = '无效的分享链接'
137
+ return
138
+ }
139
+
140
+ const response = await api.get(`/api/public/presentation/${shareId}`)
141
+
142
+ if (response.data.success) {
143
+ slides.value = response.data.slides
144
+ presentationTitle.value = response.data.title || '未命名演示文稿'
145
+ } else {
146
+ error.value = response.data.message || '加载失败'
147
+ }
148
+ } catch (err) {
149
+ console.error('加载公共演示文稿失败:', err)
150
+ error.value = '加载演示文稿时出错,请稍后重试'
151
+ } finally {
152
+ loading.value = false
153
+ }
154
+ }
155
+
156
+ // 幻灯片导航
157
+ const previousSlide = () => {
158
+ if (currentSlideIndex.value > 0) {
159
+ currentSlideIndex.value--
160
+ }
161
+ }
162
+
163
+ const nextSlide = () => {
164
+ if (currentSlideIndex.value < slides.value.length - 1) {
165
+ currentSlideIndex.value++
166
+ }
167
+ }
168
+
169
+ // 全屏功能
170
+ const toggleFullscreen = () => {
171
+ if (!document.fullscreenElement) {
172
+ currentSlideRef.value?.requestFullscreen()
173
+ isFullscreen.value = true
174
+ } else {
175
+ document.exitFullscreen()
176
+ isFullscreen.value = false
177
+ }
178
+ }
179
+
180
+ // 演示模式
181
+ const startSlideshow = () => {
182
+ isSlideshow.value = true
183
+ currentSlideIndex.value = 0
184
+ document.addEventListener('keydown', handleKeydown)
185
+ }
186
+
187
+ const exitSlideshow = () => {
188
+ isSlideshow.value = false
189
+ document.removeEventListener('keydown', handleKeydown)
190
+ }
191
+
192
+ // 键盘事件处理
193
+ const handleKeydown = (event: KeyboardEvent) => {
194
+ switch (event.key) {
195
+ case 'ArrowLeft':
196
+ case 'ArrowUp':
197
+ previousSlide()
198
+ break
199
+ case 'ArrowRight':
200
+ case 'ArrowDown':
201
+ case ' ':
202
+ nextSlide()
203
+ break
204
+ case 'Escape':
205
+ exitSlideshow()
206
+ break
207
+ }
208
+ }
209
+
210
+ // 全屏状态监听
211
+ const handleFullscreenChange = () => {
212
+ isFullscreen.value = !!document.fullscreenElement
213
+ }
214
+
215
+ onMounted(() => {
216
+ loadPublicPresentation()
217
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
218
+ })
219
+
220
+ onUnmounted(() => {
221
+ document.removeEventListener('keydown', handleKeydown)
222
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
223
+ })
224
+ </script>
225
+
226
+ <style lang="scss" scoped>
227
+ .public-viewer {
228
+ width: 100%;
229
+ height: 100vh;
230
+ background: #f5f5f5;
231
+ }
232
+
233
+ .error-container {
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ height: 100vh;
238
+
239
+ .error-message {
240
+ text-align: center;
241
+ padding: 2rem;
242
+ background: white;
243
+ border-radius: 8px;
244
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
245
+
246
+ h3 {
247
+ color: #ff4757;
248
+ margin-bottom: 1rem;
249
+ }
250
+
251
+ p {
252
+ color: #666;
253
+ }
254
+ }
255
+ }
256
+
257
+ .presentation-container {
258
+ display: flex;
259
+ flex-direction: column;
260
+ height: 100vh;
261
+ }
262
+
263
+ .presentation-header {
264
+ display: flex;
265
+ justify-content: space-between;
266
+ align-items: center;
267
+ padding: 1rem 2rem;
268
+ background: white;
269
+ border-bottom: 1px solid #e0e0e0;
270
+
271
+ h1 {
272
+ margin: 0;
273
+ color: #333;
274
+ font-size: 1.5rem;
275
+ }
276
+
277
+ .controls {
278
+ display: flex;
279
+ gap: 1rem;
280
+ }
281
+
282
+ .control-btn {
283
+ padding: 0.5rem 1rem;
284
+ border: 1px solid #ddd;
285
+ border-radius: 4px;
286
+ background: white;
287
+ cursor: pointer;
288
+ transition: all 0.2s;
289
+
290
+ &:hover {
291
+ background: #f8f9fa;
292
+ }
293
+
294
+ &.primary {
295
+ background: #007bff;
296
+ color: white;
297
+ border-color: #007bff;
298
+
299
+ &:hover {
300
+ background: #0056b3;
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ .slides-container {
307
+ flex: 1;
308
+ display: flex;
309
+ flex-wrap: wrap;
310
+ gap: 1rem;
311
+ padding: 1rem;
312
+ overflow-y: auto;
313
+ max-height: 200px;
314
+ }
315
+
316
+ .slide-preview {
317
+ position: relative;
318
+ width: 200px;
319
+ height: 150px;
320
+ border: 2px solid #ddd;
321
+ border-radius: 8px;
322
+ cursor: pointer;
323
+ transition: all 0.2s;
324
+ background: white;
325
+
326
+ &:hover {
327
+ border-color: #007bff;
328
+ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
329
+ }
330
+
331
+ &.active {
332
+ border-color: #007bff;
333
+ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
334
+ }
335
+
336
+ .slide-number {
337
+ position: absolute;
338
+ top: 8px;
339
+ left: 8px;
340
+ background: rgba(0, 0, 0, 0.7);
341
+ color: white;
342
+ padding: 2px 8px;
343
+ border-radius: 4px;
344
+ font-size: 0.8rem;
345
+ z-index: 10;
346
+ }
347
+
348
+ .slide-content {
349
+ width: 100%;
350
+ height: 100%;
351
+ padding: 8px;
352
+ }
353
+ }
354
+
355
+ .current-slide-container {
356
+ flex: 1;
357
+ display: flex;
358
+ flex-direction: column;
359
+ padding: 1rem;
360
+ background: white;
361
+ }
362
+
363
+ .slide-navigation {
364
+ display: flex;
365
+ justify-content: center;
366
+ align-items: center;
367
+ gap: 1rem;
368
+ margin-bottom: 1rem;
369
+
370
+ .nav-btn {
371
+ padding: 0.5rem 1rem;
372
+ border: 1px solid #ddd;
373
+ border-radius: 4px;
374
+ background: white;
375
+ cursor: pointer;
376
+
377
+ &:disabled {
378
+ opacity: 0.5;
379
+ cursor: not-allowed;
380
+ }
381
+
382
+ &:not(:disabled):hover {
383
+ background: #f8f9fa;
384
+ }
385
+ }
386
+
387
+ .slide-counter {
388
+ font-weight: 500;
389
+ color: #666;
390
+ }
391
+ }
392
+
393
+ .current-slide {
394
+ flex: 1;
395
+ display: flex;
396
+ justify-content: center;
397
+ align-items: center;
398
+
399
+ .slide-display {
400
+ max-width: 90%;
401
+ max-height: 90%;
402
+ }
403
+ }
404
+
405
+ .slideshow-mode {
406
+ position: fixed;
407
+ top: 0;
408
+ left: 0;
409
+ width: 100vw;
410
+ height: 100vh;
411
+ background: black;
412
+ z-index: 9999;
413
+ }
414
+
415
+ .slideshow-container {
416
+ position: relative;
417
+ width: 100%;
418
+ height: 100%;
419
+ display: flex;
420
+ justify-content: center;
421
+ align-items: center;
422
+ }
423
+
424
+ .slideshow-slide {
425
+ max-width: 95%;
426
+ max-height: 95%;
427
+ }
428
+
429
+ .slideshow-controls {
430
+ position: absolute;
431
+ bottom: 2rem;
432
+ left: 50%;
433
+ transform: translateX(-50%);
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 2rem;
437
+
438
+ .exit-btn {
439
+ padding: 0.5rem 1rem;
440
+ background: rgba(255, 255, 255, 0.1);
441
+ color: white;
442
+ border: 1px solid rgba(255, 255, 255, 0.3);
443
+ border-radius: 4px;
444
+ cursor: pointer;
445
+
446
+ &:hover {
447
+ background: rgba(255, 255, 255, 0.2);
448
+ }
449
+ }
450
+
451
+ .slideshow-navigation {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 1rem;
455
+ color: white;
456
+
457
+ button {
458
+ padding: 0.5rem;
459
+ background: rgba(255, 255, 255, 0.1);
460
+ color: white;
461
+ border: 1px solid rgba(255, 255, 255, 0.3);
462
+ border-radius: 4px;
463
+ cursor: pointer;
464
+
465
+ &:disabled {
466
+ opacity: 0.5;
467
+ cursor: not-allowed;
468
+ }
469
+
470
+ &:not(:disabled):hover {
471
+ background: rgba(255, 255, 255, 0.2);
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ @media (max-width: 768px) {
478
+ .presentation-header {
479
+ flex-direction: column;
480
+ gap: 1rem;
481
+
482
+ .controls {
483
+ width: 100%;
484
+ justify-content: center;
485
+ }
486
+ }
487
+
488
+ .slides-container {
489
+ max-height: 120px;
490
+ }
491
+
492
+ .slide-preview {
493
+ width: 120px;
494
+ height: 90px;
495
+ }
496
+ }
497
  </style>
frontend/src/views/components/element/ChartElement/BaseChartElement.vue CHANGED
@@ -1,68 +1,68 @@
1
- <template>
2
- <div class="base-element-chart"
3
- :class="{ 'is-thumbnail': target === 'thumbnail' }"
4
- :style="{
5
- top: elementInfo.top + 'px',
6
- left: elementInfo.left + 'px',
7
- width: elementInfo.width + 'px',
8
- height: elementInfo.height + 'px',
9
- }"
10
- >
11
- <div
12
- class="rotate-wrapper"
13
- :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
14
- >
15
- <div
16
- class="element-content"
17
- :style="{
18
- backgroundColor: elementInfo.fill,
19
- }"
20
- >
21
- <ElementOutline
22
- :width="elementInfo.width"
23
- :height="elementInfo.height"
24
- :outline="elementInfo.outline"
25
- />
26
- <Chart
27
- :width="elementInfo.width"
28
- :height="elementInfo.height"
29
- :type="elementInfo.chartType"
30
- :data="elementInfo.data"
31
- :themeColors="elementInfo.themeColors"
32
- :textColor="elementInfo.textColor"
33
- :options="elementInfo.options"
34
- />
35
- </div>
36
- </div>
37
- </div>
38
- </template>
39
-
40
- <script lang="ts" setup>
41
- import type { PPTChartElement } from '@/types/slides'
42
-
43
- import ElementOutline from '@/views/components/element/ElementOutline.vue'
44
- import Chart from './Chart.vue'
45
-
46
- defineProps<{
47
- elementInfo: PPTChartElement
48
- target?: string
49
- }>()
50
- </script>
51
-
52
- <style lang="scss" scoped>
53
- .base-element-chart {
54
- position: absolute;
55
-
56
- &.is-thumbnail {
57
- pointer-events: none;
58
- }
59
- }
60
- .rotate-wrapper {
61
- width: 100%;
62
- height: 100%;
63
- }
64
- .element-content {
65
- width: 100%;
66
- height: 100%;
67
- }
68
  </style>
 
1
+ <template>
2
+ <div class="base-element-chart"
3
+ :class="{ 'is-thumbnail': target === 'thumbnail' }"
4
+ :style="{
5
+ top: elementInfo.top + 'px',
6
+ left: elementInfo.left + 'px',
7
+ width: elementInfo.width + 'px',
8
+ height: elementInfo.height + 'px',
9
+ }"
10
+ >
11
+ <div
12
+ class="rotate-wrapper"
13
+ :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
14
+ >
15
+ <div
16
+ class="element-content"
17
+ :style="{
18
+ backgroundColor: elementInfo.fill,
19
+ }"
20
+ >
21
+ <ElementOutline
22
+ :width="elementInfo.width"
23
+ :height="elementInfo.height"
24
+ :outline="elementInfo.outline"
25
+ />
26
+ <Chart
27
+ :width="elementInfo.width"
28
+ :height="elementInfo.height"
29
+ :type="elementInfo.chartType"
30
+ :data="elementInfo.data"
31
+ :themeColors="elementInfo.themeColors"
32
+ :textColor="elementInfo.textColor"
33
+ :options="elementInfo.options"
34
+ />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script lang="ts" setup>
41
+ import type { PPTChartElement } from '@/types/slides'
42
+
43
+ import ElementOutline from '@/views/components/element/ElementOutline.vue'
44
+ import Chart from './Chart.vue'
45
+
46
+ defineProps<{
47
+ elementInfo: PPTChartElement
48
+ target?: string
49
+ }>()
50
+ </script>
51
+
52
+ <style lang="scss" scoped>
53
+ .base-element-chart {
54
+ position: absolute;
55
+
56
+ &.is-thumbnail {
57
+ pointer-events: none;
58
+ }
59
+ }
60
+ .rotate-wrapper {
61
+ width: 100%;
62
+ height: 100%;
63
+ }
64
+ .element-content {
65
+ width: 100%;
66
+ height: 100%;
67
+ }
68
  </style>
frontend/src/views/components/element/ChartElement/index.vue CHANGED
@@ -1,88 +1,88 @@
1
- <template>
2
- <div class="editable-element-chart"
3
- :class="{ 'lock': elementInfo.lock }"
4
- :style="{
5
- top: elementInfo.top + 'px',
6
- left: elementInfo.left + 'px',
7
- width: elementInfo.width + 'px',
8
- height: elementInfo.height + 'px',
9
- }"
10
- >
11
- <div
12
- class="rotate-wrapper"
13
- :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
14
- >
15
- <div
16
- class="element-content"
17
- :style="{
18
- backgroundColor: elementInfo.fill,
19
- }"
20
- v-contextmenu="contextmenus"
21
- @mousedown="$event => handleSelectElement($event)"
22
- @touchstart="$event => handleSelectElement($event)"
23
- @dblclick="openDataEditor()"
24
- >
25
- <ElementOutline
26
- :width="elementInfo.width"
27
- :height="elementInfo.height"
28
- :outline="elementInfo.outline"
29
- />
30
- <Chart
31
- :width="elementInfo.width"
32
- :height="elementInfo.height"
33
- :type="elementInfo.chartType"
34
- :data="elementInfo.data"
35
- :themeColors="elementInfo.themeColors"
36
- :textColor="elementInfo.textColor"
37
- :options="elementInfo.options"
38
- />
39
- </div>
40
- </div>
41
- </div>
42
- </template>
43
-
44
- <script lang="ts" setup>
45
- import type { PPTChartElement } from '@/types/slides'
46
- import type { ContextmenuItem } from '@/components/Contextmenu/types'
47
- import emitter, { EmitterEvents } from '@/utils/emitter'
48
-
49
- import ElementOutline from '@/views/components/element/ElementOutline.vue'
50
- import Chart from './Chart.vue'
51
-
52
- const props = defineProps<{
53
- elementInfo: PPTChartElement
54
- selectElement: (e: MouseEvent | TouchEvent, element: PPTChartElement, canMove?: boolean) => void
55
- contextmenus: () => ContextmenuItem[] | null
56
- }>()
57
-
58
- const handleSelectElement = (e: MouseEvent | TouchEvent) => {
59
- if (props.elementInfo.lock) return
60
- e.stopPropagation()
61
-
62
- props.selectElement(e, props.elementInfo)
63
- }
64
-
65
- const openDataEditor = () => {
66
- emitter.emit(EmitterEvents.OPEN_CHART_DATA_EDITOR)
67
- }
68
- </script>
69
-
70
- <style lang="scss" scoped>
71
- .editable-element-chart {
72
- position: absolute;
73
-
74
- &.lock .element-content {
75
- cursor: default;
76
- }
77
- }
78
- .rotate-wrapper {
79
- width: 100%;
80
- height: 100%;
81
- }
82
- .element-content {
83
- width: 100%;
84
- height: 100%;
85
- overflow: hidden;
86
- cursor: move;
87
- }
88
- </style>
 
1
+ <template>
2
+ <div class="editable-element-chart"
3
+ :class="{ 'lock': elementInfo.lock }"
4
+ :style="{
5
+ top: elementInfo.top + 'px',
6
+ left: elementInfo.left + 'px',
7
+ width: elementInfo.width + 'px',
8
+ height: elementInfo.height + 'px',
9
+ }"
10
+ >
11
+ <div
12
+ class="rotate-wrapper"
13
+ :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
14
+ >
15
+ <div
16
+ class="element-content"
17
+ :style="{
18
+ backgroundColor: elementInfo.fill,
19
+ }"
20
+ v-contextmenu="contextmenus"
21
+ @mousedown="$event => handleSelectElement($event)"
22
+ @touchstart="$event => handleSelectElement($event)"
23
+ @dblclick="openDataEditor()"
24
+ >
25
+ <ElementOutline
26
+ :width="elementInfo.width"
27
+ :height="elementInfo.height"
28
+ :outline="elementInfo.outline"
29
+ />
30
+ <Chart
31
+ :width="elementInfo.width"
32
+ :height="elementInfo.height"
33
+ :type="elementInfo.chartType"
34
+ :data="elementInfo.data"
35
+ :themeColors="elementInfo.themeColors"
36
+ :textColor="elementInfo.textColor"
37
+ :options="elementInfo.options"
38
+ />
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <script lang="ts" setup>
45
+ import type { PPTChartElement } from '@/types/slides'
46
+ import type { ContextmenuItem } from '@/components/Contextmenu/types'
47
+ import emitter, { EmitterEvents } from '@/utils/emitter'
48
+
49
+ import ElementOutline from '@/views/components/element/ElementOutline.vue'
50
+ import Chart from './Chart.vue'
51
+
52
+ const props = defineProps<{
53
+ elementInfo: PPTChartElement
54
+ selectElement: (e: MouseEvent | TouchEvent, element: PPTChartElement, canMove?: boolean) => void
55
+ contextmenus: () => ContextmenuItem[] | null
56
+ }>()
57
+
58
+ const handleSelectElement = (e: MouseEvent | TouchEvent) => {
59
+ if (props.elementInfo.lock) return
60
+ e.stopPropagation()
61
+
62
+ props.selectElement(e, props.elementInfo)
63
+ }
64
+
65
+ const openDataEditor = () => {
66
+ emitter.emit(EmitterEvents.OPEN_CHART_DATA_EDITOR)
67
+ }
68
+ </script>
69
+
70
+ <style lang="scss" scoped>
71
+ .editable-element-chart {
72
+ position: absolute;
73
+
74
+ &.lock .element-content {
75
+ cursor: default;
76
+ }
77
+ }
78
+ .rotate-wrapper {
79
+ width: 100%;
80
+ height: 100%;
81
+ }
82
+ .element-content {
83
+ width: 100%;
84
+ height: 100%;
85
+ overflow: hidden;
86
+ cursor: move;
87
+ }
88
+ </style>
frontend/src/views/components/element/ShapeElement/GradientDefs.vue CHANGED
@@ -1,30 +1,30 @@
1
- <template>
2
- <linearGradient
3
- v-if="type === 'linear'"
4
- :id="id"
5
- x1="0%"
6
- y1="0%"
7
- x2="100%"
8
- y2="0%"
9
- :gradientTransform="`rotate(${rotate},0.5,0.5)`"
10
- >
11
- <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
12
- </linearGradient>
13
-
14
- <radialGradient :id="id" v-else>
15
- <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
16
- </radialGradient>
17
- </template>
18
-
19
- <script lang="ts" setup>
20
- import type { GradientColor, GradientType } from '@/types/slides'
21
-
22
- withDefaults(defineProps<{
23
- id: string
24
- type: GradientType
25
- colors: GradientColor[]
26
- rotate?: number
27
- }>(), {
28
- rotate: 0,
29
- })
30
  </script>
 
1
+ <template>
2
+ <linearGradient
3
+ v-if="type === 'linear'"
4
+ :id="id"
5
+ x1="0%"
6
+ y1="0%"
7
+ x2="100%"
8
+ y2="0%"
9
+ :gradientTransform="`rotate(${rotate},0.5,0.5)`"
10
+ >
11
+ <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
12
+ </linearGradient>
13
+
14
+ <radialGradient :id="id" v-else>
15
+ <stop v-for="(item, index) in colors" :key="index" :offset="`${item.pos}%`" :stop-color="item.color" />
16
+ </radialGradient>
17
+ </template>
18
+
19
+ <script lang="ts" setup>
20
+ import type { GradientColor, GradientType } from '@/types/slides'
21
+
22
+ withDefaults(defineProps<{
23
+ id: string
24
+ type: GradientType
25
+ colors: GradientColor[]
26
+ rotate?: number
27
+ }>(), {
28
+ rotate: 0,
29
+ })
30
  </script>