可持续发展项目(十一):轨迹组件开发

文章目录


前言

为自己搭建一个可以自我思考的平台,其核心为“心想事成”。


一、思考过程

画轨迹的功能实现,想着应该是挺难的一个功能点,在实际开发过程中发现,鼠标在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下标导致。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值