Vue进阶之Vue无代码可视化项目(九)

背景介绍、方案设计

  1. 总结项目经验、业务价值
    平常写代码内容、能力都是具备的,但是很多没有将代码背后的经验、业务背后的内容没有总结下来,
  2. 公司中低代码项目难度较高,一般不会参与,因此这个无代码可视化项目为了让大家练手,有一个体系架构
  3. 项目核心:数据源管理与加工、页面组装、流程引擎、低代码编辑器
  4. 要有复用、封装的思想
    写好的轮子要后面的人更轻松的使用。所以要时时刻刻有组件封装和开源的意识,每天只在做代码,写代码都没有成长呢,是因为没有思考,没有思考怎么让代码更完善,让代码更好。哪怕是一个简单的登录注册,里面都有很多可优化的点。
  • 表单
    • 校验
    • 密码加密
    • 验证码

登录注册真这么简单吗?

  • 微信校验(authing、auth2) 做第三方鉴权的
  • 项目发展到一定体量之后,会有单独的用户中心,专门做登录、鉴权等处理的
  1. 框架的选型
  2. 只要涉及到拖拽,会下意识的反应是drag事件,但其实经过我们实践,drag事件有很多浏览器受限的时候,因此,我们选择使用的是基于move事件封装vue draggable
  3. 自由拖拽,在做的低代码平台不考虑分辨率的兼容,就可以用这种方案。但是如果要考虑分辨率的兼容,这种方式是不太好处理的。因此,在之后做架构设计的时候,要考虑他背后的本质逻辑是什么,而不是停留在技术本身,要对每一项技术说出优劣势,能说出各个技术栈,各个技术点,各个框架库他的用武之地是什么,这样才算一个优秀的架构师,放在平常的时候,一定要刻意练习才行。
  4. 低代码平台参考借鉴的是国外的平台:Glide
  5. 发布之后的操作是什么?
    一般无代码平台都不涉及到私有化部署,所以都是基于公有云部署,这样所有的用户都使用我们这一套代码,这样就不会牵扯按量计费的逻辑
    国内涉及到的部署,牵扯到两个问题,主机ECS证书、域名,主机用的是本公司的服务器,但是,在域名和证书的时候,很多用户想要的是自定义域名,自定义域名涉及到域名的备案和域名证书的申请,域名备案跟随的是自己的公司主体,证书问题可以使用Caddy,可以自动的分发https,一般都会借助docker caddy

Canvas Table

针对普通的table,数据量特别大的时候,要怎么做呢?

  1. 需求:加分页、滚动加载
  2. 虚拟表格(列表) vue-virtual-scroller
  3. canvas table,因为画布性能好,他的性能瓶颈取决于你的绘图算法

创建一个新的vue项目

pnpm create vue@latest

请添加图片描述

cd vue-canvas-table
pnpm i
pnpm format
pnpm dev

请添加图片描述

普通表格的效果

<template>
  <div>
    <!-- 简单表格的渲染 -->
    <table>
      <thead>
        <tr>
          <th v-for="column in data.columns" :key="column.key">{{column.title}}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="record in data.dataSource" :key="record.key">
          <td v-for="column in data.columns" :key="column.key">
            {{ record[column.key] }}
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
// 确认表格的数据
const data = {
  columns:[{
    title: '姓名',
    key: 'name',
    width: 100
  },{
    title: '年龄',
    key: 'age',
    width: 100
  }],
  dataSource: Array.from({length: 100000}).map((item,index)=>({
    key:index,
    name:`name-${index}`,
    age: Math.floor(Math.random() * 100)
  }))
}
</script>

<style scoped>
table{
  border-collapse: collapse;
}
td{
  border: 1px solid #ccc;
  width: 100px;
}
</style>

请添加图片描述

Canvas上手

<template>
  <div>
    <!-- 当window.devicePixelRatio为2的时候 -->
    <!-- <canvas ref="canvas" width="1600" height="1200">对不起,您的浏览器不支持</canvas> -->
    <canvas ref="canvas" width="800" height="600">对不起,您的浏览器不支持</canvas>
  </div>
</template>

<script setup>
// 为了获取到canvas dom
import {ref,onMounted} from 'vue'

