| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578 |
- <template>
- <div class="canvas-tool">
- <div class="left-handler">
- <Popover trigger="click" v-model:value="toolVisible"
- style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="hasInteractiveTool">
- <template #content>
- <div class="popover-item" @click="editContent(45)" v-if="frametype != 45">
- <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10" />
- <path d="M12 16v-4m0-4h.01" />
- </svg>
- <span>{{ lang.ssChoiceQ }}</span>
- </div>
- <div class="popover-item" @click="editContent(15)" v-if="frametype != 15">
- <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
- </svg>
- <span>{{ lang.ssQandA }}</span>
- </div>
- </template>
- <div class="handler-item">
- <span class="svg-icon">
- <svg v-if="frametype == 45" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10" />
- <path d="M12 16v-4m0-4h.01" />
- </svg>
- <svg v-if="frametype == 15" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
- </svg>
- </span>
- <span>{{ iframeLabel }}</span>
- </div>
- </Popover>
- <Popover trigger="click" v-model:value="textTypeSelectVisible"
- style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="!isFrame">
- <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" v-if="!isFrame">
- <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" v-if="!isFrame">
- <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>
- <!-- 确认对话框 -->
- <Modal
- :visible="confirmDialogVisible"
- :width="420"
- :closeButton="true"
- :closeOnClickMask="false"
- :closeOnEsc="false"
- @update:visible="val => confirmDialogVisible = val"
- >
- <div class="clear-confirm">
- <div class="clear-confirm__title">{{ lang.ssConfirmOperation }}</div>
- <div class="clear-confirm__content">
- {{ lang.ssClearToolContent }}
- </div>
- <div class="clear-confirm__footer">
- <Button type="default" @click="handleCancel">{{ lang.ssCancel }}</Button>
- <Button type="primary" @click="handleConfirm">{{ lang.ssApply }}</Button>
- </div>
- </div>
- </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'
- import Button from '@/components/Button.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 iframeLabel = computed(() => {
- return getTypeLabel(frametype.value)
- })
- const frametype = computed(() => {
- const elements = currentSlide.value?.elements || []
- return hasInteractiveTool.value ? elements[0].type === 'frame' ? elements[0]?.toolType || 0 : 0 : 0
- })
- // 获取类型标签
- const getTypeLabel = (type: number) => {
- const typeMap: Record<number, string> = {
- 45: lang.ssChoiceQ,
- 15: lang.ssQATest,
- 72: lang.ssAiApp,
- 73: lang.ssHPage,
- 74: lang.ssVideo,
- 75: lang.ssBiliVideo,
- 76: lang.ssCreative
- }
- return typeMap[type] || lang.ssUnknown
- }
- const isFrame = computed(() => {
- const elements = currentSlide.value?.elements || []
- return elements.some((el: any) => el.type === 'frame')
- })
- 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 toolVisible = 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)
- }
- interface ContentItem {
- tool?: number
- title?: string
- url?: string
- id?: string
- }
- // 确认对话框状态
- const confirmDialogVisible = ref(false)
- const confirmDialogCallback = ref<(() => void) | null>(null)
- const showConfirmDialog = (callback: () => void) => {
- confirmDialogCallback.value = callback
- confirmDialogVisible.value = true
- }
- const handleConfirm = () => {
- confirmDialogCallback.value?.()
- confirmDialogVisible.value = false
- confirmDialogCallback.value = null
- }
- const handleCancel = () => {
- confirmDialogVisible.value = false
- confirmDialogCallback.value = null
- }
- const editContent = (toolType: number) => {
- showConfirmDialog(() => {
- interface ParentWindowWithToolList extends Window {
- toolBtn2?: (action: number, id: string, toolType: number) => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- const elements = currentSlide.value?.elements || []
- const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
- parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
- toolVisible.value = false
- })
- }
- </script>
- <style lang="scss" scoped>
- .confirm-dialog-content {
- padding: 20px;
- text-align: center;
- h3 {
- margin: 0 0 16px;
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- p {
- margin: 0 0 24px;
- font-size: 14px;
- color: #666;
- line-height: 1.5;
- }
- .confirm-dialog-buttons {
- display: flex;
- justify-content: center;
- gap: 16px;
- button {
- padding: 8px 24px;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s;
- &.cancel-btn {
- background: #f5f5f5;
- color: #666;
- border: 1px solid #e0e0e0;
- &:hover {
- background: #e8e8e8;
- }
- }
- &.confirm-btn {
- background: #FF9300;
- color: white;
- border: 1px solid #FF9300;
- &:hover {
- background: #e68a00;
- }
- }
- }
- }
- }
- .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;
- }
- .svg-icon {
- margin-right: 5px;
- display: flex;
- }
- }
- }
- .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>
|