1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159 |
- <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-tooltip effect="light" content="保存模板" placement="top">
- <el-button v-if="!readonly" :icon="Check" circle style="margin-right: 10px" @click="saveJson = !saveJson">
- </el-button>
- </el-tooltip>
- <el-tooltip effect="light" content="模板列表" placement="top">
- <el-button v-if="!readonly" :icon="List" circle style="margin-right: 10px" @click="handleButtonClick">
- </el-button>
- </el-tooltip>
- <el-tooltip effect="light" content="导出文件" placement="top">
- <el-button v-if="!readonly" circle style="margin-right: 10px" @click="savetemplate(1)">导出
- </el-button>
- </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="saveJson" title="保存模板" :width="800">
- <!-- <el-switch v-model="filetype" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #00ffff"
- active-text="文件" inactive-text="模板"/> -->
- <el-input v-if="!readonly" v-model="saveFileName" style="width: 150px; margin-right: 10px" placeholder="请输入模板名"
- clearable></el-input>
- <el-button type="primary" @click="savetemplate(0)">保存模板</el-button>
- </el-dialog>
- <!-- 文件列表 -->
- <el-dialog v-model="filelist" title="模板列表" :width="800">
- <!-- <el-switch v-model="filetype" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #00ffff"
- active-text="文件" inactive-text="模板" @click="templateItems" /> -->
- <el-input v-model="search" style="width: 150px; margin-right: 10px" placeholder="请输入模板名" clearable></el-input>
- <el-button type="primary" :icon="Search" @click="searchTemplate" plain>搜索</el-button>
- <div class="demo-image">
- <div v-for="name in names" :key="name" class="block">
- <span class="demonstration">{{ name[0] }}</span>
- <el-image style="width: 100px; height: 100px" :src="name[1]" @click="importFromJsonServer(name[0])" />
- <el-button type="danger" @click="DeleteeFromJsonServer(name[0])" plain>删除</el-button>
- </div>
- </div>
- </el-dialog>
- <!-- 帮助弹窗 -->
- <el-dialog v-model="helpDialogVisible" title="帮助" :width="500">
- <div class="helpDialogContent">
- <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, getCurrentInstance } from 'vue'
- import TinyWhiteboard from 'tiny-whiteboard'
- import ColorPicker from './components/ColorPicker.vue'
- import {
- Delete,
- CopyDocument,
- ZoomIn,
- ZoomOut,
- Remove,
- RefreshLeft,
- RefreshRight,
- Download,
- FolderOpened,
- Upload,
- CaretTop,
- CaretBottom,
- Minus,
- Finished,
- Grid,
- View,
- Check,
- Edit,
- List,
- Search,
- QuestionFilled
- } from '@element-plus/icons-vue'
- import Contextmenu from './components/Contextmenu.vue'
- import { fontFamilyList, fontSizeList } from './constants'
- import { ElMessage } from 'element-plus'
- // 当前操作类型
- const currentType = ref('selection')
- let saveFileName = ref('')
- // 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 filetype = ref(true)
- // 当前缩放
- 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 InportJsonDialogVisible = ref(false)
- const saveJson = ref(false)
- const filelist = 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 handleButtonClick = async () => {
- await templateItems();
- filelist.value = !filelist.value; // 设置为true以显示弹窗
- };
- let names = reactive(new Map())
- // 从本地导入模板
- 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 importFromJsonServer = async (itemId) => {
- try {
- const url = new URL(process.env.TEMPLATE_API_URL+'template');
- url.searchParams.append('name', itemId);
- const response = await fetch(url, {
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- if (response.ok) {
- // const jsonData = await response.json()
- // const jsonObject = JSON.parse(jsonData.data)
- const data = await response.json()
- if (data.code !== 200) {
- ElNotification.error(data.message)
- } else {
- app.setData(data.data.data)
- filelist.value = false
- ElNotification.success('导入成功')
- }
- }
- } catch (error) {
- ElNotification.warning('导入失败')
- console.log(error)
- }
- }
- //搜索功能
- let search = ref('')
- const searchTemplate = async () => {
- try {
- const url = new URL(process.env.TEMPLATE_API_URL+'search');
- url.searchParams.append('name', search.value);
- const response = await fetch(url, {
- headers: {
- 'Content-Type': 'application/json',
- }
- })
- if (response.ok) {
- const jsonData = await response.json()
- if (jsonData.code === 200) {
- names.clear()
- for (let index = 0; index < jsonData.data.length; index++) {
- names.set(jsonData.data[index].name, jsonData.data[index].url)
- }
- ElNotification.success('搜索成功')
- search.value = null
- } else {
- ElNotification.warning('文件不存在')
- }
- }
- } catch (error) {
- ElNotification.warning('系统出错')
- }
- }
- //删除
- const DeleteeFromJsonServer = async (itemId) => {
- ElMessageBox.confirm('确定删除吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }).then(async () => {
- try {
- const url = new URL(process.env.TEMPLATE_API_URL+'template');
- url.searchParams.append('name', itemId);
- const response = await fetch(url, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- if (!response.ok) throw new Error('Failed to fetch JSON data')
- const data = await response.json()
- if (data.code !== 200) {
- ElNotification.error(data.message)
- } else {
- templateItems()
- ElNotification.success('删除成功')
- }
- } catch (error) {
- ElNotification.warning('删除失败', error)
- }
- })
- }
- // 导出为本地图片或者链接
- const handleExportCommand = type => {
- if (type === 'png') {
- // exportImageUrl.value = app.exportImage({
- // renderBg: exportRenderBackground.value,
- // paddingX: exportImagePaddingX.value,
- // paddingY: exportImagePaddingY.value,
- // onlySelected: exportOnlySelected.value
- exportImageUrl.value = app.exportImage({
- renderBg: true,
- paddingX: exportImagePaddingX.value,
- paddingY: exportImagePaddingY.value,
- onlySelected: exportOnlySelected.value
- })
- // exportImageDialogVisible.value = true
- // 将生成的图片链接复制到剪贴板
- // navigator.clipboard.writeText(exportImageUrl.value).catch(error => {
- // ElNotification.warning('导出链接失败');
- // });
- // saveJson.value = true
- ElNotification.success('图片链接已复制到剪贴板')
- } else if (type === 'json') {
- exportJsonData.value = app.exportJson()
- exportJsonDialogVisible.value = true
- nextTick(() => {
- if (!tree.value) {
- tree.value = jsonTree.create(exportJsonData.value, jsonPreviewBox.value)
- console.log(tree.value)
- } else {
- tree.value.loadData(exportJsonData.value)
- console.log(tree.value)
- }
- })
- }
- }
- //保存文件
- const savetemplate = async (types) => {
- if (types === 1) {
- console.log("当前url", window.location.href)
- const url = window.location.href
- const fileName = getQueryParams(url);
- saveFileName.value = fileName.fileName
- // console.log(exportJson)
- if (fileName.fileName === undefined) {
- ElNotification.warning('URL参数错误')
- return
- }
- }
- if (saveFileName.value === null) {
- ElNotification.warning('请输入模板名')
- return
- }
- exportImageUrl.value = app.exportImage({
- renderBg: true,
- paddingX: exportImagePaddingX.value,
- paddingY: exportImagePaddingY.value,
- onlySelected: exportOnlySelected.value
- })
- try {
- const response = await fetch(process.env.TEMPLATE_API_URL+'saveTemplate', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- type: types, // 1:代表普通保存,0:代表模板保存
- name: saveFileName.value,
- data: app.exportJson(),
- imageUrl: exportImageUrl.value
- })
- })
- if (!response.ok) throw new Error('Failed to fetch JSON data')
- //重置状态
- const data = await response.json()
- saveFileName.value = null
- saveJson.value = false
- if (data.code !== 200) {
- ElNotification.error(data.message)
- } else {
- ElNotification.success('保存成功')
- }
- } catch (error) {
- ElNotification.error(error)
- console.log(error)
- }
- }
- async function getTemplats() {
- const url = window.location.href
- const fileName = getQueryParams(url);
- if (fileName.fileName != undefined) {
- try {
- const url = new URL(process.env.TEMPLATE_API_URL+'template');
- url.searchParams.append('name', fileName.fileName);
- const response = await fetch(url, {
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- if (response.ok) {
- const jsonData = await response.json()
- if (jsonData.code === 200) {
- app.setData(jsonData.data.data)
- }
- }
- } catch (error) {
- console.log(error)
- }
- }
- }
- //获得模板信息
- const templateItems = async () => {
- const requestBody = {
- type: 0,
- };
- names.clear() //清空map 重新渲染
- try {
- const response = await fetch(process.env.TEMPLATE_API_URL+'template', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(requestBody),
- });
- if (response.ok) {
- const jsonData = await response.json()
- if (jsonData.code === 200) {
- for (let index = 0; index < jsonData.data.length; index++) {
- names.set(jsonData.data[index].name, jsonData.data[index].url)
- }
- ElNotification.success('获取成功')
- search.value = null
- } else {
- ElNotification.warning('文件不存在')
- }
- }
- } catch (error) {
- ElNotification.warning('获取失败')
- console.log(error);
- }
- };
- //解析url地址
- function getQueryParams(url) {
- const queryParamsStr = url.includes('?') ? url.split('?')[1] : '';
- const queryParams = new URLSearchParams(queryParamsStr);
- const fileName = queryParams.get('task_id') + "_" + queryParams.get('vt_id') + "_" + queryParams.get('key_id')
- if (!queryParams.has('task_id') || !queryParams.has('vt_id') || !queryParams.has('key_id')) {
- return {};
- } else {
- return {
- fileName
- };
- }
- }
- 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)
- }
- // 监听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)
- })
- })
- onMounted(() => {
- getTemplats()
- })
- </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;
- }
- .demo-image .block {
- padding: 30px 0;
- text-align: center;
- border-right: solid 1px var(--el-border-color);
- display: inline-block;
- width: 20%;
- box-sizing: border-box;
- vertical-align: top;
- }
- .demo-image .block:last-child {
- border-right: none;
- }
- .demo-image .demonstration {
- display: block;
- color: var(--el-text-color-secondary);
- font-size: 14px;
- margin-bottom: 20px;
- }
- </style>
|