// 获取canvas dom引用
// 注意一定不要用dom操作
// const canvas = document.querySelector("#canvas")

//ts写法:
// const canvas = ref<HTMLCanvasElement|null>(null)
// 这里直接
const canvas = ref(null)

onMounted(() => {
  // 1.获取canvas dom
  // 2.获取绘制上下文
  const ctx=canvas.value.getContext('2d')  //还可以是webgl(这里参考three.js)
  // 画笔的概念
  // 假设你现在在用毛笔练书法
  // 按下去
  ctx.beginPath()
  // 开始写字
  ctx.moveTo(0,0)
  // ctx.lineTo(200,200)   //当window.devicePixelRatio为2的时候
  ctx.lineTo(100,100)
  // 设置画笔
  // fill 填充 
  // stroke 描边
  ctx.strokeStyle='red'
  // 画文字
  ctx.font="48px serif"
  ctx.fillText("Hello world",100,200)  //content,x,y
  ctx.stroke()

  // canvas是位图,需要处理缩放的问题,很多图表或图画,渲染后会模糊,因此一般都是*2或*3这样
  // svg是矢量图
  // 怎么获取浏览器设备的像素比
  // console.log(window.devicePixelRatio); 

  
  // 回锋收笔
  ctx.closePath()

});
</script>

<style scoped>
canvas{
  background-color: #eee;
  width: 800px;   /* 这个是正常宽度 */
  height: 600px;  /* 这个是正常高度 */
}
</style>

请添加图片描述

Canvas画表格-画基本表格

<template>
  <div>
    <!-- 当window.devicePixelRatio为2的时候 -->
    <!-- <canvas ref="canvas" width="1600" height="1200">对不起,您的浏览器不支持</canvas> -->
    <canvas ref="canvas" width="800" height="600">对不起,您的浏览器不支持</canvas>
  </div>
</template>

<script setup>
// 为了获取到canvas dom
import {ref,onMounted} from 'vue'

// 获取canvas dom引用
// 注意一定不要用dom操作
// const canvas = document.querySelector("#canvas")

//ts写法:
// const canvas = ref<HTMLCanvasElement|null>(null)
// 这里直接
const canvas = ref(null)

const data = {
  columns:[{
    title: '姓名',
    key: 'name',
    width: 100
  },{
    title: '年龄',
    key: 'age',
    width: 100
  }],
  dataSource: Array.from({length: 10}).map((item,index)=>({
    key:index,
    name:`name-${index}`,
    age: Math.floor(Math.random() * 100)
  }))
}

onMounted(() => {
  const ctx=canvas.value.getContext('2d')  //还可以是webgl(这里参考three.js)
  
  const {columns,dataSource}=data

  const pixelRatio=window.devicePixelRatio

  const cellWidth = 100 * pixelRatio
  const cellHeight = 50 * pixelRatio
  const padding = 10
  // 画表格

  // 画表头
  for (let i=0; i<columns.length; i++){
    const column=columns[i]
    ctx.font =`bold ${16*pixelRatio}px serif`
    ctx.fillText(column.title,i*cellWidth+padding,cellHeight/2)
    console.log("column",column);
  } 

  // 表格
  for (let i=0; i<dataSource.length; i++){
    const record=dataSource[i]
    for (let j=0; j<columns.length; j++){
      const column=columns[j]
      ctx.font =`${16*pixelRatio}px serif`
      ctx.fillText(record[column.key],j*cellWidth+padding,cellHeight*(i+1)+cellHeight/2)
    }
  } 

  ctx.beginPath()
  
  ctx.closePath()

});
</script>

<style scoped>
canvas{
  background-color: #eee;
  width: 800px;   /* 这个是正常宽度 */
  height: 600px;  /* 这个是正常高度 */
}
</style>

请添加图片描述

CanvasTable处理事件系统

Canvas是没有事件机制的,因此这个事件(鼠标点击事件等)需要我们自己代理去做,把这个事件计算,拿到位置等后处理
选中单元格

<template>
  <div>
    <!-- 当window.devicePixelRatio为2的时候 -->
    <!-- <canvas ref="canvas" width="1600" height="1200">对不起,您的浏览器不支持</canvas> -->
    <!-- <canvas ref="canvas" width="800" height="600">对不起,您的浏览器不支持</canvas> -->
    <!-- 当window.devicePixelRatio为1.25的时候 -->
    <canvas ref="canvas" width="1000" height="750">对不起,您的浏览器不支持</canvas>
  </div>
