使用vite搭建vue3

这篇博客详细介绍了如何使用Vite搭建Vue3项目,涵盖了从安装、render函数和JSX的使用,到路由配置、状态管理(Vuex和Pinia)、事件总线mitt、axios封装、Element-Plus集成、Echarts封装、百度地图异步加载、自定义指令、Less样式穿透、适配方案、日期处理库dayjs、Cesium三维地图、全局组件和自定义Hook防抖等全面内容。

前言

①、node版本建议14.8 以上版本。
②、使用 yarn 来安装。

一、安装

npm init vite@latest

在这里插入图片描述

进入目录后,yarn 一下,然后 npm run dev 即可查看工程页面。

二、render 函数和JSX

1. render

①、在 .vue文件里,直接书写,与setup同级:

import {h} from "vue";
export default {
  setup() {
	
  }
  // 第一个参数是 标签名字
  // 第二个参数是 属性或prop,不写的话,使用null占位
  // 第三个参数是 children,内容或者子节点
  render: () => h(
    'div', {
      id: 'map',
      style: {
        width: '100%',
        height: '100%'
      }
    }
  )
}

②、在setup的返回中书写

import {h} from "vue";
export default {
  setup() {
  
	return () => h(
        'div', {
          id: 'map',
          style: {
            width: '100%',
            height: '100%'
          }
        }
    )
  }

}

注意:目前不支持setup语法糖中使用(可能是我没找到)

2. JSX

①、vite搭建的工程默认不支持 jsx,需要安装插件

yarn add @vitejs/plugin-vue-jsx -D

②、验证jsx成功,在src下新建App.jsx,内容:

import { defineComponent } from "vue";

export default defineComponent({
    setup() {
        return () => {
            return <div>hello Vue3 Jsx</div>
        }
    }
})

③、在vite.config.js中使用

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pluginJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), pluginJsx()]
})

修改main.js文件的引入,再启动显示正常。

三、使用路由

①、安装:

yarn add vue-router@4

②、使用:在src目录下新建router文件夹,里面新建 index.js

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            name: 'home',
            component: () => import('../pages/home.vue')
        },
        {
            path: '/about',
            component: () => import('../pages/about.vue')
        }
    ]
})

export default router;

③、在src下新建 pages目录,新建 home.vue 和 about.vue
④、在 main.js 中引用

import { createApp } from 'vue'
import router from "./router/index.js";
import './style.css'
import App from './App.jsx'

const app = createApp(App)

app.use(router).mount('#app')

⑤、修改App.jsx文件

import {defineComponent} from "vue";

export default defineComponent({
    setup() {
        return () => <>
            <div id="nav">
                <router-link to="/">Home</router-link> |
                <router-link to="/about">About</router-link>
            </div>
            <router-view />
        </>
    }
})

四、状态管理:vuex(可选)

①、安装

yarn add vuex@next --save

②、在src下新建store目录,新建index.js文件:

import { createStore } from 'vuex'

const store = createStore({
    state() {
        return {
            num: 0
        }
    },
    mutations: {
        add(state, payload = 1) {
            state.num += payload;
        }
    }
})

export default store;

③、在main.js中引用

import store from './store'

app.use(store)

④、使用

<template>

  <div>{{ num }}</div>
  <button @click="add">add</button>

</template>

<script>
import {useStore} from 'vuex'
import {computed} from "vue";

export default {
  name: "home",
  setup() {
    const store = useStore();

    return {
      num: computed(() => store.state.num),

      add: () => store.commit('add')
    }
  }
}
</script>

五、状态管理:Pinia(可选)

①、安装

// 先停止项目运行(解决报错)
yarn add pinia

②、main.js中引用

import { createPinia } from 'pinia'
app.use(createPinia())

③、在src下新建 stores目录,新建user.js

import { defineStore, acceptHMRUpdate } from "pinia";
import request from "../api/interface.js";

