可持续发展项目(十二):组件拖拽排序开发


前言

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


一、思考过程

此篇文章是将排序修改的接口以及页面开发!

二、完善

1、后端代码

(1)controller层

package com.etp.sustainable.controller.component;

import com.etp.sustainable.domain.req.SortQueryReq;
import com.etp.sustainable.domain.req.SortReq;
import com.etp.sustainable.service.SortService;
import com.etp.sustainable.util.R;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author ETP
 * @since 2023/10/25 15:46
 */
@Tag(name="组件排序管理")
@RestController
@AllArgsConstructor
@RequestMapping("/sort")
public class SortController {

    private final SortService sortService;

    @GetMapping("/list")
    public R list(SortQueryReq req) {
        return R.ok(sortService.componentWithTypeList(req));
    }

    @PutMapping("/updateSort")
    public R updateSort(@RequestBody List<SortReq> req) {
        return R.ok(sortService.updateComponent(req));
    }

}

(2)service层

package com.etp.sustainable.service;

import com.etp.sustainable.domain.req.SortQueryReq;
import com.etp.sustainable.domain.req.SortReq;
import com.etp.sustainable.domain.resp.SortResp;

import java.util.List;

/**
 * @author ETP
 * @since 2023/10/25 17:01
 */
public interface SortService {

    /**
     * 组件与组件类型集合
     * @param req
     * @return
     */
    List<SortResp> componentWithTypeList(SortQueryReq req);

    /**
     * 组件与组件类型集合
     * @param req
     * @return
     */
    boolean updateComponent(List<SortReq> req);

}

(3)serviceImpl层

package com.etp.sustainable.service.impl;

import cn.hutool.core.collection.CollUtil;
import com.etp.sustainable.domain.component.Component;
import com.etp.sustainable.domain.component.ComponentType;
import com.etp.sustainable.domain.req.SortQueryReq;
import com.etp.sustainable.domain.req.SortReq;
import com.etp.sustainable.domain.resp.SortResp;
import com.etp.sustainable.mapper.component.ComponentMapper;
import com.etp.sustainable.mapper.component.ComponentTypeMapper;
import com.etp.sustainable.service.SortService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author ETP
 * @since 2023/10/25 17:03
 */
@Service
@AllArgsConstructor
public class SortServiceImpl implements SortService {

    private final ComponentTypeMapper componentTypeMapper;

    private final ComponentMapper componentMapper;

    @Override
    public List<SortResp> componentWithTypeList(SortQueryReq req) {
        List<SortResp> componentTypeList = componentTypeMapper.getComponentTypeList(req);
        if (CollUtil.isEmpty(componentTypeList)) {
            return new ArrayList<>();
        }
        List<Long> componentTypeIdList = componentTypeList.stream().map(SortResp::getId).distinct().toList();
        req.setComponentTypeIdList(componentTypeIdList);

        List<SortResp> componentList = componentMapper.getComponentList(req);
        Map<Long, List<SortResp>> componentMap = componentList.stream().collect(Collectors.groupingBy(SortResp::getTypeId, LinkedHashMap::new, Collectors.toList()));
        for (SortResp sortResp : componentTypeList) {
            Long key = sortResp.getId();
            List<SortResp> component = componentMap.getOrDefault(key, new ArrayList<>());
            sortResp.setComponentList(component);
        }
        return componentTypeList;
    }

    @Override
    public boolean updateComponent(List<SortReq> req) {
        req.forEach(componentTypeReq -> {
            ComponentType componentType = new ComponentType();
            componentType.setId(componentTypeReq.getId());
            componentType.setSort(componentTypeReq.getSort());
            componentTypeMapper.updateById(componentType);

            if (CollUtil.isNotEmpty(componentTypeReq.getComponentList())) {
                componentTypeReq.getComponentList().forEach(componentReq -> {
                    Component component = new Component();
                    component.setId(componentReq.getId());
                    component.setSort(componentReq.getSort());
                    componentMapper.updateById(component);
                });
            }
        });
        return true;
    }

}

2、上前端代码

(1)上sort页面