</template>

<script setup>
// 为了获取到canvas dom
import {ref,onMounted,onUnmounted,reactive} from 'vue'

// 定义事件
defineEmits('click')


// 获取canvas dom引用
// 注意一定不要用dom操作
// const canvas = document.querySelector("#canvas")

//ts写法:
// const canvas = ref<HTMLCanvasElement|null>(null)
// 这里直接
const canvas = ref(null)

const pixelRatio=window.devicePixelRatio
console.log("pixelRatio",pixelRatio);

const cellWidth = 100 * pixelRatio
const cellHeight = 50 * pixelRatio

const selectedCell = reactive({row:-1,column:-1})  

const data = {
  columns:[{
    title: '姓名',
    key: 'name',
    width: 100
  },{
    title: '年龄',
    key: 'age',
    width: 100
  }],
  dataSource: Array.from({length: 10}).map((item,index)=>({
    key:index,
    name:`name-${index}`,
    age: Math.floor(Math.random() * 100)
  }))
}

const handleClick=(ev)=>{
  // 当前点了哪里
  const {left,top}=canvas.value.getBoundingClientRect()   //画布距离屏幕左边,顶部的距离
  const x=ev.clientX-left  //鼠标距离屏幕左边的距离-画布距离屏幕左边的距离=》鼠标距离画布左侧的长度
  const y=ev.clientY-top  //鼠标距离屏幕顶部的距离-画布距离屏幕顶部的长度=》鼠标距离画布顶部的长度  

  // 判断我点的x和y,落到了哪个单元格下  碰撞检测的问题
  const rowIndex=Math.floor(y*1.25/cellHeight)-1  //再减去一个表格头部(头部不算)
  console.log("x,y",x,y);
  const colIndex=Math.floor(x*1.25/cellWidth)
  console.log("rowIndex,colIndex",rowIndex,colIndex);

  // 我只需要判断 rowIndex >= 0 rowIndex < data.dataSource.length 说明被选中
  if(rowIndex>=0 && rowIndex<data.dataSource.length){
    // 说明有单元格被选中
    selectedCell.row=rowIndex
    selectedCell.column=colIndex

    // this.$emit('click')

    // 重绘表格
    drawTable()
  }
}

const randomColor=()=>{
  const random = Math.random()

  if(random>0&&random<0.3){
    return 'red'
  }else if(random>0.3&&random<0.6){
    return 'blue'
  }else{
    return 'yellow'
  }
  // return `rgba(${random*255},${random*255},${random*255},1)`
}


const drawTable=()=>{
  canvas.value.addEventListener('click',handleClick)

  const ctx=canvas.value.getContext('2d')  //还可以是webgl(这里参考three.js)

  const {columns,dataSource}=data

  // 清除选中
  ctx.clearRect(0,0,1000,750)
  
  const padding = 10

  ctx.beginPath()
  // 画表格

  // 画表头
  for (let i=0; i<columns.length; i++){
    const column=columns[i]
    ctx.font =`bold ${16*pixelRatio}px serif`
    ctx.fillText(column.title,i*cellWidth+padding,cellHeight/2)
    console.log("column",column);
  } 

  // 表格
  for (let i=0; i<dataSource.length; i++){
    const record=dataSource[i]
    for (let j=0; j<columns.length; j++){
      // 做判断,如果被选中了,那么就需要画矩形来高亮单元格
      const column=columns[j]
      // 先画背景
      // 画矩形其实是一个fill填充操作
      if(selectedCell.row===i&&selectedCell.column===j){
        ctx.fillStyle = randomColor()
        ctx.fillRect(j*cellWidth,cellHeight*(i+1),cellWidth,cellHeight)
        ctx.fillStyle = 'black'
      }

      // 再写文字,文字填充
      ctx.font =`${16*pixelRatio}px serif`
      ctx.fillText(record[column.key],j*cellWidth+padding,cellHeight*(i+1)+cellHeight/2)
      
    }
  } 

  ctx.closePath()

}

onMounted(() => { 
  drawTable()
  
});


