diff --git a/src/configs/defaultElement.ts b/src/configs/defaultElement.ts index a8bf51be..d03f922f 100644 --- a/src/configs/defaultElement.ts +++ b/src/configs/defaultElement.ts @@ -1,4 +1,4 @@ -const DEFAULT_COLOR = '#888' +const DEFAULT_COLOR = '#41464b' export const DEFAULT_TEXT = { type: 'text', @@ -6,12 +6,10 @@ export const DEFAULT_TEXT = { top: 0, width: 300, height: 0, - padding: 5, opacity: 1, lineHeight: 1.5, segmentSpacing: 5, - textType: 'content', - content: '
“单击此处添加文本”
', + content: '请输入内容', } export const DEFAULT_IMAGE = { @@ -27,25 +25,6 @@ export const DEFAULT_SHAPE = { lockRatio: false, } -export const DEFAULT_SHAPE_LINE = { - type: 'shape', - borderStyle: 'solid', - borderWidth: 2, - borderColor: DEFAULT_COLOR, - fill: 'rgba(0, 0, 0, 0)', - lockRatio: false, -} - -export const DEFAULT_ICON = { - type: 'icon', - left: 0, - top: 0, - width: 80, - height: 80, - color: DEFAULT_COLOR, - lockRatio: true, -} - export const DEFAULT_LINE = { type: 'line', style: 'solid', @@ -62,14 +41,6 @@ export const DEFAULT_CHART = { height: 500, } -export const DEFAULT_IFRAME = { - type: 'iframe', - left: 0, - top: 0, - width: 800, - height: 480, -} - export const DEFAULT_TABLE = { type: 'table', left: 0, diff --git a/src/mocks/index.ts b/src/mocks/index.ts index f20b7aa8..e4bdcf43 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -18,12 +18,10 @@ export const slides: Slide[] = [ borderColor: '#5b7d89', fill: 'rgba(220,220,220,0.8)', shadow: '1px 1px 3px rgba(10,10,10,.5)', - padding: 10, opacity: 1, lineHeight: 1.5, segmentSpacing: 10, isLock: false, - textType: 'title', content: '
一段测试文字,字号固定为28px
', }, { @@ -79,12 +77,10 @@ export const slides: Slide[] = [ width: 220, height: 188, rotate: 0, - padding: 10, opacity: 1, lineHeight: 1.5, segmentSpacing: 10, isLock: false, - textType: 'content', content: '
😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒
', }, ], diff --git a/src/store/mutations.ts b/src/store/mutations.ts index 2d385cef..c7e812ba 100644 --- a/src/store/mutations.ts +++ b/src/store/mutations.ts @@ -2,7 +2,7 @@ import { MutationTypes } from './constants' import { State } from './state' import { Slide, PPTElement } from '@/types/slides' import { FONT_NAMES } from '@/configs/fontName' -import { isSupportFontFamily } from '@/utils/index' +import { isSupportFontFamily } from '@/utils/fontFamily' interface AddSlidesData { index?: number; diff --git a/src/types/slides.ts b/src/types/slides.ts index 1e903ff4..171dee25 100644 --- a/src/types/slides.ts +++ b/src/types/slides.ts @@ -1,3 +1,5 @@ +export type ElementType = 'text' | 'image' | 'shape' | 'line' | 'chart' | 'table' + export interface PPTElementBaseProps { elId: string; isLock: boolean; @@ -51,8 +53,6 @@ export interface PPTShapeElement extends PPTElementBaseProps, PPTElementSizeProp rotate?: number; opacity?: number; shadow?: string; - text?: string; - textAlign?: string; } export interface PPTLineElement extends PPTElementBaseProps { @@ -73,7 +73,7 @@ export interface PPTChartElement extends PPTElementBaseProps, PPTElementSizeProp data: Object; } -export interface TableCell { +export interface TableElementCell { colspan: number; rowspan: number; content: string; @@ -85,7 +85,7 @@ export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProp theme: string; rowSizes: number[]; colSizes: number[]; - data: TableCell[][]; + data: TableElementCell[][]; } export type PPTElement = PPTTextElement | diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts new file mode 100644 index 00000000..ec8a6782 --- /dev/null +++ b/src/utils/clipboard.ts @@ -0,0 +1,35 @@ +import Clipboard from 'clipboard' + +// 复制文本到剪贴板 +export const copyText = (text: string) => { + return new Promise((resolve, reject) => { + const fakeElement = document.createElement('button') + const clipboard = new Clipboard(fakeElement, { + text: () => text, + action: () => 'copy', + container: document.body, + }) + clipboard.on('success', e => { + clipboard.destroy() + resolve(e) + }) + clipboard.on('error', e => { + clipboard.destroy() + reject(e) + }) + document.body.appendChild(fakeElement) + fakeElement.click() + document.body.removeChild(fakeElement) + }) +} + +// 读取剪贴板 +export const readClipboard = () => { + if(navigator.clipboard) { + navigator.clipboard.readText().then(text => { + if(!text) return { err: '剪贴板为空或者不包含文本' } + return { text } + }) + } + return { err: '浏览器不支持或禁止访问剪贴板' } +} \ No newline at end of file diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 00000000..3f20f2ca --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,18 @@ +import padStart from 'lodash/padStart' + +// 生成随机码 +export const createRandomCode = (len = 6) => { + const charset = `_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` + const maxLen = charset.length + let ret = '' + for(let i = 0; i < len; i++) { + const randomIndex = Math.floor(Math.random() * maxLen) + ret += charset[randomIndex] + } + return ret +} + +// 数字补足位数,例如将6补足3位 -> 003 +export const fillDigit = (digit: number, len: number) => { + return padStart('' + digit, len, '0') +} \ No newline at end of file diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 00000000..45986ef5 --- /dev/null +++ b/src/utils/crypto.ts @@ -0,0 +1,14 @@ +import CryptoJS from 'crypto-js' + +const CRYPTO_KEY = 'zxc_ppt_online_editor' + +// 加密函数 +export const encrypt = (msg: string) => { + return CryptoJS.AES.encrypt(msg, CRYPTO_KEY).toString() +} + +// 解密函数 +export const decrypt = (ciphertext: string) => { + const bytes = CryptoJS.AES.decrypt(ciphertext, CRYPTO_KEY) + return bytes.toString(CryptoJS.enc.Utf8) +} \ No newline at end of file diff --git a/src/utils/fontFamily.ts b/src/utils/fontFamily.ts new file mode 100644 index 00000000..6e2ae095 --- /dev/null +++ b/src/utils/fontFamily.ts @@ -0,0 +1,31 @@ +// 判断用户的操作系统是否安装了某字体 +export const isSupportFontFamily = (fontFamily: string) => { + if(typeof fontFamily !== 'string') return false + const arial = 'Arial' + if(fontFamily.toLowerCase() === arial.toLowerCase()) return true + const a = 'a' + const size = 100 + const width = 100 + const height = 100 + + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + if(!ctx) return false + + canvas.width = width + canvas.height = height + ctx.textAlign = 'center' + ctx.fillStyle = 'black' + ctx.textBaseline = 'middle' + + const getDotArray = (_fontFamily: string) => { + ctx.clearRect(0, 0, width, height) + ctx.font = `${size}px ${_fontFamily}, ${arial}` + ctx.fillText(a, width / 2, height / 2) + const imageData = ctx.getImageData(0, 0, width, height).data + return [].slice.call(imageData).filter(item => item !== 0) + } + + return getDotArray(arial).join('') !== getDotArray(fontFamily).join('') +} \ No newline at end of file diff --git a/src/utils/fullscreen.ts b/src/utils/fullscreen.ts new file mode 100644 index 00000000..c0c56690 --- /dev/null +++ b/src/utils/fullscreen.ts @@ -0,0 +1,8 @@ +// 进入全屏 +export const enterFullscreen = document.documentElement.requestFullscreen + +// 退出全屏 +export const exitFullscreen = document.exitFullscreen + +// 判断是否全屏 +export const isFullscreen = () => document.fullscreenEnabled \ No newline at end of file diff --git a/src/utils/image.ts b/src/utils/image.ts new file mode 100644 index 00000000..c4c41bfe --- /dev/null +++ b/src/utils/image.ts @@ -0,0 +1,42 @@ +interface ImageSize { + width: number; + height: number; +} + +// 获取图片的原始宽高 +export const getImageSize = (imgUrl: string): Promise => { + return new Promise(resolve => { + const img = document.createElement('img') + img.src = imgUrl + img.style.opacity = '0' + document.body.appendChild(img) + + img.onload = () => { + const imgWidth = img.clientWidth + const imgHeight = img.clientHeight + + img.onload = null + img.onerror = null + + document.body.removeChild(img) + + resolve({ width: imgWidth, height: imgHeight }) + } + + img.onerror = () => { + img.onload = null + img.onerror = null + } + }) +} + +// 获取图片文件的dataURL +export const getImageDataURL = (file: File): Promise => { + return new Promise(resolve => { + const reader = new FileReader() + reader.addEventListener('load', () => { + resolve(reader.result as string) + }) + reader.readAsDataURL(file) + }) +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index b8cb8113..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,209 +0,0 @@ -import padStart from 'lodash/padStart' -import Clipboard from 'clipboard' -import CryptoJS from 'crypto-js' - -const CRYPTO_KEY = 'zxc_ppt_online_editor' - -// 生成随机数 -export const createRandomNumber = (min: number, max: number) => { - return Math.floor(min + Math.random() * (max - min)) -} - -// 生成随机码 -export const createRandomCode = (len = 6) => { - const charset = `_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` - const maxLen = charset.length - let ret = '' - for(let i = 0; i < len; i++) { - const randomIndex = Math.floor(Math.random() * maxLen) - ret += charset[randomIndex] - } - return ret -} - -// 生成uuid -export const createUUID = () => { - const url = URL.createObjectURL(new Blob()) - const uuid = url.toString() - URL.revokeObjectURL(url) - return uuid.substr(uuid.lastIndexOf('/') + 1) -} - -// 获取当前日期字符串 -export const getDateTime = (format = 'yyyy-MM-dd hh:mm:ss') => { - const date = new Date() - - const formatMap = { - 'y+': date.getFullYear(), - 'M+': date.getMonth() + 1, - 'd+': date.getDate(), - 'h+': date.getHours(), - 'm+': date.getMinutes(), - 's+': date.getSeconds(), - } - - for(const item of Object.keys(formatMap)) { - if(new RegExp('(' + item + ')').test(format)) { - const formated = (formatMap[item] + '').length < RegExp.$1.length ? padStart('' + formatMap[item], RegExp.$1.length, '0') : formatMap[item] - format = format.replace(RegExp.$1, formated) - } - } - return format -} - -// 数字转中文,如1049 -> 一千零四十九 -export const digitalToChinese = (n: number) => { - const str = n + '' - const len = str.length - 1 - const idxs = ['', '十', '百', '千'] - const num = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'] - return str.replace(/([1-9]|0+)/g, ($, $1, idx) => { - const pos = len - idx - if($1 !== 0) { - if(idx === 0 && $1 === 1 && idxs[pos] === '十') return idxs[pos] - return num[$1] + idxs[pos] - } - if(idx + $1.length >= str.length) return '' - return '零' - }) -} - -// 数字补足位数,例如将6补足3位 -> 003 -export const fillDigit = (digit: number, len: number) => { - return padStart('' + digit, len, '0') -} - -// 进入全屏 -export const enterFullscreen = () => { - const docElm = document.documentElement - docElm.requestFullscreen() -} - -// 退出全屏 -export const exitFullscreen = document.exitFullscreen - -// 判断是否全屏 -export const isFullscreen = () => document.fullscreenEnabled - -// 判断用户的操作系统是否安装了某字体 -export const isSupportFontFamily = (fontFamily: string) => { - if(typeof fontFamily !== 'string') return false - const arial = 'Arial' - if(fontFamily.toLowerCase() === arial.toLowerCase()) return true - const a = 'a' - const size = 100 - const width = 100 - const height = 100 - - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - - if(!ctx) return false - - canvas.width = width - canvas.height = height - ctx.textAlign = 'center' - ctx.fillStyle = 'black' - ctx.textBaseline = 'middle' - - const getDotArray = (_fontFamily: string) => { - ctx.clearRect(0, 0, width, height) - ctx.font = `${size}px ${_fontFamily}, ${arial}` - ctx.fillText(a, width / 2, height / 2) - const imageData = ctx.getImageData(0, 0, width, height).data - return [].slice.call(imageData).filter(item => item !== 0) - } - - return getDotArray(arial).join('') !== getDotArray(fontFamily).join('') -} - -// 获取图片的原始宽高 -export const getImageSize = (imgUrl: string) => { - return new Promise((resolve, reject) => { - const img = document.createElement('img') - img.src = imgUrl - img.style.opacity = '0' - document.body.appendChild(img) - - img.onload = () => { - const imgWidth = img.clientWidth - const imgHeight = img.clientHeight - - img.onload = null - img.onerror = null - - document.body.removeChild(img) - - resolve({ imgWidth, imgHeight }) - } - - img.onerror = () => { - img.onload = null - img.onerror = null - - reject('图片加载失败') - } - }) -} - -// 复制文本到剪贴板 -export const copyText = (text: string) => { - return new Promise((resolve, reject) => { - const fakeElement = document.createElement('button') - const clipboard = new Clipboard(fakeElement, { - text: () => text, - action: () => 'copy', - container: document.body, - }) - clipboard.on('success', e => { - clipboard.destroy() - resolve(e) - }) - clipboard.on('error', e => { - clipboard.destroy() - reject(e) - }) - document.body.appendChild(fakeElement) - fakeElement.click() - document.body.removeChild(fakeElement) - }) -} - -// 读取剪贴板 -export const readClipboard = () => { - if(navigator.clipboard) { - navigator.clipboard.readText().then(text => { - if(!text) return { err: '剪贴板为空或者不包含文本' } - return { text } - }) - } - return { err: '浏览器不支持或禁止访问剪贴板' } -} - -// 加密函数 -export const encrypt = (msg: string) => { - return CryptoJS.AES.encrypt(msg, CRYPTO_KEY).toString() -} - -// 解密函数 -export const decrypt = (ciphertext: string) => { - const bytes = CryptoJS.AES.decrypt(ciphertext, CRYPTO_KEY) - return bytes.toString(CryptoJS.enc.Utf8) -} - -// 获取DOM节点样式 -export const getStyle = (el: HTMLElement, style: string) => { - if(!el) return null - return window.getComputedStyle(el, null).getPropertyValue(style) -} - -// 检查元素是否处在可视区域内 -export const checkElementInViewport = (el: HTMLElement) => { - const rect = el.getBoundingClientRect() - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ) -} \ No newline at end of file diff --git a/src/views/Editor/Canvas/index.vue b/src/views/Editor/Canvas/index.vue index bdb6afde..4180be11 100644 --- a/src/views/Editor/Canvas/index.vue +++ b/src/views/Editor/Canvas/index.vue @@ -46,6 +46,7 @@ import { State } from '@/store/state' import { MutationTypes } from '@/store/constants' import { ContextmenuItem } from '@/components/Contextmenu/types' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' +import { getImageDataURL } from '@/utils/image' import useDropImage from '@/hooks/useDropImage' @@ -67,7 +68,11 @@ export default defineComponent({ const dropImageFile = useDropImage(viewportRef) watch(dropImageFile, () => { - console.log(dropImageFile.value) + if(dropImageFile.value) { + getImageDataURL(dropImageFile.value).then(dataURL => { + console.log(dataURL) + }) + } }) const viewportStyles = reactive({ diff --git a/src/views/Editor/Canvas/utils/createElement.ts b/src/views/Editor/Canvas/utils/createElement.ts new file mode 100644 index 00000000..9f901abf --- /dev/null +++ b/src/views/Editor/Canvas/utils/createElement.ts @@ -0,0 +1,120 @@ +import { createRandomCode } from '@/utils/common' +import { getImageSize } from '@/utils/image' +import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' +import { TableElementCell } from '@/types/slides' +import { + DEFAULT_IMAGE, + DEFAULT_TEXT, + DEFAULT_SHAPE, + DEFAULT_LINE, + DEFAULT_CHART, + DEFAULT_TABLE, +} from '@/configs/defaultElement' + +interface CommonElementPosition { + top: number; + left: number; + width: number; + height: number; +} + +interface LineElementPosition { + top: number; + left: number; + start: [number, number]; + end: [number, number]; +} + +export const insertImage = (imgUrl: string) => { + getImageSize(imgUrl).then(({ width, height }) => { + const scale = width / height + + if(scale < VIEWPORT_ASPECT_RATIO && width > VIEWPORT_SIZE) { + width = VIEWPORT_SIZE + height = width * scale + } + else if(height > VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO) { + height = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + width = height / scale + } + + return { + ...DEFAULT_IMAGE, + elId: createRandomCode(), + imgUrl, + width, + height, + } + }) +} + +export const insertChart = (chartType: string, data: Object) => { + return { + ...DEFAULT_CHART, + elId: createRandomCode(), + chartType, + data, + } +} + +export const insertTable = (rowCount: number, colCount: number) => { + const row: TableElementCell[] = new Array(colCount).fill({ colspan: 1, rowspan: 1, content: '' }) + const data: TableElementCell[][] = new Array(rowCount).fill(row) + + const DEFAULT_CELL_WIDTH = 80 + const DEFAULT_CELL_HEIGHT = 35 + const DEFAULT_BORDER_WIDTH = 2 + + const colSizes: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH) + const rowSizes: number[] = new Array(rowCount).fill(DEFAULT_CELL_HEIGHT) + + return { + ...DEFAULT_TABLE, + elId: createRandomCode(), + width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH, + height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH, + colSizes, + rowSizes, + data, + } +} + +export const insertText = (position: CommonElementPosition) => { + const { left, top, width, height } = position + return { + ...DEFAULT_TEXT, + elId: createRandomCode(), + left, + top, + width, + height, + } +} + +export const insertShape = (position: CommonElementPosition, svgCode: string) => { + const { left, top, width, height } = position + return { + ...DEFAULT_SHAPE, + elId: createRandomCode(), + left, + top, + width, + height, + svgCode, + } +} + +export const insertLine = (position: LineElementPosition, marker: [string, string], lineType: string) => { + const { left, top, start, end } = position + + return { + ...DEFAULT_LINE, + elId: createRandomCode(), + left, + top, + start, + end, + marker, + lineType, + } +} \ No newline at end of file diff --git a/src/views/Editor/Canvas/utils/elementCombine.ts b/src/views/Editor/Canvas/utils/elementCombine.ts index 38fbcbc4..c2fa5fd1 100644 --- a/src/views/Editor/Canvas/utils/elementCombine.ts +++ b/src/views/Editor/Canvas/utils/elementCombine.ts @@ -1,4 +1,4 @@ -import { createRandomCode } from '@/utils/index' +import { createRandomCode } from '@/utils/common' import { PPTElement } from '@/types/slides' // 组合元素(为当前所有激活元素添加一个相同的groupId) diff --git a/src/views/Editor/index.vue b/src/views/Editor/index.vue index ed0d6bb6..f966f4a4 100644 --- a/src/views/Editor/index.vue +++ b/src/views/Editor/index.vue @@ -17,7 +17,8 @@ import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue' import { useStore } from 'vuex' import { State } from '@/store/state' import { KEYCODE } from '@/configs/keyCode' -import { decrypt } from '@/utils/index' +import { decrypt } from '@/utils/crypto' +import { getImageDataURL } from '@/utils/image' import { message } from 'ant-design-vue' @@ -151,7 +152,9 @@ export default defineComponent({ } const pasteImageFile = (imageFile: File) => { - console.log(imageFile) + getImageDataURL(imageFile).then(dataURL => { + console.log(dataURL) + }) } const pasteText = (text: string) => {