<template>
    <el-space
        fill
        wrap
        :fill-ratio="fillRatio"
        :direction="direction"
        style="width: 100%; margin-bottom: 15px;"
    >
        <div>
            <div v-if="startDrag">
                <el-button @click="dgStart">启动拖拽</el-button>
            </div>
            <div v-else>
                <el-button :type="dragTypeFlag && dragComponentFlag ? 'primary' : ''" @click="allStart">自由拖拽</el-button>
                <el-button :type="dragTypeFlag && !dragComponentFlag ? 'primary' : ''" @click="typeStart">类型拖拽</el-button>
                <el-button :type="!dragTypeFlag && dragComponentFlag ? 'primary' : ''" @click="componentStart">组件拖拽</el-button>
            </div>
        </div>
    </el-space>
    <el-space
        fill
        wrap
        :fill-ratio="fillRatio"
        :direction="direction"
        style="width: 100%; margin-bottom: 15px;"
    >
        <div v-if="changeDrag">
            <el-button @click="submit">确认</el-button>
            <el-button @click="cancel">取消</el-button>
        </div>
    </el-space>
    <div class="component-with-type-box">
        <transition-group name="drag" class="row-line">
            <ul
                v-for="(item, index) in componentTypeList"
                class="component-type-box"
                :draggable="dragFlag && dragTypeFlag"
                @dragstart="dragstart(1, index, null)"
                @dragenter="dragenterType($event, index, null)"
                @dragover="dragover($event)"
                @dragend="dragend()"
                @mouseover="topicTypeHover($event)"
                @mouseout="removeTopicTypeHover($event)"
                :key="index"
            >
                <div class="component-type">
                    <div class="component-type-title">
                        {{ item.name }}
                    </div>
                    <span v-if="!startDrag && dragFlag && dragTypeFlag" class="drag-sort">拖拽换序</span>
                </div>
                <li
                    v-for="(component, i) in item.componentList"
                    class="component-type-info"
                    :draggable="dragFlag && dragComponentFlag"
                    @dragstart="dragstart(2, index, i)"
                    @dragenter="dragenter($event, index, i)"
                    @dragover="dragover($event)"
                    @dragend="dragend()"
                    @mouseover="addNumberHover($event)"
                    @mouseout="removeNumberHover($event)"
                    :key="i"
                >
                    <el-image style="width: 100px; height: 100px" :src="component.imgUrl" draggable="false" :fit="'fill'" @click="bigImage(component.imgUrl)" />
                    {{ component ? component.name : "" }}
                </li>
                <div style="clear: both; height: 0"></div>
            </ul>
        </transition-group>
        <el-image-viewer
            style="z-index:1500"
            v-if="showImageViewer"
            @close="closeBigImage"
            :url-list="imageData"/>
    </div>
</template>

<script lang="ts" setup>
import {onMounted, ref, reactive, Ref} from 'vue'
import {cwtList, updateSort} from '@/api/sortApi'
import {canvasToImg} from '@/utils/drawCanvasToImg'
import ComponentCanvas from "@/views/component/ComponentCanvas.vue";
import {ElMessage} from "element-plus";

const fillRatio = ref(30)
const direction = ref('horizontal')
interface QueryParams {
    name: string
}
const queryParams = reactive<QueryParams>({
    name: '',
})
interface ComponentList {
    id: number
    imgUrl: string
    name: string
    sort: number
    trajectory: string
    typeId: number
}
interface ComponentTypeList {
    id: number
    imgUrl: string
    name: string
    sort: number
    trajectory: string
    typeId: number
    componentList: Array<ComponentList>
}
const componentTypeList:Ref<ComponentTypeList[]> = ref([])
let canvasWidth = 100, canvasHeight = 100;
onMounted(() => {
    cwtList(queryParams).then(res => {
        if (res.code != 200) return
        if (res.data && res.data.length > 0) {
            res.data.forEach(item => {
                if (item.componentList && item.componentList.length > 0) {
                    item.componentList.forEach(component => {
                        if (component.trajectory) {
                            component.trajectory = component.trajectory.split(",")
                            let imgUrl = canvasToImg(component.trajectory, canvasWidth, canvasHeight)
                            component.imgUrl = imgUrl
                        }
                    })
                }
            })
        }
        componentTypeList.value = res.data
    })
})
let startDrag = ref(true);
const dgStart = () => {
    startDrag.value = false;
    allStart()
}
let changeDrag = ref(false);
const cancel = () => {
    startDrag.value = true;
    changeDrag.value = false;
    dragTypeFlag.value = false;
    dragComponentFlag.value = false;
}

