| <template> | |
| <div class="canvas-tool"> | |
| <div class="left-handler"> | |
| <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销(Ctrl + Z)'" @click="undo()" /> | |
| <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做(Ctrl + Y)'" @click="redo()" /> | |
| <div class="more"> | |
| <Divider type="vertical" style="height: 20px;" /> | |
| <Popover class="more-icon" trigger="click" v-model:value="moreVisible" :offset="10"> | |
| <template #content> | |
| <PopoverMenuItem center @click="toggleNotesPanel(); moreVisible = false">批注面板</PopoverMenuItem> | |
| <PopoverMenuItem center @click="toggleSelectPanel(); moreVisible = false">选择窗格</PopoverMenuItem> | |
| <PopoverMenuItem center @click="toggleSraechPanel(); moreVisible = false">查找替换</PopoverMenuItem> | |
| </template> | |
| <IconMore class="handler-item" /> | |
| </Popover> | |
| <IconComment class="handler-item" :class="{ 'active': showNotesPanel }" v-tooltip="'批注面板'" @click="toggleNotesPanel()" /> | |
| <IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="'选择窗格'" @click="toggleSelectPanel()" /> | |
| <IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="'查找/替换(Ctrl + F)'" @click="toggleSraechPanel()" /> | |
| </div> | |
| </div> | |
| <div class="add-element-handler"> | |
| <div class="handler-item group-btn" v-tooltip="'插入文字'"> | |
| <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" /> | |
| <Popover trigger="click" v-model:value="textTypeSelectVisible" style="height: 100%;" :offset="10"> | |
| <template #content> | |
| <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</PopoverMenuItem> | |
| <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</PopoverMenuItem> | |
| </template> | |
| <IconDown class="arrow" /> | |
| </Popover> | |
| </div> | |
| <div class="handler-item group-btn" v-tooltip="'插入形状'" :offset="10"> | |
| <Popover trigger="click" style="height: 100%;" v-model:value="shapePoolVisible" :offset="10"> | |
| <template #content> | |
| <ShapePool @select="shape => drawShape(shape)" /> | |
| </template> | |
| <IconGraphicDesign class="icon" :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" /> | |
| </Popover> | |
| <Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10"> | |
| <template #content> | |
| <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem> | |
| </template> | |
| <IconDown class="arrow" /> | |
| </Popover> | |
| </div> | |
| <FileInput @change="files => insertImageElement(files)"> | |
| <IconPicture class="handler-item" v-tooltip="'插入图片'" /> | |
| </FileInput> | |
| <Popover trigger="click" v-model:value="linePoolVisible" :offset="10"> | |
| <template #content> | |
| <LinePool @select="line => drawLine(line)" /> | |
| </template> | |
| <IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" v-tooltip="'插入线条'" /> | |
| </Popover> | |
| <Popover trigger="click" v-model:value="chartPoolVisible" :offset="10"> | |
| <template #content> | |
| <ChartPool @select="chart => { createChartElement(chart); chartPoolVisible = false }" /> | |
| </template> | |
| <IconChartProportion class="handler-item" v-tooltip="'插入图表'" /> | |
| </Popover> | |
| <Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10"> | |
| <template #content> | |
| <TableGenerator | |
| @close="tableGeneratorVisible = false" | |
| @insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }" | |
| /> | |
| </template> | |
| <IconInsertTable class="handler-item" v-tooltip="'插入表格'" /> | |
| </Popover> | |
| <IconFormula class="handler-item" v-tooltip="'插入公式'" @click="latexEditorVisible = true" /> | |
| <Popover trigger="click" v-model:value="mediaInputVisible" :offset="10"> | |
| <template #content> | |
| <MediaInput | |
| @close="mediaInputVisible = false" | |
| @insertVideo="src => { createVideoElement(src); mediaInputVisible = false }" | |
| @insertAudio="src => { createAudioElement(src); mediaInputVisible = false }" | |
| /> | |
| </template> | |
| <IconVideoTwo class="handler-item" v-tooltip="'插入音视频'" /> | |
| </Popover> | |
| </div> | |
| <div class="right-handler"> | |
| <IconMinus class="handler-item viewport-size" v-tooltip="'画布缩小(Ctrl + -)'" @click="scaleCanvas('-')" /> | |
| <Popover trigger="click" v-model:value="canvasScaleVisible"> | |
| <template #content> | |
| <PopoverMenuItem | |
| center | |
| v-for="item in canvasScalePresetList" | |
| :key="item" | |
| @click="applyCanvasPresetScale(item)" | |
| >{{item}}%</PopoverMenuItem> | |
| <PopoverMenuItem center @click="resetCanvas(); canvasScaleVisible = false">适应屏幕</PopoverMenuItem> | |
| </template> | |
| <span class="text">{{canvasScalePercentage}}</span> | |
| </Popover> | |
| <IconPlus class="handler-item viewport-size" v-tooltip="'画布放大(Ctrl + =)'" @click="scaleCanvas('+')" /> | |
| <IconFullScreen class="handler-item viewport-size-adaptation" v-tooltip="'适应屏幕(Ctrl + 0)'" @click="resetCanvas()" /> | |
| </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 } from 'vue' | |
| import { storeToRefs } from 'pinia' | |
| import { useMainStore, useSnapshotStore } from '@/store' | |
| import { getImageDataURL } from '@/utils/image' | |
| 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 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 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 { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore) | |
| const { canUndo, canRedo } = storeToRefs(useSnapshotStore()) | |
| 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, | |
| } = useCreateElement() | |
| const insertImageElement = (files: FileList) => { | |
| const imageFile = files[0] | |
| if (!imageFile) return | |
| getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL)) | |
| } | |
| const shapePoolVisible = ref(false) | |
| const linePoolVisible = ref(false) | |
| const chartPoolVisible = ref(false) | |
| const tableGeneratorVisible = ref(false) | |
| const mediaInputVisible = ref(false) | |
| 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; | |
| justify-content: space-between; | |
| padding: 0 10px; | |
| font-size: 13px; | |
| user-select: none; | |
| } | |
| .left-handler, .more { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .more-icon { | |
| display: none; | |
| } | |
| .add-element-handler { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| display: flex; | |
| .handler-item { | |
| width: 32px; | |
| &:not(.group-btn):hover { | |
| background-color: #f1f1f1; | |
| } | |
| &.active { | |
| color: $themeColor; | |
| } | |
| &.group-btn { | |
| width: auto; | |
| margin-right: 5px; | |
| &:hover { | |
| background-color: #f3f3f3; | |
| } | |
| .icon, .arrow { | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .icon { | |
| width: 26px; | |
| padding: 0 2px; | |
| &:hover { | |
| background-color: #e9e9e9; | |
| } | |
| &.active { | |
| color: $themeColor; | |
| } | |
| } | |
| .arrow { | |
| font-size: 12px; | |
| &:hover { | |
| background-color: #e9e9e9; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| .handler-item { | |
| height: 30px; | |
| 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 8px; | |
| &.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; | |
| } | |
| } | |
| @media screen and (width <= 1200px) { | |
| .right-handler .text { | |
| display: none; | |
| } | |
| .more > .handler-item { | |
| display: none; | |
| } | |
| .more-icon { | |
| display: block; | |
| } | |
| } | |
| @media screen and (width <= 1000px) { | |
| .left-handler, .right-handler { | |
| display: none; | |
| } | |
| } | |
| </style> |