vue3+ts项目推荐(企业级0到1开发)H5

一、技术方案

  1. 基于 vue3+typescript 中大型项目开发解决方案
  2. 基于 vant 组件库快速构建H5界面解决方案
  3. 基于 vue-router 的前端路由解决方案
  4. 基于 vite 的 create-vue 构建vue3项目解决方案
  5. 基于 pinia 的状态管理解决方案
  6. 基于 pinia-plugin-persistedstate 状态持久化解决方案
  7. 基于 @vuecore/use 的组合式API工具库解决方案
  8. 身份证信息校验解决方案
  9. 基于 postcss-px-to-viewport 移动端适配解决方案
  10. 基于 vite-plugin-svg-icons 的svg图标组件解决方案
  11. 基于 vite-plugin-html 自定义html模板解决方案
  12. 基于 unplugin-vue-components 组件自动注册解决方案
  13. 基于 socket.io 的即时通讯问诊室解决方案
  14. 第三方登录解决方案
  15. 第三方支付解决方案
  16. 第三方地图解决方案
  17. pnpm 包管理方案
  18. css 变量主题定制方案
  19. 自定义 hook 解决方案
  20. axios 二次封装解决方案
  21. services API接口分层解决方案
  22. 基于 vant 的通用组件封装解决方案
  23. mock 本地数据模拟解决方案
  24. 基于 eruda 的移动端调试解决方案
  25. 生产环境配置方案
  26. CI/CD 持续集成自动部署方案 

二、业务涵盖

  1. 医生与文章推荐业务
  2. 快速问诊业务
  3. 问诊费用支付宝支付业务
  4. 问诊室业务
  5. 药品订单支付宝支付业务
  6. 实时物流高德地图业务
  7. QQ登录业务

三、 项目搭建

1.创建项目

pnpm create vue
# or
npm init vue@latest
# or
yarn create vue

2.选择项目依赖

✔ Project name: … patients-h5-100
✔ Add TypeScript? … No / `Yes`
✔ Add JSX Support? … `No` / Yes
✔ Add Vue Router for Single Page Application development? … No / `Yes`
✔ Add Pinia for state management? … No / `Yes`
✔ Add Vitest for Unit Testing? … `No` / Yes
✔ Add Cypress for both Unit and End-to-End testing? … `No` / Yes
✔ Add ESLint for code quality? … No / `Yes`
✔ Add Prettier for code formatting? … No / `Yes`

Scaffolding project in /Users/zhousg/Desktop/patient-h5-100...

Done. Now run:

  cd patient-h5-100
  pnpm install
  pnpm lint
  pnpm dev

3.插件推荐

必装:

  • Vue Language Features (Volar) vue3语法支持
  • TypeScript Vue Plugin (Volar) vue3中更好的ts提示
  • Eslint 代码风格校验

注意

  • vscode 安装了 Prettier 插件的可以先 禁用,或者关闭保存自动格式化功能,避免和项目的 Eslint 风格冲突。

可选:

  • gitLens 代码git提交记录提示
  • json2ts json自动转ts类型
  • Error Lens 行内错误提示

提示:

  • 大中型项目建议开启 TS托管模式 , 更好更快的类型提示。

4.eslint 预制配置

  rules: {
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true,
        semi: false,
        printWidth: 80,
        trailingComma: 'none',
        endOfLine: 'auto'
      }
    ],
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index']
      }
    ],
    'vue/no-setup-props-destructure': ['off'],
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }

  • 格式:单引号,没有分号,行宽度80字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
  • vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
  • 允许对 props 进行解构,我们会开启解构保持响应式的语法糖

执行:

# 修复格式
pnpm lint

vscode 开启 eslint 自动修复

    "editor.codeActionsOnSave": {
        "source.fixAll": true,
    },
  • 如果公司中会有自己的代码风格规则,大家只需遵守即可
  • Options · Prettier 常见规则

5.代码检查工作流

husky 配置

  • 初始化与安装
pnpm dlx husky-init && pnpm install
  • 修改 .husky/pre-commit 文件
pnpm lint

 lint-staged 配置

  • 安装
pnpm i lint-staged -D
  • 配置 package.json
{
  // ... 省略 ...
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}
{
  "scripts": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
  }
}
  • 修改 .husky/pre-commit 文件
pnpm lint-staged

6.项目结构调整