export const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: 'WuDi',
        isLoading: false
    }),
    actions: {
        /**
         * 登陆
         * @param user
         * @param password
         * @returns {Promise<void>}
         */
        async login(user, password) {
            const userData = await request.login(user, password)

            this.$patch({
                name: user,
                ...userData,
            })

        },
        /**
         * 登出
         */
        logout() {
            this.$patch({
                name: '',
                isLoading: false,
            })
        }
    }
})
if (import.meta.hot) {
    import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}

④、使用

import {useUserStore} from "../stores/user.js";

  setup() {
    const user = useUserStore();
	console.log(user);	// 可以通过user获取state/getter/actions
  }

六、事件总线:mitt

vue3官方不推荐使用,推荐使用状态管理。针对喜欢事件总线的,推荐mitt
①、安装

yarn add mitt

②、在main.js中挂在到全局

import mitt from "mitt";
app.config.globalProperties.$EventBus = mitt();

③、使用


// A 组件 -监听
import { getCurrentInstance } from "vue";
const {proxy: { $EventBus }} = getCurrentInstance();
$EventBus.on('foo', e => console.log('foo', e) )

// B 组件 -抛出
import { getCurrentInstance } from "vue";
const {proxy: { $EventBus }} = getCurrentInstance();
$EventBus.emit('foo', { a: 'b' })

七、封装axios

①、安装

yarn add axios

// 用到qs库来处理参数
yarn add qs

②、在src下新建 utils目录,新建 request.js

import axios from "axios";
import qs from 'qs'

class XHR {


    constructor(props) {
        this.config = {
            baseURL: '',    // 服务器地址
            headers: {},
            crossDomain: true
        }
        this.config = Object.assign(this.config, props);
        this.$request = axios.create(this.config);

        // 请求拦截器,
        this.$request.interceptors.request.use(
            config => {
                config.withCredentials = false;
                config.headers.Authorization = sessionStorage.getItem('wyToken');
                return config;
            },
            error => {
                return Promise.reject(error);
            });
    }

    // 处理get方法
    get(url, data) {
        return this.request(url, data, 'GET');
    }
    // 处理post方法
    post(url, data) {
        return this.request(url, data, 'POST')
    }

    request(url, data, method = "GET") {
        if(typeof data === 'string') {
            method = data
            data = {}
        }
        const conf = {
            url,
            method
        }

        if(method === 'GET') {
            conf.params = data;
            conf.data = qs.stringify(data)
        }else {
            conf.data = data;
        }

        return this.$request(conf).then(({data}) => {
            return {
                code: parseInt(data.code || 0),
                count: parseInt(data.count || 0),
                data: data.data,
                msg: data.message || data.msg
            }
        }).catch((err) => {
            let error = err && err.toString();
            console.error('接口调用异常', error)
            if(err && (err.includes('Error:') || err.includes('<html'))) {
                error = '接口异常,请联系管理员';
            }
            return Promise.reject(error);
        })
    }
}

export default new XHR();

③、在src下新建api目录,新建 interface.js

import request from '../utils/request.js'

export default {

    /**
     * 登陆
     * @param params
     */
    login: params => request.get('/Login', params),


}

八、使用element-Plus-解决Elmessage和Elloading不生效

①、安装

yarn add element-plus

// 自动按需引入
npm install -D unplugin-vue-components unplugin-auto-import

②、在配置文件 vite.config.js 中引入

import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        pluginJsx(),
        AutoImport({
            resolvers: [ElementPlusResolver()],
        }),
        Components({
            resolvers: [ElementPlusResolver()],
        })
    ]
})

③、解决Elmessage和Elloading的css样式文件, 在main.js中

import 'element-plus/theme-chalk/el-loading.css';
import 'element-plus/theme-chalk/el-message.css';

九、封装 echarts-解决自缩放和路由切换刷新

①、安装 - 版本要求(定制、水球)

// 建议使用4.9.0版本
npm i echarts@^4.9.0