let dragFlag = ref(false);
let dragTypeFlag = ref(false);
let dragComponentFlag = ref(false);
const allStart = () => {
    dragFlag.value = true
    dragTypeFlag.value = true
    dragComponentFlag.value = true
}
const typeStart = () => {
    dragFlag.value = true
    dragTypeFlag.value = true
    dragComponentFlag.value = false
}
const componentStart = () => {
    dragFlag.value = true
    dragTypeFlag.value = false
    dragComponentFlag.value = true
}
const submit = () => {
    interface ReqParam {
        id: number,
        sort: number,
    }
    interface ReqParams {
        id: number,
        sort: number,
        componentList: Array<ReqParam>
    }
    let reqParams:ReqParams[] = []
    let typeIndex = 1;
    componentTypeList.value.forEach(componentType => {
        let reqParam:ReqParams = {
            id: componentType.id,
            sort: typeIndex,
            componentList: [],
        }
        typeIndex++;
        if (componentType.componentList && componentType.componentList.length > 0) {
            let componentIndex = 1;
            componentType.componentList.forEach(component => {
                let componentParam:ReqParam = {
                    id: component.id,
                    sort: componentIndex,
                }
                componentIndex++;
                reqParam.componentList.push(componentParam)
            })
        }
        reqParams.push(reqParam)
    })
    updateSort(reqParams).then(res => {
        if (res.code != 200) {
            ElMessage.success("请求错误")
            return
        }
        ElMessage.success("修改成功")
        startDrag.value = true;
        dragFlag.value = false
        changeDrag.value = false
        dragTypeFlag.value = false
        dragComponentFlag.value = false
    })
}

const dragType = ref(0)
const typeDefaultIndex = ref(0)
const typeIndex = ref(0)
const dragDefaultIndex = ref(0)
const dragIndex = ref(0)
const updateLi = ref(false)
interface TypeInfo {
    id: number
    imgUrl: string
    name: string
    sort: number
    trajectory: string
    typeId: number
}
const typeInfo = ref({})
const moveInfo = ref({})

const showImageViewer = ref(false)
interface ImageData {
    imgUrl: string;
}
const imageData: Ref<ImageData[]> = ref([])
const bigImage = (imgUrl: any) => {
    imageData.value.push(imgUrl)
    showImageViewer.value = true
}
const closeBigImage = () => {
    showImageViewer.value = false
}

/**
 * 拖拽开始
 * @param type 类型:1-组件类型,2-组件
 * @param index 组件类型下标
 * @param i 组件下标
 */
