| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867 |
- <template>
- <div class="canvas-tool">
- <div class="left-handler">
- <!-- <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="lang.ssUndoTip" @click="undo()" v-if="!isFrame" />
- <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="lang.ssRedoTip" @click="redo()" v-if="!isFrame" /> -->
- <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>
- <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
- <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polyline points="9 11 12 14 22 4"></polyline>
- <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
- </svg>
- <span>{{ lang.ssVote }}</span>
- </div>
- <div class="popover-item" @click="editContent(79)" v-if="frametype != 79">
- <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
- <circle cx="12" cy="13" r="4"></circle>
- </svg>
- <span>{{ lang.ssPhoto }}</span>
- </div>
- </template>
- <div class="handler-item" :class="{ active: toolVisible }">
- <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>
- <svg v-if="frametype == 78" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polyline points="9 11 12 14 22 4"></polyline>
- <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
- </svg>
- <svg v-if="frametype == 79" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
- <circle cx="12" cy="13" r="4"></circle>
- </svg>
- </span>
- <span>{{ iframeLabel }}</span>
- <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
- p-id="4735" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12">
- <path
- d="M562.5 771c-14.3 14.3-33.7 27.5-52 23.5-18.4 3.1-35.7-11.2-50-23.5L18.8 327.3c-22.4-22.4-22.4-59.2 0-81.6s59.2-22.4 81.6 0L511.5 668l412.1-422.3c22.4-22.4 59.2-22.4 81.6 0s22.4 59.2 0 81.6L562.5 771z"
- p-id="4736" fill="currentColor"></path>
- </svg>
- </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>
- <RichTextBase v-if="handleElement?.type == 'text'"/>
- <div style="display: flex; align-items: center;" v-if="handleElement?.type == 'shape'">
- <Select
- style="flex: 1;"
- :value="fillType"
- @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
- :options="[
- { label: lang.ssSolidFill, value: 'fill' },
- { label: lang.ssGradFill, value: 'gradient' },
- // { label: lang.ssImgFill, value: 'pattern' },
- ]"
- />
- <Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
- <template #content>
- <ColorPicker
- :modelValue="fill"
- @update:modelValue="value => updateFill(value)"
- />
- </template>
- <ColorButton style="width: 100px;" :color="fill" />
- </Popover>
- <Select
- style="flex: 1;"
- :value="gradient.type"
- @update:value="value => updateGradient({ type: value as GradientType })"
- v-else-if="fillType === 'gradient'"
- :options="[
- { label: lang.ssLinearGrad, value: 'linear' },
- { label: lang.ssRadialGrad, value: 'radial' },
- ]"
- />
- </div>
- <template v-if="fillType === 'gradient' && handleElement?.type == 'shape'">
- <div class="row">
- <GradientBar
- :value="gradient.colors"
- :index="currentGradientIndex"
- @update:value="value => updateGradient({ colors: value })"
- @update:index="index => currentGradientIndex = index"
- />
- </div>
- <div class="row">
- <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
- <Popover trigger="click" style="width: 60%;">
- <template #content>
- <ColorPicker
- :modelValue="gradient.colors[currentGradientIndex].color"
- @update:modelValue="value => updateGradientColors(value)"
- />
- </template>
- <ColorButton :color="gradient.colors[currentGradientIndex].color" />
- </Popover>
- </div>
- <div class="row" v-if="gradient.type === 'linear'">
- <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
- <Slider
- style="width: 60%;"
- :min="0"
- :max="360"
- :step="15"
- :value="gradient.rotate"
- @update:value="value => updateGradient({ rotate: value as number })"
- />
- </div>
- </template>
- <ElementFlip v-if="handleElement?.type == 'shape'"/>
- <!-- 英语口语工具:重置预览 -->
- <div
- v-if="canResetSpeakingPreview"
- class="handler-item reset-preview-btn"
- @click="handleResetSpeakingPreview"
- >
- <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor"
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <polyline points="23 4 23 10 17 10" />
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
- </svg>
- <span>重置预览</span>
- </div>
- </div>
- <Modal v-model:visible="latexEditorVisible" :width="880">
- <LaTeXEditor @close="latexEditorVisible = false"
- @update="data => { createLatexElement(data); latexEditorVisible = false }" />
- </Modal>
- <!-- 确认对话框 -->
- <Modal :visible="confirmDialogVisible" :width="400" :contentStyle="{ borderRadius: '15px' }" :closeButton="false"
- :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">
- <div class="btn-c" @click="handleCancel">{{ lang.ssCancel }}</div>
- <div class="btn-c confirm" @click="handleConfirm">{{ lang.ssApply }}</div>
- </div>
- </div>
- </Modal>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, computed, watch } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
- import { useSpeakingStore } from '@/store/speaking'
- 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'
- import RichTextBase from '@/views/Editor/Toolbar/common/RichTextBase2.vue'
- import Select from '@/components/Select.vue'
- import ColorPicker from '@/components/ColorPicker/index.vue'
- import GradientBar from '@/components/GradientBar.vue'
- import Slider from '@/components/Slider.vue'
- import ColorButton from '@/components/ColorButton.vue'
- import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.vue'
- const mainStore = useMainStore()
- const slidesStore = useSlidesStore()
- const speakingStore = useSpeakingStore()
- const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
- const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
- const { currentSlide } = storeToRefs(slidesStore)
- const { previewState } = storeToRefs(speakingStore)
- // 当前 slide 是否包含英语口语工具(toolType=77)
- const hasEnglishSpeakingTool = computed(() => {
- const elements = currentSlide.value?.elements || []
- return elements.some((el: any) => el.type === 'frame' && Number(el.toolType) === 77)
- })
- // 仅当存在 toolType=77 且预览不在 ready 阶段时,显示"重置预览"按钮
- const canResetSpeakingPreview = computed(
- () => hasEnglishSpeakingTool.value && previewState.value !== 'ready'
- )
- const handleResetSpeakingPreview = () => {
- speakingStore.requestResetPreview()
- }
- 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 || el.toolType === 78 || el.toolType === 79))
- })
- 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,
- 81: lang.ssHPage,
- 74: lang.ssVideo,
- 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
- 76: lang.ssCreative,
- 77: lang.ssEnglishSpeakingTool,
- 78: lang.ssVote,
- 79: lang.ssPhoto,
- }
- 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 || el.toolType === 78 || el.toolType === 79))
- parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
- toolVisible.value = false
- })
- }
- import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
- const { handleElement, handleElementId } = storeToRefs(useMainStore())
- const fillType = ref('fill')
- const fill = ref<string>('#000')
- const textAlign = ref('middle')
- const pattern = ref('')
- import emitter, { EmitterEvents } from '@/utils/emitter'
- const { addHistorySnapshot } = useHistorySnapshot()
- const currentGradientIndex = ref(0)
- watch(handleElementId, () => {
- currentGradientIndex.value = 0
- })
- const updateElement = (props: Partial<PPTShapeElement>) => {
- slidesStore.updateElement({ id: handleElementId.value, props })
- addHistorySnapshot()
- }
- const gradient = ref<Gradient>({
- type: 'linear',
- rotate: 0,
- colors: [
- { pos: 0, color: '#fff' },
- { pos: 100, color: '#fff' },
- ],
- })
- watch(handleElement, () => {
- if (!handleElement.value || handleElement.value.type !== 'shape') return
- fill.value = handleElement.value.fill || '#fff'
- const defaultGradientColor = [
- { pos: 0, color: fill.value },
- { pos: 100, color: '#fff' },
- ]
- gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
- pattern.value = handleElement.value.pattern || ''
- fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
- textAlign.value = handleElement.value?.text?.align || 'middle'
- if (handleElement.value.text?.content) {
- emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
- }
- }, { deep: true, immediate: true })
- // 设置填充类型:渐变、纯色
- const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
- console.log('设置填充类型:', type)
- if (type === 'fill') {
- slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
- addHistorySnapshot()
- }
- else if (type === 'gradient') {
- currentGradientIndex.value = 0
- slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
- updateElement({ gradient: gradient.value })
- }
- // else if (type === 'pattern') {
- // slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
- // updateElement({ pattern: '' })
- // }
- }
- // 设置填充色
- const updateFill = (value: string) => {
- updateElement({ fill: value })
- }
- // 设置渐变填充
- const updateGradient = (gradientProps: Partial<Gradient>) => {
- if (!gradient.value) return
- const _gradient = { ...gradient.value, ...gradientProps }
- updateElement({ gradient: _gradient })
- }
- const updateGradientColors = (color: string) => {
- const colors = gradient.value.colors.map((item, index) => {
- if (index === currentGradientIndex.value) return { ...item, color }
- return item
- })
- updateGradient({ colors })
- }
- </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,
- .right-handler {
- .handler-item {
- padding: 0 15px;
- &:not(.disable):hover,
- &:not(.active):hover {
- background-color: #f1f1f1;
- }
- }
- }
- .reset-preview-btn {
- gap: 5px;
- color: #6b7280;
- svg {
- width: 14px;
- height: 14px;
- }
- &:hover {
- color: #f97316;
- }
- }
- .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;
- color: #000;
- border: 1px solid #f59f4a;
- background: #fef7ef !important;
- font-weight: 600;
- }
- .icon {
- margin-right: 5px;
- }
- .svg-icon {
- margin-right: 5px;
- display: flex;
- }
- .xia-icon {
- margin-left: 5px;
- width: 10px;
- height: 10px;
- }
- }
- }
- .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;
- }
- }
- .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;
- background-color: #fff5e5;
- color: #f89c46;
- }
- &+.popover-menu-item {
- margin-top: 2px;
- }
- }
- .btn-c {
- display: inline-block;
- line-height: 1;
- white-space: nowrap;
- cursor: pointer;
- background: #FFF;
- border: 1px solid #DCDFE6;
- color: #606266;
- text-align: center;
- box-sizing: border-box;
- outline: 0;
- margin: 0;
- transition: .1s;
- font-weight: 500;
- padding: 12px 20px;
- font-size: 14px;
- border-radius: 10px;
- +.btn-c {
- margin-left: 10px;
- }
- &.confirm {
- background: #FF9400;
- color: white;
- border: 1px solid #FF9400;
- &:hover {
- background: #FFA500;
- }
- }
- }
- .row {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 180px;
- }
- </style>
|