// 定制
npm install echarts-gl@^1.1.2 
// 水球
npm install echarts-liquidfill@^2.0.6 zrender@^4.3.1

②、在utils目录下新建 myChart.js

import * as echarts from 'echarts'
import 'echarts-gl'
import 'echarts-liquidfill'

export default {
    line1: (id, callBack) => {
        const myChart = document.getElementById(id)
        myChart.removeAttribute('_echarts_instance_')
        const chart = echarts.init(myChart);
        chart.setOption({
            xAxis: {
                type: 'category',
                data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
            },
            yAxis: {
                type: 'value'
            },
            series: [
                {
                    data: [150, 230, 224, 218, 135, 147, 260],
                    type: 'line'
                }
            ]
        })
        window.addEventListener('resize', () => {
            chart.resize();
        })

        chart.off('click');
        chart.on('click', param => {
            callBack && callBack(param);
        })
    }
}

③、使用

<template>
  <div id="testLine" style="width: 50%; height: 300px;"></div>
</template>

<script>
import { onMounted } from "vue";
import myChart from "../utils/myChart.js";
export default {
  name: "about",
  setup() {
    onMounted(() => {
      myChart.line1('testLine', (e) => {
        console.log('点我了', e)
      });
    })
  }
}
</script>

十、封装百度地图 - 异步加载

①、在utils目录下 新建loadBmap.js

/**
 * @description 加载百度地图基础组件js
 */
export function asyncLoadBaiduJs () {
    return new Promise((resolve, reject) => {
        window.onBMapCallback = function () {
            resolve(BMapGL)
        }
        let script = document.createElement('script')
        script.type = 'text/javascript'
        // script.src = "https://api.map.baidu.com/api?v=3.0&ak=秘钥&callback=onBMapCallback"	// 3.0版本
        script.src = "https://api.map.baidu.com/api?v=1.0&type=webgl&ak=秘钥=onBMapCallback"
        script.onerror = reject
        document.head.appendChild(script)
    })
}

②、使用

<template>
  <div style="width:800px;height:400px;" id="map"></div>
</template>

<script>
import {asyncLoadBaiduJs} from "../../utils/loadBmap.js";
import mapStyle from "../../utils/mapStyle.js";
import otherimg from './assets/mk-other.png'
import {h} from "vue";
export default {
  name: "minMap",
  setup() {
    let mineMap = null;

    // 初始化地图
    const initMap = async () => {
      try {
        await asyncLoadBaiduJs(); // 加载百度地图
        mineMap = new BMapGL.Map("map");  // 初始化地图
        mineMap.enableScrollWheelZoom(true);  // 启用滚轮缩放
        setMapStyle();
        setMapCenter(118.758781, 31.979259, 16)
      } catch (error) {
        console.log(error);
      }
    }
    // 设置地图中心及缩放等级
    const setMapCenter = (lng, lat, zoom) => {
      mineMap.centerAndZoom(new BMapGL.Point(lng, lat), zoom);
    }
    // 重置地图
    const resetMap = (val) => {
      mineMap.clearOverlays();  // 清除图层
      // 路况、动画、轨迹视情况而定
    }
    // 个性化地图
    const setMapStyle = () => {
      mineMap.setMapStyleV2({
        styleJson: mapStyle
      });
    }
    // 绘制边界
    const setBoundaries = () => {

    }
    // 添加文本标注
    const addLabel = (name) => {

    }

    // 清除文本标注
    const clearLabel = () => {

    }

    /**
     * 添加点位标记
     * @param item:含坐标和label名
     * @param callback
     */
    const addMaker = (item, callback) => {
      let markerIcon = new BMapGL.Icon(otherimg, new BMapGL.Size(30, 39), {
        offset: new BMapGL.Size(13, 16) // 图标中央下端的尖角位置
      });
      // 设置标记坐标 (经, 纬)
      let markerPoint = new BMapGL.Point(item.lng * 1, item.lat * 1);
      // 生成地图标记
      let marker = new BMapGL.Marker(markerPoint, { icon: markerIcon });
      /* 为标记对象添加属性 start*/
      marker.type = item.type;
      marker.id = item.id;
      marker.name = item.name;
      /* end */

      // 添加label
      let markerLabel = new BMapGL.Label(item.shopName, {
        offset: new BMapGL.Size(0, -38)
      });
      markerLabel.setStyle({
        color: "#fff",
        backgroundColor: "0.05",
        border: "0",
        transform: "translateX(-50%)"
      });

      marker.setLabel(markerLabel);

      /* 为标记对象绑定点击事件 start*/
      marker.addEventListener("click", e => {
        console.log('mapClick',e)
        callback && callback(e);
        e.domEvent.stopPropagation();
      });

      mineMap.addOverlay(marker);
      setMapCenter(item.lng * 1, item.lat * 1);

    }

    // 清除点位标记
    const clearMaker = (marker) => {

    }


    return {
      initMap,
      resetMap,
      addLabel,
      clearLabel,
      addMaker,
      clearMaker
    }
  }
}
</script>

