pipipi-pikachu пре 5 година
родитељ
комит
c218a4ba5a

+ 2 - 31
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: '<div>“单击此处添加文本”</div>',
+  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,

+ 0 - 4
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: '<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,
         height: 188,
         rotate: 0,
-        padding: 10,
         opacity: 1,
         lineHeight: 1.5,
         segmentSpacing: 10,
         isLock: false,
-        textType: 'content',
         content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
       },
     ],

+ 1 - 1
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;

+ 4 - 4
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 | 

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

+ 18 - 0
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')
+}

+ 14 - 0
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)
+}

+ 31 - 0
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('')
+}

+ 8 - 0
src/utils/fullscreen.ts

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

+ 42 - 0
src/utils/image.ts

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

+ 0 - 209
src/utils/index.ts

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

+ 6 - 1
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({

+ 120 - 0
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,
+  }
+}

+ 1 - 1
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)

+ 5 - 2
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) => {