1048 lines
27 KiB
Vue
1048 lines
27 KiB
Vue
<template>
|
||
<div class="container">
|
||
<div class="canvasBox" ref="box"></div>
|
||
<div class="toolbar" v-if="!readonly">
|
||
<el-radio-group v-model="currentType" @change="onCurrentTypeChange">
|
||
<el-radio-button label="selection">选择</el-radio-button>
|
||
<el-radio-button label="rectangle">矩形</el-radio-button>
|
||
<el-radio-button label="diamond">菱形</el-radio-button>
|
||
<el-radio-button label="triangle">三角形</el-radio-button>
|
||
<el-radio-button label="circle">圆形</el-radio-button>
|
||
<el-radio-button label="line">线段</el-radio-button>
|
||
<el-radio-button label="arrow">箭头</el-radio-button>
|
||
<el-radio-button label="freedraw">自由画笔</el-radio-button>
|
||
<el-radio-button label="text">文字</el-radio-button>
|
||
<el-radio-button label="image">图片</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<Transition>
|
||
<div class="sidebar" v-show="activeElement || hasSelectedElements">
|
||
<div class="elementStyle">
|
||
<!-- 描边 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="
|
||
!['text', 'image'].includes(activeElement?.type) ||
|
||
hasSelectedElements
|
||
"
|
||
>
|
||
<div class="styleBlockTitle">描边</div>
|
||
<div class="styleBlockContent">
|
||
<ColorPicker
|
||
type="stroke"
|
||
:value="activeElement?.style.strokeStyle"
|
||
@change="updateStyle('strokeStyle', $event)"
|
||
></ColorPicker>
|
||
</div>
|
||
</div>
|
||
<!-- 填充 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="
|
||
!['image', 'line', 'arrow', 'freedraw'].includes(
|
||
activeElement?.type
|
||
) || hasSelectedElements
|
||
"
|
||
>
|
||
<div class="styleBlockTitle">填充</div>
|
||
<div class="styleBlockContent">
|
||
<ColorPicker
|
||
type="fill"
|
||
:value="activeElement?.style.fillStyle"
|
||
@change="updateStyle('fillStyle', $event)"
|
||
></ColorPicker>
|
||
</div>
|
||
</div>
|
||
<!-- 字体 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="['text'].includes(activeElement?.type) || hasSelectedElements"
|
||
>
|
||
<div class="styleBlockTitle">字体</div>
|
||
<div class="styleBlockContent">
|
||
<el-select
|
||
size="mini"
|
||
v-model="fontFamily"
|
||
placeholder="字体"
|
||
@change="updateStyle('fontFamily', $event)"
|
||
>
|
||
<el-option
|
||
v-for="item in fontFamilyList"
|
||
:key="item.value"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
:style="{ fontFamily: item.value }"
|
||
>
|
||
</el-option>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
<!-- 字号 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="['text'].includes(activeElement?.type) || hasSelectedElements"
|
||
>
|
||
<div class="styleBlockTitle">字号</div>
|
||
<div class="styleBlockContent">
|
||
<el-select
|
||
size="mini"
|
||
v-model="fontSize"
|
||
placeholder="字号"
|
||
@change="updateStyle('fontSize', $event)"
|
||
>
|
||
<el-option
|
||
v-for="item in fontSizeList"
|
||
:key="item.value"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
:style="{ fontSize: item.value }"
|
||
>
|
||
</el-option>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
<!-- 描边宽度 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="
|
||
!['image', 'text'].includes(activeElement?.type) ||
|
||
hasSelectedElements
|
||
"
|
||
>
|
||
<div class="styleBlockTitle">描边宽度</div>
|
||
<div class="styleBlockContent">
|
||
<el-radio-group
|
||
v-model="lineWidth"
|
||
@change="updateStyle('lineWidth', $event)"
|
||
>
|
||
<el-radio-button label="small">
|
||
<div class="lineWidthItem small">
|
||
<div class="bar"></div>
|
||
</div>
|
||
</el-radio-button>
|
||
<el-radio-button label="middle">
|
||
<div class="lineWidthItem middle">
|
||
<div class="bar"></div>
|
||
</div>
|
||
</el-radio-button>
|
||
<el-radio-button label="large">
|
||
<div class="lineWidthItem large">
|
||
<div class="bar"></div>
|
||
</div>
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
<!-- 边框样式 -->
|
||
<div
|
||
class="styleBlock"
|
||
v-if="
|
||
!['freedraw', 'image', 'text'].includes(activeElement?.type) ||
|
||
hasSelectedElements
|
||
"
|
||
>
|
||
<div class="styleBlockTitle">边框样式</div>
|
||
<div class="styleBlockContent">
|
||
<el-radio-group
|
||
v-model="lineDash"
|
||
@change="updateStyle('lineDash', $event)"
|
||
>
|
||
<el-radio-button :label="0">
|
||
<div>实线</div>
|
||
</el-radio-button>
|
||
<el-radio-button :label="5">
|
||
<div>大虚线</div>
|
||
</el-radio-button>
|
||
<el-radio-button :label="2">
|
||
<div>小虚线</div>
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
<!-- 透明度 -->
|
||
<div class="styleBlock">
|
||
<div class="styleBlockTitle">透明度</div>
|
||
<div class="styleBlockContent">
|
||
<el-slider
|
||
v-model="globalAlpha"
|
||
:min="0"
|
||
:max="1"
|
||
:step="0.1"
|
||
@change="updateStyle('globalAlpha', $event)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<!-- 角度 -->
|
||
<div class="styleBlock" v-if="!hasSelectedElements">
|
||
<div class="styleBlockTitle">角度</div>
|
||
<div class="styleBlockContent">
|
||
<el-slider
|
||
v-model="rotate"
|
||
:min="0"
|
||
:max="360"
|
||
:step="1"
|
||
@input="onRotateChange"
|
||
/>
|
||
<el-input-number
|
||
style="width: 80px; margin-left: 20px"
|
||
:controls="false"
|
||
v-model="rotate"
|
||
:min="0"
|
||
:max="360"
|
||
@focus="onInputNumberFocus"
|
||
@blur="onInputNumberBlur"
|
||
@change="onRotateChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<!-- 操作 -->
|
||
<div class="styleBlock">
|
||
<div class="styleBlockTitle">操作</div>
|
||
<div class="styleBlockContent">
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
circle
|
||
@click="deleteElement"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
:icon="CopyDocument"
|
||
circle
|
||
@click="copyElement"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
<div class="footerLeft" @click.stop>
|
||
<!-- 缩放 -->
|
||
<div class="blockBox">
|
||
<el-tooltip effect="light" content="缩小" placement="top">
|
||
<el-button :icon="ZoomOut" circle @click="zoomOut" />
|
||
</el-tooltip>
|
||
<el-tooltip effect="light" content="重置缩放" placement="top">
|
||
<span class="zoom" @click="resetZoom">{{ currentZoom }}%</span>
|
||
</el-tooltip>
|
||
<el-tooltip effect="light" content="放大" placement="top">
|
||
<el-button :icon="ZoomIn" circle @click="zoomIn" />
|
||
</el-tooltip>
|
||
</div>
|
||
<!-- 前进回退 -->
|
||
<div class="blockBox" v-if="!readonly">
|
||
<el-tooltip effect="light" content="回退" placement="top">
|
||
<el-button
|
||
:icon="RefreshLeft"
|
||
circle
|
||
:disabled="!canUndo"
|
||
@click="undo"
|
||
/>
|
||
</el-tooltip>
|
||
<el-tooltip effect="light" content="前进" placement="top">
|
||
<el-button
|
||
:icon="RefreshRight"
|
||
circle
|
||
:disabled="!canRedo"
|
||
@click="redo"
|
||
/>
|
||
</el-tooltip>
|
||
</div>
|
||
<!-- 滚动 -->
|
||
<div class="blockBox">
|
||
<el-tooltip effect="light" content="滚动至中心" placement="top">
|
||
<el-button @click="scrollToCenter"
|
||
>X:{{ scroll.x }} Y:{{ scroll.y }}
|
||
</el-button>
|
||
</el-tooltip>
|
||
</div>
|
||
<!-- 橡皮擦、显示网格、清空 -->
|
||
<div class="blockBox">
|
||
<!-- 橡皮擦 -->
|
||
<el-tooltip
|
||
effect="light"
|
||
:content="currentType === 'eraser' ? '关闭橡皮擦' : '橡皮擦'"
|
||
placement="top"
|
||
>
|
||
<el-button
|
||
v-if="!readonly"
|
||
:icon="Remove"
|
||
circle
|
||
:type="currentType === 'eraser' ? 'primary' : null"
|
||
@click="toggleEraser"
|
||
/>
|
||
</el-tooltip>
|
||
<!-- 网格 -->
|
||
<el-tooltip
|
||
effect="light"
|
||
:content="showGrid ? '隐藏网格' : '显示网格'"
|
||
placement="top"
|
||
>
|
||
<el-button
|
||
:icon="Grid"
|
||
circle
|
||
:type="showGrid ? 'primary' : null"
|
||
@click="toggleGrid"
|
||
/>
|
||
</el-tooltip>
|
||
<!-- 只读、编辑模式切换 -->
|
||
<el-tooltip
|
||
effect="light"
|
||
:content="readonly ? '切换到编辑模式' : '切换到只读模式'"
|
||
placement="top"
|
||
>
|
||
<el-button
|
||
:icon="readonly ? View : Edit"
|
||
circle
|
||
@click="toggleMode"
|
||
/>
|
||
</el-tooltip>
|
||
<!-- 清空 -->
|
||
<el-tooltip effect="light" content="清空" placement="top">
|
||
<el-button v-if="!readonly" :icon="Delete" circle @click="empty" />
|
||
</el-tooltip>
|
||
</div>
|
||
<!-- 导入导出 -->
|
||
<div class="blockBox">
|
||
<el-tooltip effect="light" content="从json文件导入" placement="top">
|
||
<el-button
|
||
v-if="!readonly"
|
||
:icon="Upload"
|
||
circle
|
||
style="margin-right: 10px"
|
||
@click="importFromJson"
|
||
/>
|
||
</el-tooltip>
|
||
<el-dropdown @command="handleExportCommand">
|
||
<span class="el-dropdown-link">
|
||
<el-button :icon="Download" circle />
|
||
</span>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="png">导出为图片</el-dropdown-item>
|
||
<el-dropdown-item command="json">导出为json</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
<!-- 背景 -->
|
||
<div class="blockBox">
|
||
<ColorPicker
|
||
style="width: 280px"
|
||
type="background"
|
||
:value="backgroundColor"
|
||
:showEmptySelect="true"
|
||
placement="top"
|
||
name="背景颜色"
|
||
@change="setBackgroundColor"
|
||
></ColorPicker>
|
||
</div>
|
||
<!-- 帮助 -->
|
||
<div class="blockBox">
|
||
<el-tooltip effect="light" content="帮助" placement="top">
|
||
<el-button
|
||
:icon="QuestionFilled"
|
||
circle
|
||
style="margin-right: 10px"
|
||
@click="helpDialogVisible = !helpDialogVisible"
|
||
/>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
<!-- 导出图片弹窗 -->
|
||
<el-dialog
|
||
v-model="exportImageDialogVisible"
|
||
title="导出为图片"
|
||
:width="800"
|
||
>
|
||
<div class="exportImageContainer">
|
||
<div class="imagePreviewBox">
|
||
<img :src="exportImageUrl" alt="" />
|
||
</div>
|
||
<div class="handleBox">
|
||
<el-checkbox
|
||
v-model="exportOnlySelected"
|
||
label="仅导出被选中"
|
||
size="large"
|
||
@change="reRenderExportImage"
|
||
style="margin-right: 10px"
|
||
/>
|
||
<el-checkbox
|
||
v-model="exportRenderBackground"
|
||
label="背景"
|
||
size="large"
|
||
@change="reRenderExportImage"
|
||
style="margin-right: 10px"
|
||
/>
|
||
<el-input
|
||
v-model="exportFileName"
|
||
style="width: 150px; margin-right: 10px"
|
||
></el-input>
|
||
<el-input-number
|
||
v-model="exportImagePaddingX"
|
||
:min="10"
|
||
:max="100"
|
||
:step="5"
|
||
controls-position="right"
|
||
@change="reRenderExportImage"
|
||
style="margin-right: 10px"
|
||
/>
|
||
<el-input-number
|
||
v-model="exportImagePaddingY"
|
||
:min="10"
|
||
:max="100"
|
||
:step="5"
|
||
controls-position="right"
|
||
@change="reRenderExportImage"
|
||
style="margin-right: 10px"
|
||
/>
|
||
<el-button type="primary" @click="downloadExportImage"
|
||
>下载</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
<!-- 导出json弹窗 -->
|
||
<el-dialog
|
||
v-model="exportJsonDialogVisible"
|
||
title="导出为json"
|
||
:width="800"
|
||
>
|
||
<div class="exportJsonContainer">
|
||
<div class="jsonPreviewBox" ref="jsonPreviewBox"></div>
|
||
<div class="handleBox">
|
||
<el-input
|
||
v-model="exportFileName"
|
||
style="width: 150px; margin-right: 10px"
|
||
></el-input>
|
||
<el-button type="primary" @click="downloadExportJson">下载</el-button>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
<!-- 帮助弹窗 -->
|
||
<el-dialog v-model="helpDialogVisible" title="帮助" :width="500">
|
||
<div class="helpDialogContent">
|
||
<h2>获取源码</h2>
|
||
<p>
|
||
github:<a
|
||
style="color: #409eff"
|
||
href="https://github.com/wanglin2/tiny_whiteboard"
|
||
target="_blank"
|
||
>https://github.com/wanglin2/tiny_whiteboard</a
|
||
>
|
||
</p>
|
||
<h2>tips</h2>
|
||
<p>移动画布:按住空格键进行拖动</p>
|
||
<h2>快捷键</h2>
|
||
<el-table :data="shortcutKeyList">
|
||
<el-table-column property="name" label="操作" />
|
||
<el-table-column property="value" label="快捷键" />
|
||
</el-table>
|
||
</div>
|
||
</el-dialog>
|
||
<!-- 右键菜单 -->
|
||
<Contextmenu v-if="appInstance" :app="appInstance"></Contextmenu>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref, watch, toRaw, nextTick, computed, reactive } from 'vue'
|
||
import TinyWhiteboard from 'tiny-whiteboard'
|
||
import ColorPicker from './components/ColorPicker.vue'
|
||
import {
|
||
Delete,
|
||
CopyDocument,
|
||
ZoomIn,
|
||
ZoomOut,
|
||
Remove,
|
||
RefreshLeft,
|
||
RefreshRight,
|
||
Download,
|
||
Upload,
|
||
CaretTop,
|
||
CaretBottom,
|
||
Minus,
|
||
Grid,
|
||
View,
|
||
Edit,
|
||
QuestionFilled
|
||
} from '@element-plus/icons-vue'
|
||
import Contextmenu from './components/Contextmenu.vue'
|
||
import { fontFamilyList, fontSizeList } from './constants'
|
||
|
||
// 当前操作类型
|
||
const currentType = ref('selection')
|
||
|
||
// dom节点
|
||
const box = ref(null)
|
||
|
||
// 应用实例
|
||
let app = null
|
||
const appInstance = ref(null)
|
||
// 当前激活的元素
|
||
const activeElement = ref(null)
|
||
// 当前多选的元素
|
||
const selectedElements = ref([])
|
||
const hasSelectedElements = computed(() => {
|
||
return selectedElements.value.length > 0
|
||
})
|
||
// 描边宽度
|
||
const lineWidth = ref('small')
|
||
// 字体
|
||
const fontFamily = ref('微软雅黑, Microsoft YaHei')
|
||
// 字号
|
||
const fontSize = ref(18)
|
||
// 边框样式
|
||
const lineDash = ref(0)
|
||
// 透明度
|
||
const globalAlpha = ref(1)
|
||
// 角度
|
||
const rotate = ref(0)
|
||
// 当前缩放
|
||
const currentZoom = ref(100)
|
||
// 缩放允许前进后退
|
||
const canUndo = ref(false)
|
||
const canRedo = ref(false)
|
||
// 图片导出弹窗
|
||
const exportImageDialogVisible = ref(false)
|
||
const exportImageUrl = ref('')
|
||
const exportOnlySelected = ref(false)
|
||
const exportRenderBackground = ref(true)
|
||
const exportFileName = ref('未命名')
|
||
const exportImagePaddingX = ref(10)
|
||
const exportImagePaddingY = ref(10)
|
||
// json导出弹窗
|
||
const exportJsonDialogVisible = ref(false)
|
||
const exportJsonData = ref('')
|
||
const tree = ref(null)
|
||
const jsonPreviewBox = ref(null)
|
||
// 背景颜色
|
||
const backgroundColor = ref('')
|
||
// 当前滚动距离
|
||
const scroll = reactive({
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
// 切换显示网格
|
||
const showGrid = ref(false)
|
||
// 模式切换
|
||
const readonly = ref(false)
|
||
// 帮助弹窗
|
||
const helpDialogVisible = ref(false)
|
||
const shortcutKeyList = reactive([
|
||
{
|
||
name: '全部选中',
|
||
value: 'Control + a'
|
||
},
|
||
{
|
||
name: '删除',
|
||
value: 'Del 或 Backspace'
|
||
},
|
||
{
|
||
name: '复制',
|
||
value: 'Control + c'
|
||
},
|
||
{
|
||
name: '粘贴',
|
||
value: 'Control + v'
|
||
},
|
||
{
|
||
name: '放大',
|
||
value: 'Control + +'
|
||
},
|
||
{
|
||
name: '缩小',
|
||
value: 'Control + -'
|
||
},
|
||
{
|
||
name: '重置缩放',
|
||
value: 'Control + 0'
|
||
},
|
||
{
|
||
name: '缩放以适应所有元素',
|
||
value: 'Shift + 1'
|
||
},
|
||
{
|
||
name: '撤销',
|
||
value: 'Control + z'
|
||
},
|
||
{
|
||
name: '重做',
|
||
value: 'Control + y'
|
||
},
|
||
{
|
||
name: '显示隐藏网格',
|
||
value: "Control + '"
|
||
}
|
||
])
|
||
|
||
// 通知app更当前类型
|
||
watch(currentType, () => {
|
||
app.updateCurrentType(currentType.value)
|
||
})
|
||
|
||
// 元素角度变化
|
||
const onElementRotateChange = elementRotate => {
|
||
rotate.value = elementRotate
|
||
}
|
||
|
||
// 修改元素角度
|
||
const onRotateChange = rotate => {
|
||
app.updateActiveElementRotate(rotate)
|
||
}
|
||
|
||
// 数字输入框聚焦事件
|
||
const onInputNumberFocus = () => {
|
||
// 解绑快捷键按键事件,防止冲突
|
||
app.keyCommand.unBindEvent()
|
||
}
|
||
|
||
// 数字输入框失焦事件
|
||
const onInputNumberBlur = () => {
|
||
// 重新绑定快捷键按键事件
|
||
app.keyCommand.bindEvent()
|
||
}
|
||
|
||
// 更新样式
|
||
const updateStyle = (key, value) => {
|
||
app.setCurrentElementsStyle({
|
||
[key]: value
|
||
})
|
||
}
|
||
|
||
// 类型变化
|
||
const onCurrentTypeChange = () => {
|
||
// 清除激活项
|
||
app.cancelActiveElement()
|
||
}
|
||
|
||
// 删除元素
|
||
const deleteElement = () => {
|
||
app.deleteCurrentElements()
|
||
}
|
||
|
||
// 复制元素
|
||
const copyElement = () => {
|
||
app.copyPasteCurrentElements()
|
||
}
|
||
|
||
// 放大
|
||
const zoomIn = () => {
|
||
app.zoomIn()
|
||
}
|
||
|
||
// 缩小
|
||
const zoomOut = () => {
|
||
app.zoomOut()
|
||
}
|
||
|
||
// 恢复初始缩放
|
||
const resetZoom = () => {
|
||
app.setZoom(1)
|
||
}
|
||
|
||
// 橡皮擦
|
||
const toggleEraser = () => {
|
||
currentType.value = currentType.value === 'eraser' ? 'selection' : 'eraser'
|
||
}
|
||
|
||
// 回退
|
||
const undo = () => {
|
||
app.undo()
|
||
}
|
||
|
||
// 前进
|
||
const redo = () => {
|
||
app.redo()
|
||
}
|
||
|
||
// 清空
|
||
const empty = () => {
|
||
app.empty()
|
||
}
|
||
|
||
// 导入
|
||
const importFromJson = () => {
|
||
let el = document.createElement('input')
|
||
el.type = 'file'
|
||
el.accept = 'application/json'
|
||
el.addEventListener('input', () => {
|
||
let reader = new FileReader()
|
||
reader.onload = () => {
|
||
el.value = null
|
||
if (reader.result) {
|
||
app.setData(JSON.parse(reader.result))
|
||
}
|
||
}
|
||
reader.readAsText(el.files[0])
|
||
})
|
||
el.click()
|
||
}
|
||
|
||
// 导出
|
||
const handleExportCommand = type => {
|
||
if (type === 'png') {
|
||
exportImageUrl.value = app.exportImage({
|
||
renderBg: exportRenderBackground.value,
|
||
paddingX: exportImagePaddingX.value,
|
||
paddingY: exportImagePaddingY.value,
|
||
onlySelected: exportOnlySelected.value
|
||
})
|
||
exportImageDialogVisible.value = true
|
||
} else if (type === 'json') {
|
||
exportJsonData.value = app.exportJson()
|
||
exportJsonDialogVisible.value = true
|
||
nextTick(() => {
|
||
if (!tree.value) {
|
||
tree.value = jsonTree.create(exportJsonData.value, jsonPreviewBox.value)
|
||
} else {
|
||
tree.value.loadData(exportJsonData.value)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 重新生成导出图片
|
||
const reRenderExportImage = () => {
|
||
exportImageUrl.value = app.exportImage({
|
||
renderBg: exportRenderBackground.value,
|
||
paddingX: exportImagePaddingX.value,
|
||
paddingY: exportImagePaddingY.value,
|
||
onlySelected: exportOnlySelected.value
|
||
})
|
||
}
|
||
|
||
// 下载导出的图片
|
||
const downloadExportImage = () => {
|
||
TinyWhiteboard.utils.downloadFile(
|
||
exportImageUrl.value,
|
||
exportFileName.value + '.png'
|
||
)
|
||
}
|
||
|
||
// 下载导出的json
|
||
const downloadExportJson = () => {
|
||
let str = JSON.stringify(exportJsonData.value, null, 4)
|
||
let blob = new Blob([str])
|
||
TinyWhiteboard.utils.downloadFile(
|
||
URL.createObjectURL(blob),
|
||
exportFileName.value + '.json'
|
||
)
|
||
}
|
||
|
||
// 更新背景颜色
|
||
const setBackgroundColor = value => {
|
||
app.setBackgroundColor(value)
|
||
}
|
||
|
||
// 滚动至中心
|
||
const scrollToCenter = () => {
|
||
app.scrollToCenter()
|
||
}
|
||
|
||
// 切换显示网格
|
||
const toggleGrid = () => {
|
||
if (showGrid.value) {
|
||
showGrid.value = false
|
||
app.hideGrid()
|
||
} else {
|
||
showGrid.value = true
|
||
app.showGrid()
|
||
}
|
||
}
|
||
|
||
// 模式切换
|
||
const toggleMode = () => {
|
||
if (readonly.value) {
|
||
readonly.value = false
|
||
app.setEditMode()
|
||
} else {
|
||
readonly.value = true
|
||
app.setReadonlyMode()
|
||
}
|
||
}
|
||
|
||
// dom元素挂载完成
|
||
onMounted(() => {
|
||
// 创建实例
|
||
app = new TinyWhiteboard({
|
||
container: box.value,
|
||
drawType: currentType.value,
|
||
state: {
|
||
// backgroundColor: '#121212',
|
||
// strokeStyle: '#fff',
|
||
// fontFamily: '楷体, 楷体_GB2312, SimKai, STKaiti',
|
||
// dragStrokeStyle: '#999'
|
||
}
|
||
})
|
||
let storeData = localStorage.getItem('TINY_WHITEBOARD_DATA')
|
||
if (storeData) {
|
||
storeData = JSON.parse(storeData)
|
||
;[['backgroundColor', ''],['strokeStyle', '#000000'],['fontFamily', '微软雅黑, Microsoft YaHei'],['dragStrokeStyle', '#666'], ['fillStyle', 'transparent'], ['fontSize', 18]].forEach((item) => {
|
||
if (storeData.state[item[0]] === undefined) {
|
||
storeData.state[item[0]] = item[1]
|
||
}
|
||
})
|
||
currentZoom.value = parseInt(storeData.state.scale * 100)
|
||
scroll.x = parseInt(storeData.state.scrollX)
|
||
scroll.y = parseInt(storeData.state.scrollY)
|
||
showGrid.value = storeData.state.showGrid
|
||
readonly.value = storeData.state.readonly
|
||
app.setData(storeData)
|
||
|
||
|
||
document.body.addEventListener('touchmove', function (e) {
|
||
e.preventDefault();
|
||
}, {passive: false});
|
||
}
|
||
// 监听app内部修改类型事件
|
||
app.on('currentTypeChange', type => {
|
||
currentType.value = type
|
||
})
|
||
// 监听元素激活事件
|
||
app.on('activeElementChange', element => {
|
||
if (activeElement.value) {
|
||
activeElement.value.off('elementRotateChange', onElementRotateChange)
|
||
}
|
||
activeElement.value = element
|
||
if (element) {
|
||
let { style, rotate: elementRotate } = element
|
||
lineWidth.value = style.lineWidth
|
||
fontFamily.value = style.fontFamily
|
||
fontSize.value = style.fontSize
|
||
lineDash.value = style.lineDash
|
||
globalAlpha.value = style.globalAlpha
|
||
rotate.value = elementRotate
|
||
element.on('elementRotateChange', onElementRotateChange)
|
||
}
|
||
})
|
||
// 元素多选变化
|
||
app.on('multiSelectChange', elements => {
|
||
selectedElements.value = elements
|
||
})
|
||
// 缩放变化
|
||
app.on('zoomChange', scale => {
|
||
currentZoom.value = parseInt(scale * 100)
|
||
})
|
||
// 监听前进后退事件
|
||
app.on('shuttle', (index, length) => {
|
||
canUndo.value = index > 0
|
||
canRedo.value = index < length - 1
|
||
})
|
||
// 监听数据变化
|
||
app.on('change', data => {
|
||
showGrid.value = data.state.showGrid
|
||
localStorage.setItem('TINY_WHITEBOARD_DATA', JSON.stringify(data))
|
||
})
|
||
// 监听滚动变化
|
||
app.on('scrollChange', (x, y) => {
|
||
scroll.y = parseInt(y)
|
||
scroll.x = parseInt(x)
|
||
})
|
||
appInstance.value = app
|
||
// 窗口尺寸变化
|
||
let resizeTimer = null
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeTimer)
|
||
resizeTimer = setTimeout(() => {
|
||
app.resize()
|
||
}, 300)
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style lang="less">
|
||
ul,
|
||
ol {
|
||
list-style: none;
|
||
}
|
||
.v-enter-active,
|
||
.v-leave-active {
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.v-enter-from,
|
||
.v-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-300px);
|
||
}
|
||
</style>
|
||
<style lang="less" scoped>
|
||
.container {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.toolbar {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 10px;
|
||
transform: translateX(-50%);
|
||
z-index: 2;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.canvasBox {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 100%;
|
||
height: 100%;
|
||
transform: translate(-50%, -50%);
|
||
background-color: #fff;
|
||
}
|
||
|
||
.sidebar {
|
||
position: absolute;
|
||
left: 10px;
|
||
top: 10px;
|
||
width: 250px;
|
||
background-color: #fff;
|
||
|
||
.elementStyle {
|
||
padding: 10px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||
border-radius: 4px;
|
||
|
||
.styleBlock {
|
||
margin-bottom: 10px;
|
||
|
||
.styleBlockTitle {
|
||
color: #343a40;
|
||
font-size: 14px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.styleBlockContent {
|
||
display: flex;
|
||
|
||
.lineWidthItem {
|
||
display: flex;
|
||
width: 30px;
|
||
height: 10px;
|
||
align-items: center;
|
||
|
||
.bar {
|
||
width: 100%;
|
||
background-color: #212529;
|
||
}
|
||
|
||
&.small {
|
||
.bar {
|
||
height: 2px;
|
||
}
|
||
}
|
||
|
||
&.middle {
|
||
.bar {
|
||
height: 4px;
|
||
}
|
||
}
|
||
|
||
&.large {
|
||
.bar {
|
||
height: 6px;
|
||
}
|
||
}
|
||
}
|
||
|
||
/deep/ .el-radio-group {
|
||
.el-radio-button {
|
||
&.is-active {
|
||
.lineWidthItem {
|
||
.bar {
|
||
background-color: #fff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.footerLeft {
|
||
position: absolute;
|
||
left: 10px;
|
||
bottom: 10px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.blockBox {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
|
||
.zoom {
|
||
width: 40px;
|
||
margin: 0 10px;
|
||
user-select: none;
|
||
color: #606266;
|
||
cursor: pointer;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
background-color: #fff;
|
||
border-radius: 5px;
|
||
padding: 0 5px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.exportImageContainer {
|
||
.imagePreviewBox {
|
||
height: 400px;
|
||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==')
|
||
0;
|
||
padding: 10px;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: scale-down;
|
||
}
|
||
}
|
||
|
||
.handleBox {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 50px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.exportJsonContainer {
|
||
.jsonPreviewBox {
|
||
height: 400px;
|
||
overflow: auto;
|
||
background-color: #f5f5f5;
|
||
font-size: 14px;
|
||
color: #000;
|
||
|
||
/deep/ .jsontree_tree {
|
||
font-family: 'Trebuchet MS', Arial, sans-serif !important;
|
||
}
|
||
}
|
||
|
||
.handleBox {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 50px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.helpDialogContent {
|
||
height: 500px;
|
||
overflow: auto;
|
||
}
|
||
</style>
|