./src
├── assets        `静态资源,图片...`
├── components    `通用组件`
├── composable    `组合功能通用函数`
├── icons         `svg图标`
├── router        `路由`
│   └── index.ts
├── services      `接口服务API`
├── stores        `状态仓库`
├── styles        `样式`
│   └── main.scss
├── types         `TS类型`
├── utils         `工具函数`
├── views         `页面`
├── main.ts       `入口文件`
└──App.vue       `根组件`

项目使用sass预处理器,安装sass,即可支持scss语法: 

pnpm add sass -D

7.路由代码解析 

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

// createRouter 创建路由实例,===> new VueRouter()
// history 是路由模式,hash模式,history模式
// createWebHistory() 是开启history模块   http://xxx/user
// createWebHashHistory() 是开启hash模式    http://xxx/#/user

// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 ’/‘
// https://vitejs.dev/guide/build.html#public-base-path
// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts  添加配置  base: my-path,路由这就会加上 my-path 前缀了

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router

  • 如何创建实例的方式?

    • createRouter()
  • 如何设置路由模式?

    • createWebHistory() 或者 createWebHashHistory()
  • import.meta.env.BASE_URL 值来自哪里?

    • vite.config.ts 的 base 属性的值
  • base 作用是什么?

    • 项目的基础路径前缀,默认是 /

8.vant组件库

安装:

# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant

样式:main.ts

import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
// 样式全局使用
import 'vant/lib/index.css'
import './styles/main.scss'

const app = createApp(App)

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

组件按需使用:App.vue 

<script setup lang="ts">
import { Button as VanButton } from 'vant'
</script>

<template>
  <van-button>按钮</van-button>
</template>

<style scoped></style>
  • 注:全局使用是全量加载,是项目体积变大,加载慢

9.移动端适配 

实现:使用 vw 完成移动端适配 

安装:

npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
pnpm add -D postcss-px-to-viewport

配置: postcss.config.js

// eslint-disable-next-line no-undef
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      // 设备宽度375计算vw的值
      viewportWidth: 375,
    },
  },
};

测试: 

  • 有一个控制台警告可忽略,或者使用 postcss-px-to-viewport-8-plugin 代替当前插件

10.css变量主题定制

实现:使用css变量定制项目主题,和修改vant主题

  • 定义 css 变量使用 css 变量
:root {
  --main: #999;
}
a {
  color: var(--main)
}
  • 定义项目的颜色风格,覆盖vant的主题色 官方文档

styles/main.scss

:root {
  // 问诊患者:色板
  --cp-primary: #16C2A3;
  --cp-plain: #EAF8F6;
  --cp-orange: #FCA21C;
  --cp-text1: #121826;
  --cp-text2: #3C3E42;
  --cp-text3: #6F6F6F;
  --cp-tag: #848484;
  --cp-dark: #979797;
  --cp-tip: #C3C3C5;
  --cp-disable: #D9DBDE;
  --cp-line: #EDEDED;
  --cp-bg: #F6F7F9;
  --cp-price: #EB5757;
  // 覆盖vant主体色
  --van-primary-color: var(--cp-primary);
}

 App.vue

<script setup lang="ts"></script>

<template>
  <!-- 验证vant颜色被覆盖 -->
  <van-button type="primary">按钮</van-button>
  <a href="#">123</a>
</template>

<style scoped lang="scss">
// 使用 css 变量
a {
  color: var(--cp-primary);
}
</style>

11.用户状态仓库 

完成:用户信息仓库创建,提供用户信息,修改用信息,删除用户信息的方法 

  • 请求工具需要携带token,访问权限控制需要token,所以用户信息仓库先完成

需求:

  • 用户信息仓库创建
  • 提供用户信息
  • 修改用信息的方法
  • 删除用信息的方法

代码:

types/user.d.ts

// 用户信息
export type User = {
  /** token令牌 */
  token: string
  /** 用户ID */
  id: string
  /** 用户名称 */
  account: string
  /** 手机号 */
  mobile: string
  /** 头像 */
  avatar: string
}

 stores/user.ts

import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('cp-user', () => {
  // 用户信息
  const user = ref<User>()
  // 设置用户,登录后使用
  const setUser = (u: User) => {
    user.value = u
  }
  // 清空用户,退出后使用
  const delUser = () => {
    user.value = undefined
  }
  return { user, setUser, delUser }
})
  • pinia存储这个数据的意义?

    • 数据共享,提供给项目中任何位置使用
  • 如果存储了数据,刷新页面后数据还在吗?

    • 不在,现在仅仅是js内存中,需要进行本地存储(持久化)

