This commit is contained in:
pipipi-pikachu 2020-12-15 22:09:31 +08:00
parent 7f301cadfd
commit c218a4ba5a
15 changed files with 287 additions and 253 deletions

View File

@ -1,4 +1,4 @@
const DEFAULT_COLOR = '#888' const DEFAULT_COLOR = '#41464b'
export const DEFAULT_TEXT = { export const DEFAULT_TEXT = {
type: 'text', type: 'text',
@ -6,12 +6,10 @@ export const DEFAULT_TEXT = {
top: 0, top: 0,
width: 300, width: 300,
height: 0, height: 0,
padding: 5,
opacity: 1, opacity: 1,
lineHeight: 1.5, lineHeight: 1.5,
segmentSpacing: 5, segmentSpacing: 5,
textType: 'content', content: '请输入内容',
content: '<div>“单击此处添加文本”</div>',
} }
export const DEFAULT_IMAGE = { export const DEFAULT_IMAGE = {
@ -27,25 +25,6 @@ export const DEFAULT_SHAPE = {
lockRatio: false, 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 = { export const DEFAULT_LINE = {
type: 'line', type: 'line',
style: 'solid', style: 'solid',
@ -62,14 +41,6 @@ export const DEFAULT_CHART = {
height: 500, height: 500,
} }
export const DEFAULT_IFRAME = {
type: 'iframe',
left: 0,
top: 0,
width: 800,
height: 480,
}
export const DEFAULT_TABLE = { export const DEFAULT_TABLE = {
type: 'table', type: 'table',
left: 0, left: 0,

View File

@ -18,12 +18,10 @@ export const slides: Slide[] = [
borderColor: '#5b7d89', borderColor: '#5b7d89',
fill: 'rgba(220,220,220,0.8)', fill: 'rgba(220,220,220,0.8)',
shadow: '1px 1px 3px rgba(10,10,10,.5)', shadow: '1px 1px 3px rgba(10,10,10,.5)',
padding: 10,
opacity: 1, opacity: 1,
lineHeight: 1.5, lineHeight: 1.5,
segmentSpacing: 10, segmentSpacing: 10,
isLock: false, isLock: false,
textType: 'title',
content: '<div style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></div>', content: '<div style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></div>',
}, },
{ {
@ -79,12 +77,10 @@ export const slides: Slide[] = [
width: 220, width: 220,
height: 188, height: 188,
rotate: 0, rotate: 0,
padding: 10,
opacity: 1, opacity: 1,
lineHeight: 1.5, lineHeight: 1.5,
segmentSpacing: 10, segmentSpacing: 10,
isLock: false, isLock: false,
textType: 'content',
content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>', content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
}, },
], ],

View File

@ -2,7 +2,7 @@ import { MutationTypes } from './constants'
import { State } from './state' import { State } from './state'
import { Slide, PPTElement } from '@/types/slides' import { Slide, PPTElement } from '@/types/slides'
import { FONT_NAMES } from '@/configs/fontName' import { FONT_NAMES } from '@/configs/fontName'
import { isSupportFontFamily } from '@/utils/index' import { isSupportFontFamily } from '@/utils/fontFamily'
interface AddSlidesData { interface AddSlidesData {
index?: number; index?: number;

View File

@ -1,3 +1,5 @@
export type ElementType = 'text' | 'image' | 'shape' | 'line' | 'chart' | 'table'
export interface PPTElementBaseProps { export interface PPTElementBaseProps {
elId: string; elId: string;
isLock: boolean; isLock: boolean;
@ -51,8 +53,6 @@ export interface PPTShapeElement extends PPTElementBaseProps, PPTElementSizeProp
rotate?: number; rotate?: number;
opacity?: number; opacity?: number;
shadow?: string; shadow?: string;
text?: string;
textAlign?: string;
} }
export interface PPTLineElement extends PPTElementBaseProps { export interface PPTLineElement extends PPTElementBaseProps {
@ -73,7 +73,7 @@ export interface PPTChartElement extends PPTElementBaseProps, PPTElementSizeProp
data: Object; data: Object;
} }
export interface TableCell { export interface TableElementCell {
colspan: number; colspan: number;
rowspan: number; rowspan: number;
content: string; content: string;
@ -85,7 +85,7 @@ export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProp
theme: string; theme: string;
rowSizes: number[]; rowSizes: number[];
colSizes: number[]; colSizes: number[];
data: TableCell[][]; data: TableElementCell[][];
} }
export type PPTElement = PPTTextElement | export type PPTElement = PPTTextElement |

35
src/utils/clipboard.ts Normal file
View File

@ -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: '浏览器不支持或禁止访问剪贴板' }
}

18
src/utils/common.ts Normal file
View File

@ -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')
}

14
src/utils/crypto.ts Normal file
View File

@ -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)
}

31
src/utils/fontFamily.ts Normal file
View File

@ -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('')
}

8
src/utils/fullscreen.ts Normal file
View File

@ -0,0 +1,8 @@
// 进入全屏
export const enterFullscreen = document.documentElement.requestFullscreen
// 退出全屏
export const exitFullscreen = document.exitFullscreen
// 判断是否全屏
export const isFullscreen = () => document.fullscreenEnabled

42
src/utils/image.ts Normal file
View File

@ -0,0 +1,42 @@
interface ImageSize {
width: number;
height: number;
}
// 获取图片的原始宽高
export const getImageSize = (imgUrl: string): Promise<ImageSize> => {
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<string> => {
return new Promise(resolve => {
const reader = new FileReader()
reader.addEventListener('load', () => {
resolve(reader.result as string)
})
reader.readAsDataURL(file)
})
}

View File

@ -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)
)
}

View File

@ -46,6 +46,7 @@ import { State } from '@/store/state'
import { MutationTypes } from '@/store/constants' import { MutationTypes } from '@/store/constants'
import { ContextmenuItem } from '@/components/Contextmenu/types' import { ContextmenuItem } from '@/components/Contextmenu/types'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getImageDataURL } from '@/utils/image'
import useDropImage from '@/hooks/useDropImage' import useDropImage from '@/hooks/useDropImage'
@ -67,7 +68,11 @@ export default defineComponent({
const dropImageFile = useDropImage(viewportRef) const dropImageFile = useDropImage(viewportRef)
watch(dropImageFile, () => { watch(dropImageFile, () => {
console.log(dropImageFile.value) if(dropImageFile.value) {
getImageDataURL(dropImageFile.value).then(dataURL => {
console.log(dataURL)
})
}
}) })
const viewportStyles = reactive({ const viewportStyles = reactive({

View File

@ -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,
}
}

View File

@ -1,4 +1,4 @@
import { createRandomCode } from '@/utils/index' import { createRandomCode } from '@/utils/common'
import { PPTElement } from '@/types/slides' import { PPTElement } from '@/types/slides'
// 组合元素为当前所有激活元素添加一个相同的groupId // 组合元素为当前所有激活元素添加一个相同的groupId

View File

@ -17,7 +17,8 @@ import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State } from '@/store/state' import { State } from '@/store/state'
import { KEYCODE } from '@/configs/keyCode' 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' import { message } from 'ant-design-vue'
@ -151,7 +152,9 @@ export default defineComponent({
} }
const pasteImageFile = (imageFile: File) => { const pasteImageFile = (imageFile: File) => {
console.log(imageFile) getImageDataURL(imageFile).then(dataURL => {
console.log(dataURL)
})
} }
const pasteText = (text: string) => { const pasteText = (text: string) => {