|
|
@@ -0,0 +1,394 @@
|
|
|
+<template>
|
|
|
+ <div class="canvas-tool">
|
|
|
+ <div class="left-handler">
|
|
|
+ <Popover trigger="click" v-model:value="textTypeSelectVisible"
|
|
|
+ style="height: 100%;display: flex;align-items: center;" :offset="10">
|
|
|
+ <template #content>
|
|
|
+ <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }">
|
|
|
+ <IconTextRotationNone /> {{ lang.ssTextHorizontal }}
|
|
|
+ </PopoverMenuItem>
|
|
|
+ <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }">
|
|
|
+ <IconTextRotationDown /> {{ lang.ssTextVertical }}
|
|
|
+ </PopoverMenuItem>
|
|
|
+ </template>
|
|
|
+ <div class="handler-item">
|
|
|
+ <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
|
|
|
+ <span>{{ lang.ssText }}</span>
|
|
|
+ </div>
|
|
|
+ </Popover>
|
|
|
+ <Popover trigger="click" style="height: 100%;display: flex;align-items: center;"
|
|
|
+ v-model:value="picturePoolVisible" :offset="10">
|
|
|
+ <template #content>
|
|
|
+ <FileInput @change="files => insertImageElement(files)">
|
|
|
+ <div class="popover-item">
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
|
+ <polyline points="17 8 12 3 7 8" />
|
|
|
+ <line x1="12" y1="3" x2="12" y2="15" />
|
|
|
+ </svg>
|
|
|
+ <span>{{ lang.ssUploadFromLocal }}</span>
|
|
|
+ </div>
|
|
|
+ </FileInput>
|
|
|
+ <!-- <div class="popover-item">
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <circle cx="11" cy="11" r="8" />
|
|
|
+ <path d="M21 21l-4.35-4.35" />
|
|
|
+ </svg>
|
|
|
+ <span>{{ lang.ssSearchFromWeb }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="popover-item">
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
|
|
|
+ <path d="M2 17l10 5 10-5" />
|
|
|
+ <path d="M2 12l10 5 10-5" />
|
|
|
+ </svg>
|
|
|
+ <span>{{ lang.ssGenerateFromAI }}</span>
|
|
|
+ </div> -->
|
|
|
+ </template>
|
|
|
+ <div class="handler-item">
|
|
|
+ <IconPicture class="icon" v-tooltip="lang.ssInsertImage" />
|
|
|
+ <span>{{ lang.ssImage }}</span>
|
|
|
+ </div>
|
|
|
+ <!-- <FileInput @change="files => insertImageElement(files)">
|
|
|
+ </FileInput> -->
|
|
|
+ </Popover>
|
|
|
+ <Popover trigger="click" style="height: 100%;display: flex;align-items: center;" v-model:value="shapePoolVisible"
|
|
|
+ :offset="10">
|
|
|
+ <template #content>
|
|
|
+ <ShapePool @select="shape => drawShape(shape)" />
|
|
|
+ </template>
|
|
|
+ <div class="handler-item">
|
|
|
+ <IconGraphicDesign class="icon"
|
|
|
+ :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
|
|
|
+ <span>{{ lang.ssShape }}</span>
|
|
|
+ </div>
|
|
|
+ </Popover>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <Modal v-model:visible="latexEditorVisible" :width="880">
|
|
|
+ <LaTeXEditor @close="latexEditorVisible = false"
|
|
|
+ @update="data => { createLatexElement(data); latexEditorVisible = false }" />
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import { storeToRefs } from 'pinia'
|
|
|
+import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
|
|
|
+import { getImageDataURL } from '@/utils/image'
|
|
|
+import useImport from '@/hooks/useImport'
|
|
|
+import type { ShapePoolItem } from '@/configs/shapes'
|
|
|
+import type { LinePoolItem } from '@/configs/lines'
|
|
|
+import useScaleCanvas from '@/hooks/useScaleCanvas'
|
|
|
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
|
|
+import useCreateElement from '@/hooks/useCreateElement'
|
|
|
+import { lang } from '@/main'
|
|
|
+
|
|
|
+import ShapePool from './ShapePool.vue'
|
|
|
+import LinePool from './LinePool.vue'
|
|
|
+import ChartPool from './ChartPool.vue'
|
|
|
+import TableGenerator from './TableGenerator.vue'
|
|
|
+import MediaInput from './MediaInput.vue'
|
|
|
+import WebpageInput from './WebpageInput.vue'
|
|
|
+import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
|
|
|
+import FileInput from '@/components/FileInput.vue'
|
|
|
+import Modal from '@/components/Modal.vue'
|
|
|
+import Divider from '@/components/Divider.vue'
|
|
|
+import Popover from '@/components/Popover.vue'
|
|
|
+import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
|
|
|
+
|
|
|
+const mainStore = useMainStore()
|
|
|
+const slidesStore = useSlidesStore()
|
|
|
+const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
|
|
|
+const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
|
|
|
+const { currentSlide } = storeToRefs(slidesStore)
|
|
|
+
|
|
|
+const getInitialViewMode = () => {
|
|
|
+ const urlParams = new URLSearchParams(window.location.search)
|
|
|
+ const modeFromUrl = urlParams.get('mode')
|
|
|
+ if (modeFromUrl === 'editor2') {
|
|
|
+ return 'editor2'
|
|
|
+ }
|
|
|
+ const modeFromStorage = localStorage.getItem('viewMode')
|
|
|
+ if (modeFromStorage) {
|
|
|
+ return modeFromStorage
|
|
|
+ }
|
|
|
+ return 'editor'
|
|
|
+}
|
|
|
+
|
|
|
+const viewMode = computed(() => getInitialViewMode())
|
|
|
+
|
|
|
+const hasInteractiveTool = computed(() => {
|
|
|
+ const elements = currentSlide.value?.elements || []
|
|
|
+ return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
|
|
|
+})
|
|
|
+
|
|
|
+const editTool = () => {
|
|
|
+ const elements = currentSlide.value?.elements || []
|
|
|
+ const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
|
|
|
+ if (frameElement) {
|
|
|
+ const url = frameElement.url || ''
|
|
|
+
|
|
|
+ interface ParentWindowWithToolList extends Window {
|
|
|
+ toolBtn?: (action: number, id: string) => void;
|
|
|
+ }
|
|
|
+ const parentWindow = window.parent as ParentWindowWithToolList
|
|
|
+ parentWindow?.toolBtn?.(0, url)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const { redo, undo } = useHistorySnapshot()
|
|
|
+
|
|
|
+const {
|
|
|
+ scaleCanvas,
|
|
|
+ setCanvasScalePercentage,
|
|
|
+ resetCanvas,
|
|
|
+ canvasScalePercentage,
|
|
|
+} = useScaleCanvas()
|
|
|
+
|
|
|
+const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
|
|
|
+const canvasScaleVisible = ref(false)
|
|
|
+
|
|
|
+const applyCanvasPresetScale = (value: number) => {
|
|
|
+ setCanvasScalePercentage(value)
|
|
|
+ canvasScaleVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const {
|
|
|
+ createImageElement,
|
|
|
+ createChartElement,
|
|
|
+ createTableElement,
|
|
|
+ createLatexElement,
|
|
|
+ createVideoElement,
|
|
|
+ createAudioElement,
|
|
|
+ createFrameElement,
|
|
|
+} = useCreateElement()
|
|
|
+
|
|
|
+const { uploadFileToS3 } = useImport()
|
|
|
+const insertImageElement = async (files: FileList) => {
|
|
|
+ const imageFile = files[0]
|
|
|
+ if (!imageFile) return
|
|
|
+ const url = await uploadFileToS3(imageFile)
|
|
|
+ console.log(url)
|
|
|
+
|
|
|
+ // getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
|
|
|
+ createImageElement(url)
|
|
|
+ picturePoolVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const shapePoolVisible = ref(false)
|
|
|
+const picturePoolVisible = ref(false)
|
|
|
+const linePoolVisible = ref(false)
|
|
|
+const chartPoolVisible = ref(false)
|
|
|
+const tableGeneratorVisible = ref(false)
|
|
|
+const mediaInputVisible = ref(false)
|
|
|
+const webpageInputVisible = ref(false)
|
|
|
+
|
|
|
+// 预设的网页列表
|
|
|
+const webpageList = ref<Array<{ type: number, title: string, url: string }>>([])
|
|
|
+
|
|
|
+// 点击插入学习内容时获取数据
|
|
|
+const handleInsertLearningContent = () => {
|
|
|
+ try {
|
|
|
+ // 获取父窗口的工具列表,如果没有则使用空数组
|
|
|
+ // 定义父窗口的类型,添加pptToolList属性
|
|
|
+ interface ParentWindowWithToolList extends Window {
|
|
|
+ pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
|
|
|
+ }
|
|
|
+ const parentWindow = window.parent as ParentWindowWithToolList
|
|
|
+ const pptToolList = parentWindow?.pptToolList || []
|
|
|
+ // 转换父窗口的工具列表格式
|
|
|
+ webpageList.value = pptToolList.map((item: any) => ({
|
|
|
+ type: item.tool || 0,
|
|
|
+ title: item.title || lang.ssUnknownTool,
|
|
|
+ url: item.url || '#'
|
|
|
+ })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
|
|
|
+
|
|
|
+ console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
|
|
|
+
|
|
|
+ // 显示弹窗
|
|
|
+ webpageInputVisible.value = true
|
|
|
+
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('加载学习内容失败:', error)
|
|
|
+
|
|
|
+ // 发生错误时使用空数组
|
|
|
+ webpageList.value = []
|
|
|
+
|
|
|
+ // 显示弹窗
|
|
|
+ webpageInputVisible.value = true
|
|
|
+ }
|
|
|
+}
|
|
|
+const latexEditorVisible = ref(false)
|
|
|
+const textTypeSelectVisible = ref(false)
|
|
|
+const shapeMenuVisible = ref(false)
|
|
|
+const moreVisible = ref(false)
|
|
|
+
|
|
|
+// 绘制文字范围
|
|
|
+const drawText = (vertical = false) => {
|
|
|
+ mainStore.setCreatingElement({
|
|
|
+ type: 'text',
|
|
|
+ vertical,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制形状范围
|
|
|
+const drawShape = (shape: ShapePoolItem) => {
|
|
|
+ mainStore.setCreatingElement({
|
|
|
+ type: 'shape',
|
|
|
+ data: shape,
|
|
|
+ })
|
|
|
+ shapePoolVisible.value = false
|
|
|
+}
|
|
|
+// 绘制自定义任意多边形
|
|
|
+const drawCustomShape = () => {
|
|
|
+ mainStore.setCreatingCustomShapeState(true)
|
|
|
+ shapePoolVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制线条路径
|
|
|
+const drawLine = (line: LinePoolItem) => {
|
|
|
+ mainStore.setCreatingElement({
|
|
|
+ type: 'line',
|
|
|
+ data: line,
|
|
|
+ })
|
|
|
+ linePoolVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 打开选择面板
|
|
|
+const toggleSelectPanel = () => {
|
|
|
+ mainStore.setSelectPanelState(!showSelectPanel.value)
|
|
|
+}
|
|
|
+
|
|
|
+// 打开搜索替换面板
|
|
|
+const toggleSraechPanel = () => {
|
|
|
+ mainStore.setSearchPanelState(!showSearchPanel.value)
|
|
|
+}
|
|
|
+
|
|
|
+// 打开批注面板
|
|
|
+const toggleNotesPanel = () => {
|
|
|
+ mainStore.setNotesPanelState(!showNotesPanel.value)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.canvas-tool {
|
|
|
+ position: relative;
|
|
|
+ border-bottom: 1px solid $borderColor;
|
|
|
+ background-color: #fff;
|
|
|
+ display: flex;
|
|
|
+ padding: 0 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.left-handler {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ .handler-item {
|
|
|
+ // width: 32px;
|
|
|
+ background-color: #f9fafb;
|
|
|
+ border-radius: 5px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:not(.group-btn):hover {
|
|
|
+ background-color: #f1f1f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ color: $themeColor;
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon {
|
|
|
+ margin-right: 5px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.handler-item {
|
|
|
+ height: 35px;
|
|
|
+ font-size: 14px;
|
|
|
+ // margin: 0 2px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ border-radius: $borderRadius;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.disable {
|
|
|
+ opacity: .5;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.left-handler,
|
|
|
+.right-handler {
|
|
|
+ .handler-item {
|
|
|
+ padding: 0 15px;
|
|
|
+
|
|
|
+ &.active,
|
|
|
+ &:not(.disable):hover {
|
|
|
+ background-color: #f1f1f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.right-handler {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .text {
|
|
|
+ display: inline-block;
|
|
|
+ width: 40px;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .viewport-size {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.edit-tool-btn {
|
|
|
+ color: #285cf5;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.popover-item {
|
|
|
+ min-width: 80px;
|
|
|
+ padding: 10px 10px;
|
|
|
+ // border-radius: $borderRadius;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ margin-right: 5px;
|
|
|
+ width: 1em;
|
|
|
+ height: 1em;
|
|
|
+ }
|
|
|
+ &.center {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ }
|
|
|
+ & + .popover-menu-item {
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|