文章目录
- 前言
- 一、思考过程
- 二、完善
- 1、上数据库SQL(SQL中增加了一个字段“trajectory”)
- 2、上后端代码(不想上。。。后端也就是在实体类中增加一个“trajectory”字段。。。)
- 3、上前端代码(OK,这就有挺多的代码了)
- (1)首先需要修改的是组件页面(未用GIT导致我不好复制代码片段所以全篇粘贴了)
- (2)列表回显数据使用了子组件的方式将index作为canvas的动态ID属性,先拿到轨迹绘制点的数据,定义xMin(x轴最小值=Number.MAX_VALUE,如果数据端循环时判断x坐标是否大于xMax大于则将x坐标存入xMax)、xMax(x轴最大值=-Number.MAX_VALUE,如果数据端循环时判断x坐标是否大于xMax大于则将x坐标存入xMax)、(yMin、yMax)与x轴相同的方式取值;此时 宽度(width = xMax-xMin),高度(height = yMax-yMin)得出轨迹点的宽高,props.canvasWidth(组件定义的宽度), props.canvasHeight(组件定义的高度),调用drawCanvas中的toCanvas函数即可得到绘制数据
- (3)这组件新增后在列表回显使用子组件并且封装的ts工具类(这其中就有门道了,一个是缩放(当宽度/高度大于画布的宽高时需要使用到:scale = Math.min(scaleX,scaleY)),一个是偏移量(当宽度/高度小于画布宽高时使用到:offsetX,offsetY)用来居中,咱可以将一个canvas画布的起始位置按{x:0,y:0}为初始值,然后轨迹呢是x存储一位,y存储一位,循环轨迹列表按照下标偶数为x,奇数为y,计算方式为:(arr[i] - xMin) * scale + offsetX,其中arr[i]-xMin是将该轨迹点的位置按照0为起始位置计算)
- 三、总结
前言
为自己搭建一个可以自我思考的平台,其核心为“心想事成”。
一、思考过程
画轨迹的功能实现,想着应该是挺难的一个功能点,在实际开发过程中发现,鼠标在canvas上拖拽实现起来并不困难,而回显canvas轨迹内容实现起来却出现了些问题,不过经过我的努力,也已解决了,哈哈哈!
二、完善
1、上数据库SQL(SQL中增加了一个字段“trajectory”)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for component
-- ----------------------------
DROP TABLE IF EXISTS `component`;
CREATE TABLE `component` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`type_id` bigint(0) NULL DEFAULT NULL COMMENT '类型ID',
`img_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件图片地址',
`trajectory` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '轨迹点记录',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件名称',
`sort` int(0) NULL DEFAULT NULL COMMENT '排序',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '组件' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2、上后端代码(不想上。。。后端也就是在实体类中增加一个“trajectory”字段。。。)
3、上前端代码(OK,这就有挺多的代码了)
(1)首先需要修改的是组件页面(未用GIT导致我不好复制代码片段所以全篇粘贴了)
<template>
<div>
<!-- 操作 -->
<div>
<div style="margin-bottom: 15px">
<el-autocomplete
v-model="name"
:fetch-suggestions="querySearch"
:clearable="false"
value-key="value"
class="inline-input w-50"
placeholder="请输入组件名"
@select="handleSelect"
@keyup.enter.native="getDataList(true)"
/>
</div>
<div style="margin-bottom: 15px">
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</div>
</div>
<!-- 表格 -->
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
style="width: 100%; margin-bottom: 15px;"
>
<el-table v-loading="loading" :data="tableData" border>
<template slot="empty">
<el-empty :image-size="100" description="暂无数据"></el-empty>
</template>
<el-table-column label="名称" align="center" prop="name"/>
<el-table-column label="排序" align="center" prop="sort"/>
<el-table-column label="组件" align="center" prop="imgUrl">
<template #default="scope">
<el-image v-if="scope.row.imgUrl" style="width: 100px; height: 100px" :src="scope.row.imgUrl" :fit="'fill'" />
<component-canvas v-else :index="scope.$index" :trajectoryList="scope.row.trajectory"></component-canvas>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="primary" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-space>
<!-- 分页 -->
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
>
<el-pagination
:hide-on-single-page="hidePageVisible"
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 50, 100, 200]"
:small="small"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-space>
<!-- 新增_修改 弹框 -->
<el-dialog v-model="dialogFormVisible" @close="resetForm(dataFormRef)" :title="title" width="30%" center>
<el-form ref="dataFormRef" :model="dataForm" :rules="rules">
<el-form-item label="组件" :label-width="formLabelWidth" prop="name">
<el-input v-model="dataForm.name" placeholder="请输入组件名称" autocomplete="off" />
</el-form-item>
<el-form-item label="组件类型" :label-width="formLabelWidth" prop="typeId">
<el-select v-model="dataForm.typeId" class="m-2" placeholder="Select" filterable size="large">
<el-option
v-for="item in componentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="排序" :label-width="formLabelWidth" prop="sort">
<el-input v-model="dataForm.sort" placeholder="请输入排序" autocomplete="off" />
</el-form-item>-->
<!-- <el-form-item label="组件图片" :label-width="formLabelWidth" prop="imgUrl">
<my-upload-file :imgUrl="dataForm.imgUrl" @uploaded="getImgUrl"></my-upload-file>
</el-form-item>-->
<el-form-item label="组件轨迹" :label-width="formLabelWidth" prop="trajectory">
<canvas @mousedown="handlerMousedown" @mouseup="handlerMoseUp" ref="canvas" width="300" height="300" style="width: 300px; height: 300px; border: 1px solid #78edaf;"></canvas>
<el-button @click="clearAndRedraw">清除画布内容</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog(false, dataFormRef)">取消</el-button>
<el-button type="primary" @click="submitDataForm(dataFormRef)">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref, reactive, watch, defineComponent, nextTick, Ref} from 'vue'
import {page as keywordPage} from '@/api/keywordApi'
import {page as keywordRelationPage} from '@/api/keywordRelationApi'
import {ElMessage, ElMessageBox, FormInstance, FormRules, UploadProps, UploadUserFile} from "element-plus";
import {page, delById, getById, save, update} from '@/api/componentApi'
import type { UploadFile } from 'element-plus'
import {list} from "@/api/componentTypeApi";
interface RestaurantItem {
value: string
count: string
createTime: Date
}
interface Option {
label: string,
value: number
}
const componentTypeOptions:Ref<Option[]> = ref([])
const getComponentTypeList = () => {
list().then(resp => {
if (resp.code != 200) return
let options:Option[] = []
resp.data.forEach((item, index) => {
let option:Option = {
label: item.name,
value: item.id
}
if (index == 0) {
dataForm.typeId = item.id
}
options.push(option)
})
componentTypeOptions.value = options
})
}
onMounted(() => {
getComponentTypeList()
})
const restaurants: Ref<RestaurantItem[]> = ref([])
interface KeyWord {
key: string,
value: string,
count: number,
createTime: Date,
}
const querySearch = (queryString: string, cb: any) => {
if (queryString && queryString != '') {
let params = {
current: 1,
size: 10,
keyword: queryString,
}
let keywordList:KeyWord[] = []
params.keyword = queryString
keywordRelationPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword: KeyWord = {
key: item.keyword,
value: item.keywordRelationSentence,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
const results = queryString
? keywordList.filter(createFilter(queryString))
: keywordList
// call callback function to return suggestions
cb(results)
})
} else {
const results = queryString
? restaurants.value.filter(createInitFilter(queryString))
: restaurants.value
// call callback function to return suggestions
cb(results)
}
}
const createFilter = (queryString: string) => {
return (item) => {
return (
item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const createInitFilter = (queryString: string) => {
return (restaurant: RestaurantItem) => {
return (
restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const loadAll = () => {
let params = {
current: 1,
size: 10,
}
let keywordList:RestaurantItem[] = []
keywordPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword:RestaurantItem = {
value: item.keyword,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
})
return keywordList;
}
const handleSelect = (item: RestaurantItem) => {
console.log(item, 'restaurantItem')
}
onMounted(() => {
restaurants.value = loadAll()
})
const direction = ref('horizontal')
const fillRatio = ref(30)
const tableData = ref([])
const name = ref('')
const total = ref(0)
const loading = ref('')
const queryParams = ref({
current: 1,
size: 10,
name: '',
})
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const hidePageVisible = ref(false)
const getDataList = (reset) => {
if (reset) {
queryParams.value.current = 1
}
if (name.value) {
queryParams.value.name = name.value
}
page(queryParams.value).then(resp => {
tableData.value = resp.data.records
total.value = resp.data.total
if (resp.data.total < queryParams.value.size) {
hidePageVisible.value = true
resp.data.records.forEach(item => {
if (item.trajectory) {
item.trajectory = item.trajectory.split(",")
}
})
}
})
}
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
queryParams.value.size = val
getDataList(true)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
queryParams.value.current = val
getDataList(false)
}
onMounted(() => {
getDataList(true)
})
const handleDelete = (row: object) => {
ElMessageBox.confirm(
'请确认是否删除该组件?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
await delById(row["id"])
ElMessage({
type: 'success',
message: '删除成功!',
})
getDataList(false)
} catch (e) {
console.log(e)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
const title = ref('新增组件')
const dialogFormVisible = ref(false)
const formLabelWidth = '140px'
interface DataForm {
id: string,
name: string,
typeId: Number,
sort: Number,
imgUrl: string,
trajectory: string
}
const dataFormRef = ref<FormInstance>()
const dataForm = reactive<DataForm>({
id: '',
name: '',
typeId: 0,
sort: 0,
imgUrl: '',
trajectory: '',
})
const rules = reactive<FormRules<DataForm>>({
name: [
{ required: true, message: '请输入组件名', trigger: 'blur' },
],
typeId: [
{ required: true, message: '请选择组件类型', trigger: 'blur' },
],
sort: [
{ required: true, message: '请输入排序', trigger: 'blur' },
],
})
const handleAdd = () => {
title.value = '新增组件'
dialogFormVisible.value = true
dataForm.id = ''
dataForm.name = ''
}
const handleEdit = (row: object) => {
title.value = '修改组件'
getById(row["id"]).then(res => {
if (res.code != 200) {
return
}
dataForm.id = res.data.id
dataForm.name = res.data.name
dataForm.sort = res.data.sort
dataForm.imgUrl = res.data.imgUrl
dataForm.trajectory = res.data.trajectory
dialogFormVisible.value = true
})
}
const submitDataForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (trajectory && trajectory.length > 0) {
dataForm.trajectory = JSON.stringify(trajectory)
if (dataForm.id && dataForm.id != '') {
update(dataForm).then(res => {
if (res.code != 200) {
ElMessage.error("请求错误")
return
}
ElMessage.success("修改成功")
closeDialog(false, formEl)
})
} else {
save(dataForm).then(res => {
if (res.code != 200) {
ElMessage.error("请求错误")
return
}
ElMessage.success("新增成功")
closeDialog(true, formEl)
})
}
} else {
ElMessage.warning("组件轨迹请上传!")
}
} else {
console.log('error submit!', fields)
}
})
}
const closeDialog = (reset: boolean, formEl: FormInstance | undefined) => {
context.beginPath()
context.clearRect(0, 0, canvas.value.width, canvas.value.height); // 清空整个画布
dialogFormVisible.value = false
resetForm(formEl)
getDataList(reset)
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
dataForm.imgUrl = ''
trajectory = []
dataForm.trajectory = ''
}
const canvas = ref();
// 是否允许进行画线
let printFlag = ref<boolean>(false)
// 当前画布对象
let canvasLineDom: any = null
// 当前画布容器对象
let context: any = null
// 是否允许在canvas上进行描线
let allowPrintLine = ref<boolean>(false)
//因为script 的速度 比标签要快 所以要监听这个canvas1 不然获取不到
watch(canvas, (newValue, oldValue) => {
//这里必须使用.value 这个是ref的规则 也就是这个导致的 not a function
context = canvas.value.getContext("2d");
canvasLineDom = canvas.value;
});
/**
* 获取鼠标在canvas上的具体坐标
* @param canvas canvas对象
* @param x 原x点
* @param y 原y点
*/
const windowToCanvas = (canvas: any, x: number, y: number) => {
let rect = canvas.getBoundingClientRect()
return {
x: x - rect.left * (canvas.width / rect.width),
y: y - rect.top * (canvas.height / rect.height)
}
}
let trajectory:Array<number[]> = []
let moveTrajectory:number[] = []
/**
* 鼠标按下
* @param e 事件
*/
function handlerMousedown(e: any) {
allowPrintLine.value = true
if (canvasValid()) {
let ele = windowToCanvas(canvasLineDom, e.clientX, e.clientY)
let { x, y } = ele
moveTrajectory.push(Math.floor(x))
moveTrajectory.push(Math.floor(y))
context.moveTo(x, y)
canvasLineDom.onmousemove = handlerMouseMove
}
}
/**
* 鼠标移动
* @param e 事件
*/
const handlerMouseMove = (e: any) => {
if (canvasValid() && allowPrintLine.value) {
let ele = windowToCanvas(canvasLineDom, e.clientX, e.clientY)
let { x, y } = ele
moveTrajectory.push(Math.floor(x))
moveTrajectory.push(Math.floor(y))
context.lineTo(x, y)
context.lineWidth = '5'
context.strokeStyle = 'rgba(255,0,0,0.87)'
context.stroke()
}
}
/**
* 鼠标松开
* @param e 事件
*/
function handlerMoseUp(e: Event) {
trajectory.push(moveTrajectory)
moveTrajectory = []
console.log(trajectory, 'trajectory')
if (canvasValid()) {
allowPrintLine.value = false
}
}/**
* 是否支持canvas
*/
function canvasValid(): boolean {
return !canvasLineDom || !canvasLineDom.getContext ? false : true
}
const clearAndRedraw = () => {
if (canvas.value instanceof HTMLCanvasElement) {
if (context) {
context.beginPath()
context.clearRect(0, 0, canvas.value.width, canvas.value.height); // 清除整个 canvas
// 或者使用以下代码填充整个 canvas 以达到清除效果
trajectory = []
// context.fillStyle = "white";
// context.fillRect(0, 0, canvas.value.width, canvas.value.height);
// 保存当前状态
// context.save();
// 清除整个 canvas
// context.clearRect(0, 0, canvas.value.width, canvas.value.height);
// 恢复之前保存的状态
// context.restore();
/*nextTick(() => {
context.beginPath()
context.clearRect(0, 0, canvas.value.width, canvas.value.height)
})*/
}
}
};
import MyUploadFile from "@/components/MyUploadFile.vue";
import ComponentCanvas from "@/views/component/ComponentCanvas.vue";
const getImgUrl = (url: string) => {
dataForm.imgUrl = url
}
</script>
<style scoped>
.el-button--text {
margin-right: 15px;
}
.el-select {
width: 300px;
}
.el-input {
width: 300px;
}
.dialog-footer button:first-child {
margin-right: 10px;
}
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100% - 30px);
max-width:calc(100% - 30px);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
.none-up /deep/ .el-upload--picture-card {
display: none;
}
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100%);
max-width:calc(100%);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
</style>
(2)列表回显数据使用了子组件的方式将index作为canvas的动态ID属性,先拿到轨迹绘制点的数据,定义xMin(x轴最小值=Number.MAX_VALUE,如果数据端循环时判断x坐标是否大于xMax大于则将x坐标存入xMax)、xMax(x轴最大值=-Number.MAX_VALUE,如果数据端循环时判断x坐标是否大于xMax大于则将x坐标存入xMax)、(yMin、yMax)与x轴相同的方式取值;此时 宽度(width = xMax-xMin),高度(height = yMax-yMin)得出轨迹点的宽高,props.canvasWidth(组件定义的宽度), props.canvasHeight(组件定义的高度),调用drawCanvas中的toCanvas函数即可得到绘制数据
<template>
<div>
<canvas :id="'canvas'+props.index" :width="props.canvasWidth" :height="props.canvasHeight" :style="cvsStyle"></canvas>
</div>
</template>
<script lang="ts" setup>
import {defineProps, nextTick, onMounted} from "vue";
import {toCanvas} from "@/utils/drawCanvas"
const props = defineProps({
index: {
type: Number,
required: true,
},
trajectoryList: {
type: Array,
required: true,
},
canvasWidth: {
type: Number,
default: 300,
},
canvasHeight: {
type: Number,
default: 300,
}
})
// const { trajectoryList } = toRefs(props);
let cvsStyle = {
width: props.canvasWidth,
height: props.canvasHeight,
border: '1px solid #78edaf'
}
let trajectoryArray = props.trajectoryList
onMounted(() => {
let xMax = -Number.MAX_VALUE, xMin = Number.MAX_VALUE, yMax = -Number.MAX_VALUE, yMin = Number.MAX_VALUE
props.trajectoryList.forEach((trajectory, i) => {
if (i % 2 === 0) {
if (xMax < parseInt(trajectory)) {
xMax = parseInt(trajectory)
}
if (xMin > parseInt(trajectory)) {
xMin = parseInt(trajectory)
}
} else {
if (yMax < parseInt(trajectory)) {
yMax = parseInt(trajectory)
}
if (yMin > parseInt(trajectory)) {
yMin = parseInt(trajectory)
}
}
})
let width = xMax - xMin, height = yMax - yMin
nextTick(() => {
const canvas = document.querySelector(`#canvas${props.index}`);
if (canvas instanceof HTMLCanvasElement) {
let ctx = canvas.getContext('2d');
// 设置Canvas尺寸
canvas.width = props.canvasWidth;
canvas.height = props.canvasHeight;
toCanvas(ctx, trajectoryArray, props.canvasWidth, props.canvasHeight, width, height, xMin, yMin)
}
})
})
</script>
<style scoped>
</style>
(3)这组件新增后在列表回显使用子组件并且封装的ts工具类(这其中就有门道了,一个是缩放(当宽度/高度大于画布的宽高时需要使用到:scale = Math.min(scaleX,scaleY)),一个是偏移量(当宽度/高度小于画布宽高时使用到:offsetX,offsetY)用来居中,咱可以将一个canvas画布的起始位置按{x:0,y:0}为初始值,然后轨迹呢是x存储一位,y存储一位,循环轨迹列表按照下标偶数为x,奇数为y,计算方式为:(arr[i] - xMin) * scale + offsetX,其中arr[i]-xMin是将该轨迹点的位置按照0为起始位置计算)
export const toCanvas = (ctx: any, arr: any, canvasWidth: number, canvasHeight: number, width: number, height: number, xMin: number, yMin: number) => {
let scaleX = 1;
// 计算偏移量
let offsetX = 0;
let offsetY = 0;
if (width > canvasWidth) {
scaleX = canvasWidth / width
} else {
offsetX = (canvasWidth - width) / 2
if (offsetX < 0) {
offsetX = 0
}
}
let scaleY = 1
if (height > canvasHeight) {
scaleY = canvasHeight / height
} else {
offsetY = (canvasHeight - height) / 2
if (offsetY < 0) {
offsetY = 0
}
}
let scale = Math.min(scaleY, scaleX)
interface Dot {
x: number;
y: number;
}
let dots: Dot[] = [];
for (let i = 0; i < arr.length; i++) {
let dot = {
x: 0,
y: 0
}
dot.x = (arr[i] - xMin) * scale + offsetX
i++
if (i <= arr.length - 1) {
dot.y = (arr[i] - yMin) * scale + offsetY
dots.push(dot)
}
}
setTimeout(() => {
initCanvas(ctx, canvasWidth, canvasHeight, dots)
})
}
const initCanvas = (ctx: any, canvasWidth: any, canvasHeight: any, dots: any) => {
ctx.beginPath()
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
for (let i = 0; i < dots.length; i++) {
// 开始一条路径
ctx.beginPath();
// 设置开始位置
ctx.moveTo(dots[i].x, dots[i].y);
// 设置线条的宽度
ctx.lineWidth = 1
// 设置结束位置
if (dots[i+1]) {
ctx.lineTo(dots[i+1].x, dots[i+1].y)
}
// 设置颜色
ctx.strokeStyle = '#666666';
// 开始绘制
ctx.stroke()
}
// 保存到画布上
ctx.fill()
}
三、总结
综上所述就是一个轨迹绘制的组件解决方案。其中还是有一些小BUG,待我回家再做优化(表格列表中删除某个轨迹组件后,下面的组件轨迹会被覆盖,其实也已经解决,在我下一篇组件顺序修改的页面中已经处理好,当时是轨迹顺序变化后,补上来的的轨迹被覆盖了,原因当然是因为canvas定义了动态ref绑定了index下标导致。)