③、个性化地图:在utils目录下新建mapStyle.js

export default [
  {
    featureType: "land",
    elementType: "geometry",
    stylers: {
      visibility: "on",
      color: "#091220ff"
      // color: "#062d2e",
    }
  },
  // ...略
]

在使用地方

import mapStyle from "../utils/mapStyle.js";

this.mineMap.setMapStyleV2({ styleJson: mapStyle });

④、其他组件待完善

十一、封装自定义指令:v-drag

举个拖拽的例子:
①、在utils目录下新建 drag.js

// 全局指令:拖拽功能
// 使用:v-drag

const drag = {}
drag.install = function (Vue) {
    Vue.directive('drag', {
        mounted: function (el) {
            el.onmousedown = function (e) {
                const disx = e.pageX - el.offsetLeft;
                const disy = e.pageY - el.offsetTop;
                document.onmousemove = function (e) {
                    el.style.left = e.pageX - disx + 'px';
                    el.style.top = e.pageY - disy + 'px';
                }
                document.onmouseup = function () {
                    document.onmousemove = document.onmouseup = null;
                }
            }
        },
    })
}

export default drag;

②、在main.js中引入

import drag from "./utils/drag.js";
app.use(drag)

③、使用

  <div v-drag style="border: 1px solid red;width: 200px;height: 200px;position: fixed;top: 150px; right: 585px; z-index: 90;">
    <div>关于我</div>
  </div>

十二、Less和样式穿透

①、安装

yarn add less less-loader

②、在vite.config.js中

	// 配置根目录绝对路径
    resolve: {
        alias: {
            "@": '/src/'
        }
    },
	// 跟plugins同级
	css: {
        preprocessorOptions: {
            less: {
                javascriptEnabled: true,
            },
        },
    },

③、使用

<style lang="less" scoped>
// 1、引入其他css文件,需要@import
// 2、根路径下 @ 会报错,需要在vite.config.js配置,见上一步resolve
@import "@/css/_function_.less";

.demo {
  // 默认vite里 @ 会报错
  background-image: url('@/assets/popBg/popBg.png');
}

// 样式穿透 由原来的/deep/ 改成  :deep(.el类名)
:deep(.el-table) {
  --el-table-tr-bg-color: transparent;
}

</style>

十三、适配方案:pxtoViewPort 、autoprefixer、amfe-flexible

①、安装

yarn add amfe-flexible autoprefixer postcss-px-to-viewport 

②、在main.js中

import 'amfe-flexible';

③、在 vite.config.js 中

import autoprefixer from 'autoprefixer';
import pxtoViewPort from 'postcss-px-to-viewport'

