当前位置: 首页 > article >正文

【PPTist】插入形状、插入图片、插入图表

一、插入形状

插入形状有两种情况,一种是插入固定的形状,
在这里插入图片描述

一种是插入自定义的形状。
插入固定的形状时,跟上一篇文章 绘制文本框 是一样一样的,都是调用的 mainStore.setCreatingElement() 方法,只不多传的类型不一样。还有插入线条,也是类似的。

mainStore.setCreatingElement({
  type: 'shape',
  data: shape,
})

所以咱们那接下来主要看插入自定义形状时的代码执行流程

1、点击
<Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10">
  <template #content>
    <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem>
  </template>
  <IconDown class="arrow" />
</Popover>

src/views/Editor/CanvasTool/index.vue

// 绘制自定义任意多边形
const drawCustomShape = () => {
  mainStore.setCreatingCustomShapeState(true)
  shapePoolVisible.value = false
}

src/store/main.ts

setCreatingCustomShapeState(state: boolean) {
  this.creatingCustomShape = state
},

有了 creatingCustomShape,下面的组件就会显示

<ShapeCreateCanvas
  v-if="creatingCustomShape"
  @created="data => insertCustomShape(data)"
/>
2、mousedown

src/views/Editor/Canvas/ShapeCreateCanvas.vue
触发 created 方法

const addPoint = (e: MouseEvent) => {
  const { pageX, pageY } = getPoint(e)
  isMouseDown.value = true

  if (closed.value) emit('created', getCreateData())
  else points.value.push([pageX, pageY])

  document.onmouseup = () => {
    isMouseDown.value = false
  }
}
3、created

src/views/Editor/Canvas/index.vue
插入任意多边形

// 插入自定义任意多边形
const insertCustomShape = (data: CreateCustomShapeData) => {
  const {
    start,
    end,
    path,
    viewBox,
  } = data
  const position = formatCreateSelection({ start, end })
  if (position) {
    const supplement: Partial<PPTShapeElement> = {}
    if (data.fill) supplement.fill = data.fill
    if (data.outline) supplement.outline = data.outline
    // 创建形状元素
    createShapeElement(position, { path, viewBox }, supplement)
  }

  // 清除 creatingCustomShape
  mainStore.setCreatingCustomShapeState(false)
}
4、mousemove

src/views/Editor/Canvas/ShapeCreateCanvas.vue
如果鼠标按下,添加 points,就会形成折线的效果。
可以看到只要起点和终点比较近就算闭合了,防止对不上

const updateMousePosition = (e: MouseEvent) => {
  // 如果鼠标按下,则添加点
  if (isMouseDown.value) {
    const { pageX, pageY } = getPoint(e, true)
    points.value.push([pageX, pageY])
    mousePosition.value = null
    return
  }

  // 更新鼠标位置
  const { pageX, pageY } = getPoint(e)
  mousePosition.value = [pageX, pageY]

  // 判断是否闭合
  if (points.value.length >= 2) {
    const [firstPointX, firstPointY] = points.value[0]
    if (Math.abs(firstPointX - pageX) < 5 && Math.abs(firstPointY - pageY) < 5) {
      closed.value = true
    }
    else closed.value = false
  }
  else closed.value = false
}

根据鼠标位置 mousePosition 计算 path

const path = computed(() => {
  let d = ''
  for (let i = 0; i < points.value.length; i++) {
    const point = points.value[i]
    if (i === 0) d += `M ${point[0]} ${point[1]} `
    else d += `L ${point[0]} ${point[1]} `
  }
  if (points.value.length && mousePosition.value) {
    d += `L ${mousePosition.value[0]} ${mousePosition.value[1]}`
  }
  return d
})

模版中的 path 元素随之更新

<svg overflow="visible">
	<path
      :d="path" 
      stroke="#d14424" 
      :fill="closed ? 'rgba(226, 83, 77, 0.15)' : 'none'" 
      stroke-width="2" 
    ></path>
</svg>
5、取消绘制的按键绑定
const keydownListener = (e: KeyboardEvent) => {
  const key = e.key.toUpperCase()
  if (key === KEYS.ESC) close()
  if (key === KEYS.ENTER) create()
}
onMounted(() => {
  message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', {
    duration: 0,
  })
  document.addEventListener('keydown', keydownListener)
})

以及鼠标右键也会取消绘制

@contextmenu.stop.prevent="close()"
const close = () => {
  mainStore.setCreatingCustomShapeState(false)
}

二、插入图片

src/views/Editor/CanvasTool/index.vue
插入图片也是一个自定义组件

<FileInput @change="files => insertImageElement(files)">
  <IconPicture class="handler-item" v-tooltip="'插入图片'" />
</FileInput>

这个组件里面实现上传功能的是 input 标签

<input 
  class="input"
  type="file" 
  name="upload" 
  ref="inputRef" 
  :accept="accept" 
  @change="$event => handleChange($event)"
>

上传之后插入图片元素

const insertImageElement = (files: FileList) => {
  const imageFile = files[0]
  if (!imageFile) return
  getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
}

src/utils/image.ts

获取图片宽高的方法,相比大家都挺熟悉的

/**
 * 获取图片的原始宽高
 * @param src 图片地址
 */
export const getImageSize = (src: string): Promise<ImageSize> => {
  return new Promise(resolve => {
    const img = document.createElement('img')
    img.src = src
    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
    }
  })
}

获取图片宽高之后,创建图片元素,通过 lefttop 将图片水平垂直居中
src/hooks/useCreateElement.ts

