2023-09-10 13:00:56 +08:00

377 lines
10 KiB
Vue

<template>
<div class="canvas-tool">
<div class="left-handler">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="撤销">
<IconBack class="handler-item" :class="{ 'disable': !canUndo }" @click="undo()" />
</Tooltip>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="重做">
<IconNext class="handler-item" :class="{ 'disable': !canRedo }" @click="redo()" />
</Tooltip>
<Divider type="vertical" style="height: 20px;" />
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="选择窗格" @click="openSelectPanel()">
<IconMoveOne class="handler-item" />
</Tooltip>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="查找/替换" @click="openSraechPanel()">
<IconSearch class="handler-item" />
</Tooltip>
</div>
<div class="add-element-handler">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入文字" :align="{ offset: [0, 0] }">
<div class="handler-item group-btn">
<IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
<Popover trigger="click" v-model:open="textTypeSelectVisible">
<template #content>
<div class="text-type-item" @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</div>
<div class="text-type-item" @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</div>
</template>
<IconDown class="arrow" />
</Popover>
</div>
</Tooltip>
<FileInput @change="files => insertImageElement(files)">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入图片" :align="{ offset: [0, 0] }">
<IconPicture class="handler-item" />
</Tooltip>
</FileInput>
<Popover trigger="click" v-model:open="shapePoolVisible">
<template #content>
<ShapePool @select="shape => drawShape(shape)" />
</template>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入形状" :align="{ offset: [0, 0] }">
<IconGraphicDesign class="handler-item" :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
</Tooltip>
</Popover>
<Popover trigger="click" v-model:open="linePoolVisible">
<template #content>
<LinePool @select="line => drawLine(line)" />
</template>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入线条" :align="{ offset: [0, 0] }">
<IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" />
</Tooltip>
</Popover>
<Popover trigger="click" v-model:open="chartPoolVisible">
<template #content>
<ChartPool @select="chart => { createChartElement(chart); chartPoolVisible = false }" />
</template>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入图表" :align="{ offset: [0, 0] }">
<IconChartProportion class="handler-item" />
</Tooltip>
</Popover>
<Popover trigger="click" v-model:open="tableGeneratorVisible">
<template #content>
<TableGenerator
@close="tableGeneratorVisible = false"
@insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"
/>
</template>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入表格" :align="{ offset: [0, 0] }">
<IconInsertTable class="handler-item" />
</Tooltip>
</Popover>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入公式" :align="{ offset: [0, 0] }">
<IconFormula class="handler-item" @click="latexEditorVisible = true" />
</Tooltip>
<Popover trigger="click" v-model:open="mediaInputVisible">
<template #content>
<MediaInput
@close="mediaInputVisible = false"
@insertVideo="src => { createVideoElement(src); mediaInputVisible = false }"
@insertAudio="src => { createAudioElement(src); mediaInputVisible = false }"
/>
</template>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入音视频" :align="{ offset: [0, 0] }">
<IconVideoTwo class="handler-item" />
</Tooltip>
</Popover>
</div>
<div class="right-handler">
<IconMinus class="handler-item viewport-size" @click="scaleCanvas('-')" />
<Popover trigger="click" v-model:open="canvasScaleVisible">
<template #content>
<div class="viewport-size-preset">
<div
class="preset-item"
v-for="item in canvasScalePresetList"
:key="item"
@click="applyCanvasPresetScale(item)"
>{{item}}%</div>
</div>
</template>
<span class="text">{{canvasScalePercentage}}</span>
</Popover>
<IconPlus class="handler-item viewport-size" @click="scaleCanvas('+')" />
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="适应屏幕">
<IconFullScreen class="handler-item viewport-size-adaptation" @click="resetCanvas()" />
</Tooltip>
</div>
<Modal
v-model:open="latexEditorVisible"
:footer="null"
centered
:closable="false"
:width="880"
destroyOnClose
>
<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 {
Tooltip,
Popover,
Divider,
Modal,
} from 'ant-design-vue'
const mainStore = useMainStore()
const { creatingElement, creatingCustomShape } = storeToRefs(mainStore)
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
const { redo, undo } = useHistorySnapshot()
const {
scaleCanvas,
setCanvasScalePercentage,
resetCanvas,
canvasScalePercentage,
} = useScaleCanvas()
const canvasScalePresetList = [200, 150, 100, 80, 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 drawText = (vertical = false) => {
mainStore.setCreatingElement({
type: 'text',
vertical,
})
}
// 绘制形状范围(或绘制自定义任意多边形)
const drawShape = (shape: ShapePoolItem) => {
if (shape.title === '任意多边形') {
mainStore.setCreatingCustomShapeState(true)
}
else {
mainStore.setCreatingElement({
type: 'shape',
data: shape,
})
}
shapePoolVisible.value = false
}
// 绘制线条路径
const drawLine = (line: LinePoolItem) => {
mainStore.setCreatingElement({
type: 'line',
data: line,
})
linePoolVisible.value = false
}
// 打开选择面板
const openSelectPanel = () => {
mainStore.setSelectPanelState(true)
}
// 打开搜索替换面板
const openSraechPanel = () => {
mainStore.setSearchPanelState(true)
}
</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 {
display: flex;
align-items: center;
}
.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: 4px;
&: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: 24px;
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;
&:not(.disable):hover {
background-color: #f1f1f1;
}
}
}
.right-handler {
display: flex;
align-items: center;
.text {
width: 40px;
text-align: center;
cursor: pointer;
}
.viewport-size {
font-size: 13px;
}
}
.preset-item {
padding: 8px 20px;
text-align: center;
cursor: pointer;
&:hover {
color: $themeColor;
}
}
.text-type-item {
padding: 5px 10px;
cursor: pointer;
&:hover {
background-color: #f1f1f1;
}
& + .text-type-item {
margin-top: 3px;
}
}
@media screen and (width <= 1024px) {
.text {
display: none;
}
}
@media screen and (width <= 1000px) {
.left-handler, .right-handler {
display: none;
}
}
</style>