12.数据持久化

使用 pinia-plugin-persistedstate 实现pinia仓库状态持久化,且完成测试

参考文档

image-20220730222310940

  • 安装
pnpm i pinia-plugin-persistedstate
# or
npm i pinia-plugin-persistedstate
# or
yarn add pinia-plugin-persistedstate
  • 使用 main.ts
import persist from 'pinia-plugin-persistedstate'
const app = createApp(App)

app.use(createPinia().use(persist))
  • 配置 stores/user.ts
import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'cp-user',
  () => {
    // 用户信息
    const user = ref<User>()
    // 设置用户,登录后使用
    const setUser = (u: User) => {
      user.value = u
    }
    // 清空用户,退出后使用
    const delUser = () => {
      user.value = undefined
    }
    return { user, setUser, delUser }
  },
  {
    persist: true
  }
)

  • 测试 App.vue
<script setup lang="ts">
import { useUserStore } from './stores/user'

const store = useUserStore()
</script>

<template>
  <p>{{ store.user }}</p>
  <button @click="store.setUser({ id: '1', mobile: '1', account: '1', avatar: '1', token: '1' })">
    登录
  </button>
  <button @click="store.delUser()">退出</button>
</template>

13.stores统一导出 

仓库的导出统一从 ./stores 代码简洁,职能单一,入口唯一

  • 抽取pinia实例代码,职能单一

stores/index

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia

main.ts 

import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
import './styles/main.scss'

const app = createApp(App)

app.use(pinia)
app.use(router)
app.mount('#app')
  • 统一导出,代码简洁,入口唯一

stores/index

export * from './modules/user'

App.vue 

-import { useUserStore } from './stores/user'
+import { useUserStore } from './stores'

14.请求工具函数 

实现:token请求头携带,错误响应处理,401错误处理

utils/request.ts

模板代码:

import axios from 'axios'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
})

instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    return config
  },
  (err) => Promise.reject(err)
)

instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    return res
  },
  (err) => {
    // TODO 5. 处理401错误
    return Promise.reject(err)
  }
)

export default instance

代码实现: 

import { useUserStore } from '@/stores'
import router from '@/router'
import axios from 'axios'
import { showToast } from 'vant'

// 1. 新axios实例,基础配置
const instance = axios.create({
  baseURL: 'https://consult-api.itheima.net/',
  timeout: 10000
})