onUnmounted(() => {
  canvas.value?.removeEventListener('click',handleClick)
})
</script>

<style scoped>
canvas{
  background-color: #eee;
  width: 800px;   /* 这个是正常宽度 */
  height: 600px;  /* 这个是正常高度 */
}
</style>

请添加图片描述

CanvasTable表格滚动

<template>
  <div>
    <input type="text" @input="handleSearch">
    <!-- 当window.devicePixelRatio为2的时候 -->
    <!-- <canvas ref="canvas" width="1600" height="1200">对不起,您的浏览器不支持</canvas> -->
    <canvas ref="canvas" width="800" height="600">对不起,您的浏览器不支持</canvas>
    <!-- 当window.devicePixelRatio为1.25的时候 -->
    <!-- <canvas ref="canvas" width="1000" height="750">对不起,您的浏览器不支持</canvas> -->
  </div>
</template>

<script setup>
// 为了获取到canvas dom
import {ref,onMounted,onUnmounted,reactive,watch} from 'vue'

// 定义事件
defineEmits('click')


// 获取canvas dom引用
// 注意一定不要用dom操作
// const canvas = document.querySelector("#canvas")

//ts写法:
// const canvas = ref<HTMLCanvasElement|null>(null)
// 这里直接
const canvas = ref(null)

const pixelRatio=window.devicePixelRatio
console.log("pixelRatio",pixelRatio);

const cellWidth = 100 * pixelRatio
const cellHeight = 50 * pixelRatio

// 当前可视区域
let startRow=0;

const visableRows = Math.ceil(600*pixelRatio/cellHeight)-1 //可视区域:高度/单元格宽度-1(表头那一行)
const selectedCell = reactive({row:-1,column:-1})  

// 响应式数据
// 数据跟UI本身没有关系
// 我们做筛选,影响的是数据
const d={
  columns:[{
    title: '姓名',
    key: 'name',
    width: 100
  },{
    title: '年龄',
    key: 'age',
    width: 100
  }],
  dataSource: Array.from({length: 1000}).map((item,index)=>({
    key:index,
    name:`name-${index}`,
    age: Math.floor(Math.random() * 100)
  })),
}

const data = reactive({
  ...d,
  tmpDataSource:d.dataSource,
})

const handleClick=(ev)=>{
  // 当前点了哪里
  const {left,top}=canvas.value.getBoundingClientRect()   //画布距离屏幕左边,顶部的距离
  const x=ev.clientX-left  //鼠标距离屏幕左边的距离-画布距离屏幕左边的距离=》鼠标距离画布左侧的长度
  const y=ev.clientY-top  //鼠标距离屏幕顶部的距离-画布距离屏幕顶部的长度=》鼠标距离画布顶部的长度  

  // 判断我点的x和y,落到了哪个单元格下  碰撞检测的问题
  // const rowIndex=Math.floor(y*1.25/cellHeight)-1  //再减去一个表格头部(头部不算)
  const rowIndex=Math.floor(y*pixelRatio/cellHeight)-1+startRow  //再减去一个表格头部(头部不算)  加startRow是为了要知道她真实行的index
  console.log("x,y",x,y);
  // const colIndex=Math.floor(x*1.25/cellWidth)
  const colIndex=Math.floor(x*pixelRatio/cellWidth)
  console.log("rowIndex,colIndex",rowIndex,colIndex);

  // 我只需要判断 rowIndex >= 0 rowIndex < data.dataSource.length 说明被选中
  if(rowIndex>=0 && rowIndex<data.dataSource.length){
    // 说明有单元格被选中
    selectedCell.row=rowIndex
    selectedCell.column=colIndex

    // this.$emit('click')

    // 重绘表格
    drawTable()
  }
}

const randomColor=()=>{
  const random = Math.random()

  if(random>0&&random<0.3){
    return 'red'
  }else if(random>0.3&&random<0.6){
    return 'blue'
  }else{
    return 'yellow'
  }
  // return `rgba(${random*255},${random*255},${random*255},1)`
}