/**
 * 创建图片元素
 * @param src 图片地址
 */
const createImageElement = (src: string) => {
  getImageSize(src).then(({ width, height }) => {
    const scale = height / width

    if (scale < viewportRatio.value && width > VIEWPORT_SIZE) {
      width = VIEWPORT_SIZE
      height = width * scale
    }
    else if (height > VIEWPORT_SIZE * viewportRatio.value) {
      height = VIEWPORT_SIZE * viewportRatio.value
      width = height / scale
    }

    createElement({
      type: 'image',
      id: nanoid(10),
      src,
      width,
      height,
      left: (VIEWPORT_SIZE - width) / 2,
      top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
      fixedRatio: true,
      rotate: 0,
    })
  })
}

复习一下创建元素的方法,会把元素放到当前幻灯片的元素列表中

// 创建(插入)一个元素并将其设置为被选中元素
const createElement = (element: PPTElement, callback?: () => void) => {
  // 添加元素到元素列表
  slidesStore.addElement(element)
  // 设置被选中元素列表
  mainStore.setActiveElementIdList([element.id])

  if (creatingElement.value) mainStore.setCreatingElement(null)

  setTimeout(() => {
    // 设置编辑器区域为聚焦状态
    mainStore.setEditorareaFocus(true)
  }, 0)

  if (callback) callback()

  // 添加历史快照
  addHistorySnapshot()
}

三、插入图表

插入图表的方法,其实也差不多,就是往当前的幻灯片里添加一个图表对象。不过这里就不讲前面怎么添加元素了,讲讲后面怎么展示元素吧。先来看一下图表元素的数据:

const newElement: PPTChartElement = {
  type: 'chart',
  id: nanoid(10),
  chartType: CHART_TYPES[type],
  left: 300,
  top: 81.25,
  width: 400,
  height: 400,
  rotate: 0,
  themeColor: [theme.value.themeColor],
  gridColor: theme.value.fontColor,
  data: {
    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
    legends: ['系列1'],
    series: [
      [12, 19, 5, 2, 18],
    ],
  },
}

是这个元素对元素列表进行循环的
src/views/Editor/Canvas/index.vue

<EditableElement 
  v-for="(element, index) in elementList" 
  :key="element.id"
  :elementInfo="element"
  :elementIndex="index + 1"
  :isMultiSelect="activeElementIdList.length > 1"
  :selectElement="selectElement"
  :openLinkDialog="openLinkDialog"
  v-show="!hiddenElementIdList.includes(element.id)"
/>

src/views/Editor/Canvas/EditableElement.vue
这个组件中通过动态组件的方式控制显示哪个元素

<component
  :is="currentElementComponent"
  :elementInfo="elementInfo"
  :selectElement="selectElement"
  :contextmenus="contextmenus"
></component>
const currentElementComponent = computed<unknown>(() => {
  const elementTypeMap = {
    [ElementTypes.IMAGE]: ImageElement,
    [ElementTypes.TEXT]: TextElement,
    [ElementTypes.SHAPE]: ShapeElement,
    [ElementTypes.LINE]: LineElement,
    [ElementTypes.CHART]: ChartElement,
    [ElementTypes.TABLE]: TableElement,
    [ElementTypes.LATEX]: LatexElement,
    [ElementTypes.VIDEO]: VideoElement,
    [ElementTypes.AUDIO]: AudioElement,
  }
  return elementTypeMap[props.elementInfo.type] || null
})

我们的目标就是 ChartElementsrc/views/components/element/ChartElement/index.vue
然后图表那一小块是这个:src/views/components/element/ChartElement/Chart.vue,图表是通过 chartist 库实现的

import { BarChart, LineChart, PieChart } from 'chartist'

http://www.kler.cn/a/502261.html

相关文章:

  • thinkphp 5.0 结合redis 做延迟队列,队列无法被消费
  • ElasticSearch在Windows环境搭建测试
  • <论文>时序大模型如何应用于金融领域?
  • 回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测
  • vue3后台系统动态路由实现
  • 013:深度学习之神经网络
  • Nginx安全加固系列:404页面失去保护
  • 使用vue3、vite打包项目中组件库中的某一个组件,并将其推送至npm上
  • 【2】WLC的接口有哪些?
  • C# OpenCV机器视觉:转速测量
  • uni-app与uni-app x:跨端开发更上一层楼
  • 【Apache Paimon】-- Paimon Filesysterm Catalog 和 Hive Catalog 的适用场景
  • 深度优先和广度优先【栈、堆前端举例】
  • 1. npm 常用命令详解
  • 国产Docker可视化面板Dpanel的安装与功能解析
  • ASP.NET Core的部署、维护、日志记录和错误处理
  • 工业5G路由器 赋能车联网,让大巴车 “智” 行无忧
  • 25年无人机行业资讯 | 1.1 - 1.5
  • 【数据结构-堆】【哈希+最小堆】力扣1942. 最小未被占据椅子的编号
  • OpenAI O3模型:重构软件测试的未来
  • RK3568 Android 13 内置搜狗输入法小计
  • 【Excel】【VBA】根据某列的编号顺序筛选对应的行导入相应的sheet中
  • 网络学习记录2
  • TorchOptimizer:基于贝叶斯优化的PyTorch Lightning超参数调优框架
  • Java 模板变量替换——字符串替换器(思路Mybatis的GenericTokenParser)
  • react生命周期方法