css: {
        // less 配置略
        
        postcss: {
            plugins: [
                autoprefixer,
                pxtoViewPort({
                    unitToConvert: 'px', // 要转化的单位
                    viewportWidth: 1920, // UI设计稿的宽度
                    unitPrecision: 6, // 转换后的精度,即小数点位数
                    propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
                    viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
                    fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
                    selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
                    minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
                    mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
                    replace: true, // 是否转换后直接更换属性值
                    landscape: false // 是否处理横屏情况
                })

            ]
        }
    },

十四、dayjs使用


// 方式一:element-plus处引入
import { dayjs } from "element-plus";

// 方式二: ES6 引入使用

// install
yarn add dayjs

import dayjs from 'dayjs/esm/index.js'
const times = dayjs().format()
console.log(times)
// app.config.globalProperties.$Dayjs = dayjs;

十五、Cesium三维地图

①、安装

yarn add cesium vite-plugin-cesium vite -D

②、在vite.config.js

import cesium from 'vite-plugin-cesium'

plugins: [
  vue(),
  cesium()
],

③、新建页面CesiumMap.vue

<template>
  <div id="cesiumContainer" style="width: 100%; height: 100%;"></div>
</template>

<script setup name="CesiumMap">
import { onMounted } from "vue";
import { Viewer, Cartesian3, Math } from "cesium";

onMounted(() => {

  const viewer = new Viewer("cesiumContainer", {
    animation: true, // 是否开启动画
    timeline: false, // 是否显示时间轴
    infoBox: false
  });
  viewer._cesiumWidget._creditContainer.style.display = "none";

  viewer.camera.flyTo({
    destination: Cartesian3.fromDegrees(120, 33, 408),
    orientation: { // 镜头角度
      heading: Math.toRadians(30),  // 代表镜头左右方向,正值为右,负值为左,360度和0度是一样的
      pitch: Math.toRadians(-20),  // 代表镜头上下方向,正值为上,负值为下
      roll: 0,                            // 代表镜头左右倾斜.正值,向右倾斜,负值向左倾斜
    },
  });
})

const cesiumInit = () => {
  const viewer = new Viewer("cesiumContainer", {
    animation: true, // 是否开启动画
    timeline: false, // 是否显示时间轴
    infoBox: false
  });
  viewer._cesiumWidget._creditContainer.style.display = "none";

  viewer.camera.flyTo({
    destination: Cartesian3.fromDegrees(120, 33, 408),
    orientation: { // 镜头角度
      heading: Math.toRadians(30),  // 代表镜头左右方向,正值为右,负值为左,360度和0度是一样的
      pitch: Math.toRadians(-20),  // 代表镜头上下方向,正值为上,负值为下
      roll: 0,                            // 代表镜头左右倾斜.正值,向右倾斜,负值向左倾斜
    },
  });
}

defineExpose({
  cesiumInit
})
</script>

④、使用:在App.vue中使用(根据自身情况)

<CesiumMap></CesiumMap>

import CesiumMap from "./components/CesiumMap/CesiumMap.vue";

十六、全局组件 - 弹窗

在 components 下新建 PopTop.vue

<template>
  <div class="PopTop" :class="{'BottomBorder': BottomBorder}">
    <div class="name">{{ name }}</div>
    <div class="icon" @click="$emit('close')"></div>
  </div>
</template>

<script setup name="PopTop">
defineProps({
  name: {
    type: String,
    required: true
  },
  BottomBorder: {
    type: Boolean,
    default: false
  }
})

</script>

<style lang="less" scoped>