const drawTable=()=>{
  canvas.value.addEventListener('click',handleClick)

  const ctx=canvas.value.getContext('2d')  //还可以是webgl(这里参考three.js)

  const {columns,dataSource}=data

  // 清除选中
  ctx.clearRect(0,0,1000,750)
  
  const padding = 10

  ctx.beginPath()
  // 画表格

  // 画表头
  for (let i=0; i<columns.length; i++){
    const column=columns[i]
    ctx.font =`bold ${16*pixelRatio}px serif`
    ctx.fillText(column.title,i*cellWidth+padding,cellHeight/2)
    console.log("column",column);
  } 

  // 表格
  for (let i=startRow; i<startRow+visableRows; i++){
    const record=dataSource[i]
    for (let j=0; j<columns.length; j++){
      // 做判断,如果被选中了,那么就需要画矩形来高亮单元格
      const column=columns[j]
      // 先画背景
      // 画矩形其实是一个fill填充操作
      if(selectedCell.row===i&&selectedCell.column===j){
        ctx.fillStyle = randomColor()
        ctx.fillRect(j*cellWidth,cellHeight*(i-startRow+1),cellWidth,cellHeight)  //减startRow是因为视窗大小限制,要判断他的相对位置
        ctx.fillStyle = 'black'
      }

      // 再写文字,文字填充
      ctx.font =`${16*pixelRatio}px serif`
      ctx.fillText(record[column.key],j*cellWidth+padding,cellHeight*(i-startRow+1)+cellHeight/2)
      
    }
  } 

  ctx.closePath()

}

// 加滚动条
const handleWheel = ()=>{
  document.addEventListener("wheel", (ev)=>{
    console.log(ev);
    const {deltaY}=ev
    if(deltaY<0){
      // startRow-=1
      // if(startRow<0){
      //   startRow=0
      // }
      startRow=Math.max(0,startRow-1)
    }else{
      // startRow+=1
      // if(startRow+visableRows>data.dataSource.length){
      //   startRow=data.dataSource.length-visableRows
      // }
      startRow=Math.min(data.dataSource.length-visableRows,startRow+1)
    }
    drawTable()
  },false)
}

const handleSearch=(ev)=>{
  const {value}=ev.target
  // data.dataSource.filter((item)=>item.name.includes(value))
  // vue中间数组的操作,一定要注意
  // 太大变动会导致Vue重新渲染(rerender)

  // 但是我们现在不同,因为我们使用的是canvas Table的方案
  data.dataSource=data.tmpDataSource.filter((item)=>item.name.includes(value))
}


onMounted(() => { 
  drawTable()
  
  handleWheel()
});


watch(
  // 监听data中的dataSource,并重新绘制
  () => data.dataSource,
  ()=>drawTable()
)


onUnmounted(() => {
  canvas.value?.removeEventListener('click',handleClick)
})
</script>

<style scoped>
canvas{
  background-color: #eee;
  width: 800px;   /* 这个是正常宽度 */
  height: 600px;  /* 这个是正常高度 */
}
</style>

请添加图片描述

Vue组件封装思想

拖拽组件 —smooth-dnd

现在smooth-dnd属于是年久失修的一个状态了,因为目前已经不支持vue3的这部分内容了,所以我们使用的时候,需要先在他基础上拓展一下
将拖拽的逻辑提取出来DndContainer.js文件,做封装

DndDemo1.vue