const dragstart = (type, index, i) => {
    if (dragType.value !== 0) {
        if (dragType.value === 1 && dragFlag.value && dragTypeFlag.value) {
            changeDrag.value = true
            typeDefaultIndex.value = index;
            typeIndex.value = index;
            typeInfo.value = JSON.parse(JSON.stringify(componentTypeList.value[index]));
        } else if (dragType.value === 2 && dragFlag.value && dragComponentFlag.value) {
        } else {
            dragType.value = 0
        }
    } else {
        dragType.value = type
        if (dragType.value === 1 && dragFlag.value && dragTypeFlag.value) {
            changeDrag.value = true
            typeDefaultIndex.value = index;
            typeIndex.value = index;
            typeInfo.value = JSON.parse(JSON.stringify(componentTypeList.value[index]));
        } else if (dragType.value === 2 && dragFlag.value && dragComponentFlag.value) {
            changeDrag.value = true
            updateLi.value = true
            typeDefaultIndex.value = index;
            typeIndex.value = index;
            dragDefaultIndex.value = i;
            dragIndex.value = i;
            componentTypeList.value[index].componentList[i].sort = 0;
            moveInfo.value = componentTypeList.value[index].componentList[i];
            removeNumberHover(event);
        } else {
            dragType.value = 0
        }
    }
}
// 拖拽停留位置 --- 增加拖拽效果
const dragover = (event: any) => {
    if (dragType.value === 1 && dragFlag.value && dragTypeFlag.value) {
        event.preventDefault();
    } else if (dragType.value === 2) {
        event.preventDefault();
    }
}
// 拖拽鼠标释放
const dragenter = (event: any, index: number, i: number) => {
    event.preventDefault();
    if (dragType.value === 1 && dragFlag.value && dragTypeFlag.value) {
        componentTypeList.value.splice(typeIndex.value, 1);
        componentTypeList.value.splice(index, 0, typeInfo.value);
        // 排序变化后目标对象的索引变成源对象的索引
        typeIndex.value = index;
    } else if (dragType.value === 2) {
        // 避免源对象触发自身的dragenter事件
        // 是否跨区域,启示集合下标和移动至集合下标位置不同
        if (typeIndex.value !== index) {
            componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
            componentTypeList.value[index].componentList.splice(i, 0, moveInfo.value);
            // 排序变化后目标对象的索引变成源对象的索引
        } else if (dragIndex.value !== i) {
            componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
            componentTypeList.value[typeIndex.value].componentList.splice(i, 0, moveInfo.value);
            // 排序变化后目标对象的索引变成源对象的索引
        }
        typeIndex.value = index;
        dragIndex.value = i;
    }
}
// 跨类型拖拽
const dragenterType = (event: any, index: number) => {
    event.preventDefault();
    if (dragType.value === 1 && dragFlag.value && dragTypeFlag.value) {
        // 避免源对象触发自身的dragenter事件
        // 是否跨区域,启示集合下标和移动至集合下标位置不同
        componentTypeList.value.splice(typeIndex.value, 1);
        componentTypeList.value.splice(index, 0, typeInfo.value);
        // 排序变化后目标对象的索引变成源对象的索引
        typeIndex.value = index;
    } else if (dragType.value === 2 && dragFlag.value && dragComponentFlag.value) {
        // 避免源对象触发自身的dragenter事件
        // 是否跨区域,启示集合下标和移动至集合下标位置不同
        if (typeIndex.value !== index) {
            componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
            componentTypeList.value[index].componentList.splice(componentTypeList.value[index].componentList.length, 0, moveInfo.value);
            // 排序变化后目标对象的索引变成源对象的索引
            typeIndex.value = index;
            dragIndex.value = componentTypeList.value[index].componentList.length - 1;
        }
    }
}
// 拖拽结束
const dragend = () => {
    if (dragType.value === 1) {
        componentTypeList.value.forEach((item, index) => {
            index++;
            item.sort = index
            /*item.componentList.forEach((component, i) => {
                i++;
                component.sort = i;
            });*/
        });
    } else {
        componentTypeList.value.forEach((item, index) => {
            index++;
            item.sort = index
            /*item.componentList.forEach((component, i) => {
                i++;
                component.sort = i;
            });*/
        });
    }
    dragType.value = 0
}
// 组件鼠标经过时添加样式
const addNumberHover = (event:any) => {
    event.currentTarget.className = "component-type-info-hover";
}
// 组件鼠标经过后移除样式
const removeNumberHover = (event:any) => {
    event.currentTarget.className = "component-type-info";
}
// 组件类型经过时添加样式
const topicTypeHover = (event:any) => {
    event.currentTarget.className = "component-type-box-hover";
}
// 组件类型经过后移除样式
const removeTopicTypeHover = (event:any) => {
    event.currentTarget.className = "component-type-box";
}
</script>

<style lang="scss" scoped>
$ratio: 0.85;
.component-with-type-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px solid #78edaf;

    .component-type-box {
        min-width: 300px;
        list-style: none;
        padding: 10px;
        border: 1px solid #78edaf;

        .component-type {
            height: 40px;
            line-height: 40px;
            margin-bottom: 10px;
            display: flex;
            align-items: center;

            .component-type-title {
                display: flex;

                .item_nameBox {
                    width: 80%;
                    box-sizing: border-box;
                    //padding: 15px * $ratio 0 15px * $ratio;
                    // display: flex;
                    // flex-direction: column;

                    .item_title {
                    }

                }

            }

            .drag-sort {
                display: none;
            }
        }
    }

    .component-type-box-hover {
        min-width: 300px;
        list-style: none;
        padding: 10px;
        border: 1px solid #78edaf;
        background: #F8FAFF;
        cursor: pointer;

        .component-type {
            height: 40px;
            line-height: 40px;
            margin-bottom: 10px;
            display: flex;
            align-items: center;

            .component-type-title {
                width: 85%;
                display: flex;

                .item_nameBox {
                    width: 80%;
                    box-sizing: border-box;
                    //padding: 15px * $ratio 0 15px * $ratio;
                    // display: flex;
                    // flex-direction: column;

                    .item_title {
                    }

                }

                .item_title {
                    display: flex;
                }
                i {
                    color: #78edaf
                }
            }

            .drag-sort {
                display: inline-block;
                width: 60px;
                height: 20px;
                font-size: 14px;
                font-family: PingFangSC-Medium, PingFang SC;
                font-weight: 500;
                color: #989898;
                line-height: 20px;
            }
        }

    }

    .drag-move {
        transition: transform 0.3s;
    }

    .component-type-info {
        cursor: pointer;
        line-height: 27px;
        background: #ffffff;
        border-radius: 6px;
        border: 1px solid #78edaf;
        color: #78edaf;
        float: left;
        text-align: center;
        margin-right: 20px;
        margin-bottom: 20px;
        display: flex;
        flex-direction: column;

        ::v-deep .el-image {
            border-radius: 6px;
        }
    }

    .component-type-info-hover {
        cursor: pointer;
        line-height: 27px;
        border-radius: 6px;
        border: 1px solid #78edaf;
        float: left;
        text-align: center;
        margin-right: 20px;
        margin-bottom: 20px;
        background: #78edaf;
        color: #ffffff;
        display: flex;
        flex-direction: column;

        ::v-deep .el-image {
            border-radius: 6px;
        }
    }
}
</style>