@-webkit-keyframes spin {

  0% {
    -webkit-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  70% {
    -webkit-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }

  100% {
    -webkit-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    transform: rotate(0deg);
  }
}

.PopTop {
  position: relative;
  width: 100%;
  height: 50px;
  line-height: 50px;
  text-align: center;

  .name {
    color: #ffffff;
    font-size: 20px;
  }

  .icon {
    position: absolute;
    right: 5px;
    top: 15px;
    width: 20px;
    height: 20px;
    background-image: url('./assets/close-img.png');
    background-repeat: no-repeat;
    background-size: 100% 100%;
    background-position: center;
    cursor: pointer;
    &:hover {
      -webkit-animation: spin 0.5s;
    }
  }
}
.BottomBorder {
  border-bottom: 1px solid #ffffff;
}

</style>

main.js 中 注册

import PopTop from "./components/PopTop/PopTop.vue";
app.component('PopTop', PopTop);

使用

<pop-top :name="'直接使用'" BottomBorder @close="close"></pop-top>

const close = () => {
	// v-if 操作
}

十七、视频播放

安装

yarn add video.js@7.20.2 videojs-player/vue@1.0.0

在main.js中

import VueVideoPlayer from '@videojs-player/vue'
import 'video.js/dist/video-js.css'

app.use(VueVideoPlayer).mount('#app')

使用

<template>
  <div>
    <div class="VideoComp">
      <div class="my-video" v-for="(item, index) in list" >
        <VideoPlayer
            :src="item.monitorUrl"
            :playsinline="true"
            :options="initVideoPlayer(item.monitorUrl)"
            controls />
      </div>

    </div>
    <div class="mine-pagination">
      <el-pagination
          small
          layout="prev, pager, next"
          :total="total"
          :page-size="pageSize"
          :current-page="pageNum"
          @current-change="currentChange"
      />
    </div>
  </div>
</template>

<script setup name="VideoComp">
import { VideoPlayer } from '@videojs-player/vue'
import {getCurrentInstance, ref} from "vue";

const list = ref([])
const total = ref(0)
const pageSize = ref(6)
const pageNum = ref(1)
const {proxy: { $http }} = getCurrentInstance()

const getData = () => {
  $http.monitorList({pageNo: pageNum.value, pageSize: pageSize.value}).then(({code, data, count}) => {
    if (code == 0) {
      list.value = data
      total.value = count
    }
  })
}

getData();

const currentChange = (e) => {
  pageNum.value = e
  getData()
}

const initVideoPlayer = (url) => {
  return {
    autoplay: true,
    muted: true,
    loop: false,
    preload: "auto",
    language: "zh-CN",
    aspectRatio: "400:240",
    fluid: true,
    sources: [
      {
        type: "application/x-mpegURL",
        src: url // 视频url地址
      }
    ],
    notSupportedMessage: "此视频暂无法播放,请稍后再试",
    controlBar: {
      timeDivider: false,
      durationDisplay: false,
      remainingTimeDisplay: false,
      fullscreenToggle: true
    }
  }
}

</script>

<style lang="less" scoped>

.VideoComp {
  position: fixed;
  z-index: 99;
  width: 70%;
  height: 750px;
  left: 800px;
  // right: 1%;
  top: 120px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  overflow: auto;

  .my-video {
    width: 400px;
    height: 300px;
    background-color: #048cba;
    margin-bottom: 20px;
    /deep/ .video-js {
      width: 100%;
      height: 100%;
      display: block;
      .vjs-big-play-button {
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
      video {
        width: 100%;
        height: 100%;
        display: block;
      }
    }
  }
}

.mine-pagination {
  position: fixed;
  left: 800px;
  top: 900px;
  z-index: 99;
}
</style>

十八、自定义Hook - 防抖

在根目录下新建 hook文件夹,新建 index.js

import useDebounceRef from "./useDebounceRef.js";

export {
    useDebounceRef
    // 其他的hook都这样
}

新建 useDebounceRef.js

import {customRef} from "vue";

export default function (value) {
    let timer = null;
    return customRef((track, trigger) => {
        return {
            get() {
                track();
                return value;
            },
            set(newValue) {
                clearTimeout(timer);
                timer = setTimeout(() => {
                    value = newValue;
                    trigger();
                }, 1000)
            }
        }
    })
}

使用

<script setup>
import {
  useDebounceRef
} from '@/hook'


评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值