<template>
    <div class="content">
      <div class="material-panel" ref="materialContainer">
        <div class="material-card" v-for="material in materials" :key="material.type">
          {{ material.title }}
        </div>
      </div>
      <div class="layout" ref="layoutContainer">
        <div class="layout-item" v-for="(d, i) in data" :key="i">{{ d.content }}</div>
      </div>
    </div>
    <DndContainer>
      <div>123</div>
    </DndContainer>
  </template>
  <script setup>
  import { onMounted, ref, toRaw } from 'vue'
  import { smoothDnD, dropHandlers } from 'smooth-dnd'
  import { DndContainer } from './DndContainer'
  
  smoothDnD.dropHandler = dropHandlers.reactDropHandler().handler
  
  const materials = [
    { title: '图片', type: 'image' },
    { title: '文本', type: 'text' },
    { title: '按钮', type: 'button' },
    { title: '输入框', type: 'input' },
    { title: '下拉框', type: 'select' },
    { title: '单选框', type: 'radio' }
  ]
  const data = ref([
    {
      id: 1,
      type: 'image',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content:
        'https://img.alicdn.com/imgextra/i4/O1CN01QYQ4QI1qZQYQYQ4QI_!!6000000001382-2-tps-200-200.png'
    },
    {
      id: 2,
      type: 'text',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content: '文本'
    },
    {
      id: 3,
      type: 'button',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content: '按钮'
    },
    {
      id: 4,
      type: 'input',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content: '输入框'
    },
    {
      id: 5,
      type: 'select',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content: '下拉框'
    },
    {
      id: 6,
      type: 'radio',
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      content: '单选框'
    }
  ])
  
  const materialContainer = ref(null)
  const layoutContainer = ref(null)
  
  function arrayMove(array, from, to) {
    const newArray = array.slice()
    newArray.splice(to < 0 ? newArray.length + to : to, 0, newArray.splice(from, 1)[0])
  
    return newArray
  }
  
  const applyDrag = (arr, dragResult) => {
    const { removedIndex, addedIndex, payload } = dragResult
  
    const result = [...arr]
  
    // 没做操作
    if (addedIndex === null) return result
  
    // 添加
    if (addedIndex !== null && removedIndex === null) {
      result.splice(addedIndex, 0, {
        id: `${Math.random()}`,
        ...payload
      })
    }
  
    // 移动
    if (addedIndex !== null && removedIndex !== null) {
      return arrayMove(result, removedIndex, addedIndex)
    }
  
    return result
  }
  
  onMounted(() => {
    // 物料区
    smoothDnD(materialContainer.value, {
      groupName: 'material',
      behaviour: 'copy',
      getChildPayload(index) {
        return {
          id: data.value.length + 1,
          type: materials[index].type,
          title: materials[index].title,
          content: materials[index].title,
          x: 0,
          y: 0,
          width: 100,
          height: 100
        }
      }
    })
    // 编排区
    smoothDnD(layoutContainer.value, {
      groupName: 'material',
      onDrop(dropResult) {
        const result = applyDrag(toRaw(data.value), dropResult)
        data.value = result
        // 问题怎么引起, DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
        // dom 复用引起
        console.log('🚀 ~ file: DndDemo1.vue:140 ~ onDrop ~ result:', result)
      }
    })
  })
  </script>
  <style scoped>
  .content {
    color: #fff;
    display: flex;
    background-color: #000;
  }
  
  .material-panel {
    width: 200px;
    padding: 10px;
    box-sizing: border-box;
    overflow-y: auto;
  }
  
  .material-card {
    width: 100%;
    height: 50px;
    border: 1px solid #ccc;
    margin-bottom: 10px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: move;
    user-select: none;
  }
  .layout-item {
    width: 100%;
    height: 100px;
    background-color: #002222;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: move;
    user-select: none;
    /* border: 1px solid transparent; */
  
    &:hover {
      /* border-color: #ccc; */
      box-shadow: inset 0 0 1px 1px #ccc;
    }
  }
  </style>
  

DndContainer.js

import { defineComponent, h } from 'vue'
import { smoothDnD } from 'smooth-dnd'

export const DndContainer = defineComponent({
  name: 'DndContainer',
  setup() {
    return { dndContainer: null }
  },
  mounted() {
    const containerElement = this.$refs.container || this.$el
    this.container = smoothDnD(containerElement, { groupName: 'material' })
  },
  render() {
    return h(
      'div',
      {
        ref: 'container',
        class: 'dnd-container',
        style: {
          color: 'pink',
          fontSize: '20px'
        }
      },
      this.$slots.default?.()
    )
  }
})

请添加图片描述

CanvasTable封装

CanvasTable.js

import { defineComponent, h, onMounted, onUnmounted, ref, watch } from 'vue'

export const CanvasTable = defineComponent({
  name: 'CanvasTable',
  // 组件初始化
  setup() {
    const canvas = ref(null)
    const container = ref(null)
    const data = ref()
    const handleClick = () => {}

    const drawTable = () => {
      console.log('drawTable')
    }

    onMounted(() => {
      drawTable()
    })

    onUnmounted(() => {})

    watch(data, () => {
      drawTable()
    })

    return {
      handleClick,
      canvas,
      container
    }
  },
  // 渲染
  render() {
    return h('div', {}, this.$slots.default?.())
  }
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值