// 2. 请求拦截器,携带token
instance.interceptors.request.use(
  (config) => {
    const store = useUserStore()
    if (store.user?.token && config.headers) {
      config.headers['Authorization'] = `Bearer ${store.user?.token}`
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 3. 响应拦截器,剥离无效数据,401拦截
instance.interceptors.response.use(
  (res) => {
    // 后台约定,响应成功,但是code不是10000,是业务逻辑失败
    if (res.data?.code !== 10000) {
      showToast(res.data?.message || '业务失败')
      return Promise.reject(res.data)
    }
    // 业务逻辑成功,返回响应数据,作为axios成功的结果
    return res.data
  },
  (err) => {
    if (err.response.status === 401) {
      // 删除用户信息
      const store = useUserStore()
      store.delUser()
      // 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
      router.push({
        path: '/login',
        query: { returnUrl: router.currentRoute.value.fullPath }
      })
    }
    return Promise.reject(err)
  }
)

export { baseURL, instance }

 导出一个通用的请求工具函数,支持设置响应数据类型

  • 导出一个通用的请求工具函数
import axios, { AxiosError, type Method } from 'axios'

// 4. 请求工具函数
const request = (url: string, method: Method = 'GET', submitData?: object) => {
  return instance.request({
    url,
    method,
    [method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
  })
}
  • 支持不同接口设不同的响应数据的类型

加上泛型

// 这个需要替换axsio.request默认的响应成功后的结果类型
// 之前是:传 { name: string } 然后res是   res = { data: { name: string } }
// 但现在:在响应拦截器中返回了 res.data  也就是将来响应成功后的结果,和上面的类型一致吗?
// 所以要:request<数据类型,数据类型>() 这样才指定了 res.data 的类型
// 但是呢:后台返回的数据结构相同,所以可以抽取相同的类型
type Data<T> = {
  code: number
  message: string
  data: T
}
// 4. 请求工具函数
const request = <T>(url: string, method: Method = 'get', submitData?: object) => {
  return instance.request<T, Data<T>>({
    url,
    method,
    [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
  })
}

测试封装好的请求工具函数 

 App.vue

<script setup lang="ts">
import { request } from '@/utils/request'
import type { User } from './types/user'
import { Button as VanButton } from 'vant'
import { useUserStore } from './stores'

// 测试,请求拦截器,是否携带token,响应拦截器401拦截到登录地址
const getUserInfo = async () => {
  const res = await request('/patient/myUser')
  console.log(res)
}

// 测试,响应拦截器,出现非10000的情况,和返回剥离后的数据
const store = useUserStore()
const login = async () => {
  const res = await request<User>('/login/password', 'POST', {
    mobile: '13211112222',
    // 密码 abc123456 测试:出现非10000的情况
    password: 'abc12345'
  })
  store.setUser(res.data)
}
</script>

<template>
  <van-button type="primary" @click="getUserInfo">获取个人信息</van-button>
  <van-button type="primary" @click="login">登录</van-button>
</template>

四、登录模块 

1.约定路由规则 

 

  • / 是布局容器,是一级路由 /home /article /notify /user 是二级路由
  • 他们的配置需要嵌套,其他的页面路由都是一级路由

2.路由与组件 

  • 基础组件结构 views/Login/index.vue
<script setup lang="ts"></script>

<template>
  <div class="login-page">login</div>
</template>

<style lang="scss" scoped></style>
  • 路由规则的配置 router/index.ts
  routes: [{ path: '/login', component: () => import('@/views/Login/index.vue') }]
  • app组件路由出口 App.vue
<script setup lang="ts"></script>

<template>
  <router-view></router-view>
</template>

3. 自动按需加载

 配置函数自动按需导入

# 通过 npm 安装
npm i unplugin-vue-components -D
# 通过 yarn 安装
yarn add unplugin-vue-components -D
# 通过 pnpm 安装
pnpm add unplugin-vue-components -D
  • 配置:
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    // 解析单文件组件的插件
    vue(),
    // 自动导入的插件,解析器可以是 vant element and-vue 
    Components({
      dts: false,
      // 原因:Toast Confirm 这类组件的样式还是需要单独引入,样式全局引入了,关闭自动引入
      resolvers: [VantResolver({ importStyle: false })]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

 4.cp-nav-bar 组件结构

抽离组件:components/CpNavBar.vue

<script setup lang="ts">
import { useRouter } from 'vue-router'

//1. 一定有的功能:返回图标,返回效果,固定定位(组件内部实现)
const router = useRouter()
const onClickLeft = () => {
  // 判断历史记录中是否有回退
  if (history.state?.back) {
    router.back()
  } else {
    router.push('/')
  }
}

// 2. 使用组件时候才能确定的功能:标题,右侧文字,点击右侧文字行为(props传入)
defineProps<{
  title?: string
  rightText?: string
}>()
const emit = defineEmits<{
  (e: 'click-right'): void
}>()
const onClickRight = () => {
  emit('click-right')
}
</script>

<template>
  <van-nav-bar
    left-arrow
    @click-left="onClickLeft"
    fixed
    :title="title"
    :right-text="rightText"
    @click-right="onClickRight"
  ></van-nav-bar>
</template>

<style lang="scss" scoped>
:deep() {
  .van-nav-bar {
    &__arrow {
      font-size: 18px;
      color: var(--cp-text1);
    }
    &__text {
      font-size: 15px;
    }
  }
}
</style>

 

 5.页面布局

  • 准备全局重置样式 style/main.scss

// 全局样式
body {
  font-size: 14px;
  color: var(--cp-text1);
}
a {
  color: var(--cp-text2);
}
h1,h2,h3,h4,h5,h6,p,ul,ol {
  margin: 0;
  padding: 0;
}

  • 登录页面整体结构 vies/Login/index.vue

<script setup lang="ts"></script>

<template>
  <div class="login-page">
    <cp-nav-bar
      right-text="注册"
      @click-right="$router.push('/register')"
    ></cp-nav-bar>
    <!-- 头部 -->
    <div class="login-head">
      <h3>密码登录</h3>
      <a href="javascript:;">
        <span>短信验证码登录</span>
        <van-icon name="arrow"></van-icon>
      </a>
    </div>
    <!-- 表单 -->
    <van-form autocomplete="off">
      <van-field placeholder="请输入手机号" type="tel"></van-field>
      <van-field placeholder="请输入密码" type="password"></van-field>
      <div class="cp-cell">
        <van-checkbox>
          <span>我已同意</span>
          <a href="javascript:;">用户协议</a>
          <span>及</span>
          <a href="javascript:;">隐私条款</a>
        </van-checkbox>
      </div>
      <div class="cp-cell">
        <van-button block round type="primary">登 录</van-button>
      </div>
      <div class="cp-cell">
        <a href="javascript:;">忘记密码?</a>
      </div>
    </van-form>
    <!-- 底部 -->
    <div class="login-other">
      <van-divider>第三方登录</van-divider>
      <div class="icon">
        <img src="@/assets/qq.svg" alt="" />
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.login {
  &-page {
    padding-top: 46px;
  }
  &-head {
    display: flex;
    padding: 30px 30px 50px;
    justify-content: space-between;
    align-items: flex-end;
    line-height: 1;
    h3 {
      font-weight: normal;
      font-size: 24px;
    }
    a {
      font-size: 15px;
    }
  }
  &-other {
    margin-top: 60px;
    padding: 0 30px;
    .icon {
      display: flex;
      justify-content: center;
      img {
        width: 36px;
        height: 36px;
        padding: 4px;
      }
    }
  }
}
.van-form {
  padding: 0 14px;
  .cp-cell {
    height: 52px;
    line-height: 24px;
    padding: 14px 16px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    .van-checkbox {
      a {
        color: var(--cp-primary);
        padding: 0 5px;
      }
    }
  }
  .btn-send {
    color: var(--cp-primary);
    &.active {
      color: rgba(22,194,163,0.5);
    }
  }
}
</style>

  • 定制样式 style/main.scss

  // 覆盖vant主体色
  --van-primary-color: var(--cp-primary);
  // 单元格上下间距
  --van-cell-vertical-padding: 14px;
  // 复选框大小
  --van-checkbox-size: 14px;
  // 默认按钮文字大小
  --van-button-normal-font-size: 16px;

 五、其他模块不再过多描述,需要参考的可以底部看源码;下面介绍一下项目用到的技术模块详细介绍

1.问诊支付

支付流程:

  • 点击支付按钮,调用生成订单接口,得到 订单ID,打开选择支付方式对话框
  • 选择支付方式,(测试环境需要配置 回跳地址)调用获取支付地址接口,得到支付地址,跳转到支付宝页面
    • 使用支付宝APP支付(在手机上且安装沙箱支付宝)
    • 使用浏览器账号密码支付 (测试推荐)
  • 支付成功回跳到问诊室页面

回跳地址:

http://localhost:5173/room

 支付宝沙箱账号:

买家账号:scobys4865@sandbox.com
登录密码:111111
支付密码:111111

 OR

买家账号:askgxl8276@sandbox.com
登录密码:111111
支付密码:111111

2.websocket

什么是 websocket ? WebSockets handbook | WebSocket.org

  • 是一种网络通信协议,和 HTTP 协议 一样。

为什么需要websocket ?

  • 因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

理解 websokect 通讯过程

websocket api

var ws = new WebSocket("wss://javascript.info/article/websocket/demo/hello");

ws.onopen = function(evt) { 
  console.log("Connection open ...");
  ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};
ws.onclose = function(evt) {
  console.log("Connection closed.");
};      

ws://121.40.165.18:8800 备用 ws 服务,有广告。

项目中使用 socket.io-client 来实现客户端代码,它是基于 websocket 的库。

3.问诊室-socket.io使用

  1. socket.io 什么?

    • socket.io 是一个基于 WebSocket 的 CS(客户端-服务端)的实时通信库
    • 使用它可以在后端提供一个即时通讯服务
    • 它也提供一个 js 库,在前端可以去链接后端的 socket.io 创建的服务
    • 总结:它是一套基于 websocket 前后端即时通讯解决方案
  2. socket.io 如何使用?

关于这个模块会单独说一下项目使用过程 

源码 

lien0219/consult-patient-h5: 在线医疗h5项目,可以在线问诊,使用vue3+ts+pinia+websocket技术栈,项目包含第三方授权登录,订单支付,暂时支持支付宝支付,问诊咨询即时通讯等技术 (github.com)

  • 33
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值