(2)我猜可能看了就想得到,使用到了canvas转图片工具类

export const canvasToImg = (trajectoryArray: Array<string>, canvasWidth, canvasHeight) => {
    let canvas = document.createElement("canvas")
    if (canvas instanceof HTMLCanvasElement) {
        let ctx = canvas.getContext('2d');
        let xMax = -Number.MAX_VALUE, xMin = Number.MAX_VALUE, yMax = -Number.MAX_VALUE, yMin = Number.MAX_VALUE
        trajectoryArray.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
        // 设置Canvas尺寸
        canvas.width = 100;
        canvas.height = 100;
        return getDots(canvas, ctx, trajectoryArray, canvasWidth, canvasHeight, width, height, xMin, yMin)
    }
}
const getDots = (canvas: any, ctx: any, arr: any, canvasWidth: any, canvasHeight: any, width: any, height: any, xMin: any, yMin: any) => {

    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)
        }
    }
    return initCanvas(canvas, ctx, dots)
}
const initCanvas = (canvas: any, ctx: any, dots: any) => {
    ctx.beginPath()
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    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()
    return canvas.toDataURL('image/jpeg', 1.0);
}

(3)上api.js代码

import axios from '../utils/http'

// 分页查询组件页码
export function cwtList(params) {
    return axios('/sort/list', { params }, "get");
}

// 分页查询组件页码
export function updateSort(dataForm) {
    return axios('/sort/updateSort', { dataForm }, "put");
}

三、总结

排序界面也就开发完成啦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue3 的组件拖拽排序可以通过使用 `Sortable.js` 库来实现。以下是一个简单的示例: 1. 安装 `Sortable.js` 库: ``` npm install sortablejs --save ``` 2. 在需要排序组件中导入 `Sortable.js`: ```javascript import Sortable from 'sortablejs'; ``` 3. 在组件的 `mounted` 钩子中初始化排序: ```javascript mounted() { const el = this.$refs.sortable; this.sortable = Sortable.create(el, { onEnd: this.onSortEnd }); }, ``` 4. 创建 `onSortEnd` 方法来处理排序结束后的逻辑: ```javascript methods: { onSortEnd(event) { const itemEl = event.item; const newIndex = event.newIndex; // 处理排序结束后的逻辑 } } ``` 5. 在组件中添加拖拽排序的 HTML: ```html <template> <div ref="sortable"> <div v-for="(item, index) in items" :key="item.id"> {{ item.text }} </div> </div> </template> ``` 在以上代码中,`items` 是一个数组,包含需要排序项目。将 `items` 数组绑定到组件的属性中即可。 完整的代码示例: ```javascript <template> <div ref="sortable"> <div v-for="(item, index) in items" :key="item.id"> {{ item.text }} </div> </div> </template> <script> import Sortable from 'sortablejs'; export default { data() { return { items: [ { id: 1, text: 'Item 1' }, { id: 2, text: 'Item 2' }, { id: 3, text: 'Item 3' }, { id: 4, text: 'Item 4' } ], sortable: null }; }, mounted() { const el = this.$refs.sortable; this.sortable = Sortable.create(el, { onEnd: this.onSortEnd }); }, methods: { onSortEnd(event) { const itemEl = event.item; const newIndex = event.newIndex; // 处理排序结束后的逻辑 } } }; </script> ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值