react 项目框架的搭建(二)

一、 vscode 配置
搭建好基本的框架后,需要统一vscode的插件配置 。具体内容可以参照vscode 官网的Workspace recommended extensions部分。在项目路径下面添加一个.vscode 文件夹,再新建一个extensions.json 文件,把统一要用的插件列进去

{
  "recommendations": [
   "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "steoates.autoimport",
    "formulahendry.auto-close-tag",
    "formulahendry.auto-rename-tag",
    "clinyong.vscode-css-modules",
    "quicktype.quicktype"
   ]
}

新建一个settings.json 文件,配置下面的内容。这里配置等效于在vscode 里面的Settings editor 。setting里面有什么配置这些配置项的功能是什么,你可以去官网查看 https://code.visualstudio.com/docs/getstarted/settings。

{
{
  // #每次保存的时候将代码按eslint格式进行修复
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true
  },
  // 点击保存时,根据 eslint 规则自定修复,同时集成 prettier 到 eslint 中
  "prettier.eslintIntegration": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // 为了避免和 eslint 冲突,将编辑器默认的代码检查规则关闭(如果开启了)
  "editor.formatOnSave": true,
  // stylelint 扩展自身的校验就够了
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  // 使用本地安装的 TypeScript 替代 VSCode 内置的来提供智能提示
  "typescript.tsdk": "./node_modules/typescript/lib",
  // 指定哪些文件不参与搜索
  "search.exclude": {
    "**/node_modules": true,
    "dist": true,
    "yarn.lock": true
  },
  // 指定哪些文件不被 VSCode 监听,预防启动 VSCode 时扫描的文件太多,导致 CPU 占用过高
  "files.watcherExclude": {
    "**/.git/objects/**": true,
    "**/.git/subtree-cache/**": true,
    "**/node_modules/*/**": true,
    "**/dist/**": true
  },
  "eslint.options": {
    "configFile": "./.eslintrc.js"
  }
}

vscode 代码片段功能非常方便程序员初始化一个组件代码,熟悉这个功能,和省掉去 ctr+c一个完整的组件 然后再猛的删掉冗余代码的操作。在.vscode 里面新建一个xx.code-snippets 的文件。把整个项目里面经常重复的代码配置成代码片段。这样只要敲关键字就能自动生成已经写好的代码。

{
	// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 
	// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 
	// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 
	// used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 
	// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 
	// Placeholders with the same ids are connected.
	// Example:
	// "Print to console": {
	// 	"scope": "javascript,typescript",
	// 	"prefix": "log",
	// 	"body": [
	// 		"console.log('$1');",
	// 		"$2"
	// 	],
	// 	"description": "Log output to console"
	// }
 "ts react function component": {
    "scope": "typescriptreact",  // 语言范围ts
    "prefix": "tsrfc",    //匹配的关键字
    "body": [
      "import React from 'react';",
      "import styles from './${1:${TM_FILENAME_BASE}}.module.scss';",
      "",
      "interface ${1:${TM_FILENAME_BASE}}Props {",
      "  [key: string]: any;",
      "}",
      "",
      "const ${1:${TM_FILENAME_BASE}}: React.FC<${1:${TM_FILENAME_BASE}}Props> = () => {",
      "  return (",
      "    <div className={styles.wrap}>",
      "      ${0}",
      "    </div>",
      "  );",
      "};",
      "",
      "export default ${1:${TM_FILENAME_BASE}};"
    ]
  }
}

在vscode的tsx 文件里面 敲tsrfc
在这里插入图片描述
最后自动生成代码片段如下:
在这里插入图片描述
二、项目文件结构调整
creat-react-app facebook 提供的手脚架提供的文件结构如下:

my-app
├── README.md
├── node_modules
├── package.json
├── yarn.lock
├── .npmrc
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── reportWebVitals.js
    └── setupTests.js

在这基础上根目录上我会添加上typescript 的全局类型定义文件架 @types,src 里面添加assets、components、constants、layouts、utils、style、pages、hooks等。其他的文件夹比如 后续框架用到的easy-p-easy store action state 等文件所需要存储的文件,可以另外新建文件夹存储(这个后面用到的时候再讲)。
调整后的文件目录结构为:

my-app
├── .vscode           # vscode 配置目录,包含常用的代码片段、设置等
│   ├── extensions.json  #项目推荐使用安装的插件
│   ├── settings.json  #项目里面使用的vscode 的配置内容,比如自动保存代码格式化等
│   ├── tsrc.code-snippets #代码片段配置
├── .@types          # 全局类型声明
├── docs               # 存放项目的文档,比如接口文档等
├── node_modules  #模块
├── public               # 公共文件
│   ├── favicon.ico
│   ├── index.html   #入口html
│   └── manifest.json
└── src                 # 源码目录
│  ├── assets      # 静态资源目录
│  ├── components  # 公共业务组件目录
│  ├── constants    # constant 目录,存储api 等公共的类型常量类的变动不大的文件
│  │     │── api  # 定义对接后台api 接口的文件
│  ├── layouts       # 布局目录
│  ├── models      # 存放easy-peasy 的state和actions模块
│  ├── pages       # 页面组件目录
│  ├── routers       # 页面路由相关的文件
│  ├── services       # model 对应的api 调用函数
│  ├── style         # 全局样式
│  ├── utils         # util 目录
│  ├── App.css
│  ├── App.js
│  ├── App.test.tsx
│  ├── index.css
│  ├── index.tsx
│   ├── logo.svg
│  └── reportWebVitals.ts
│  └── setupTests.ts
├── .eslintignore        # eslint忽略文件的配置
├── .eslintrc              #eslint 的配置文件
├── .gitignore           #git 的忽略文件的配置
├── .npmrc             #淘宝镜像的使用配置
├── .prettierrc         #prettier 插件的配置
├── .stylelintrc.json        #stylelint插件的配置
├── package.json            #项目的配置文件
├── README.md            #项目的说明文件
├── tsconfig.json               #typescript 的配置
├── yarn.lock                    #yarn lock 文件

三、常用模块的引入和代码调整
3.1 安装antd 推荐的craco 模块

yarn add antd @craco/craco

修改package.json 里面的启动指令,在项目跟目录里面添加craco.config.js

/* package.json */
"scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
-   "test": "react-scripts test",
+   "start": "craco start",
+   "build": "craco build",
+   "test": "craco test",
}

craco.config.js,主要是在create-react-app 不用eject暴露webpack 配置的情况下,方便在外部修改webpack 的配置

/* craco.config.js */
module.exports = {
  // ...
};

3.2 添加antd 和ant 国际化
安装antd

yarn add antd

添加国际语言包

import zhCN from 'antd/lib/locale/zh_CN';

// index.tsx 里面添加语言包支撑
ReactDOM.render(
  <React.StrictMode>
    <ConfigProvider locale={zhCN}>
      <App />
    </ConfigProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

3.3 添加easy-peasy
项目搭建的初期选择easy-peasy 而不是选择redux 的原因有两个:1. easy-peasy 能很好的支持react 的hooks,2.easy-peasy 的代码看起来比较直观些,state 和action 直接放在一起 。

yarn add easy-peasy

easy-peasy 里面提供的示例:

const store = createStore({
  todos: {
    items: ['Create store', 'Wrap application', 'Use store'],
    add: action((state, payload) => {
      state.items.push(payload);
    }),
  },
});
function App() {
  return (
    <StoreProvider store={store}>
      <TodoList />
    </StoreProvider>
  );
}
function TodoList() {
  const todos = useStoreState((state) => state.todos.items);
  const add = useStoreActions((actions) => actions.todos.add);
  return (
    <div>
      {todos.map((todo, idx) => (
        <div key={idx}>{todo}</div>
      ))}
      <AddTodo onAdd={add} />
    </div>
  );
}

easy-Peasy 和redux 类似,createStore函数创建一个store 然后挂在在项目的入口标签。store 里面定义了对应的state 和actions (同步异步actions)。根据这个特性,项目的文件结构对应的调整下。新增modals services 两个文件夹。services 存放接口调用的函数、modals 存放state和actions。
具体实现如下:
在constants 下面新建一个store.ts

import { createStore } from 'easy-peasy';
import { storeModel } from 'src/models/index';

const store = createStore(storeModel);
export default store;

在根目录下新建一个models 文件夹用来存放state 和actions.
models 下面新建一个index.ts

export interface StoreModel {}

export const storeModel:StoreModel = {};

修改根目录的入口代码index.tsx

import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import { StoreProvider } from 'easy-peasy';
import React from 'react';
import ReactDOM from 'react-dom';
import store from 'src/constants/store';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <ConfigProvider locale={zhCN}>
      <StoreProvider store={store}>
        <App />
      </StoreProvider>
    </ConfigProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

在hooks 下面创建一个easyPeasyApi.ts文件,存储easyPeasy Api 提供的userStoreActions 、useStoreState 等工具函数方便后面组件的使用,

import { createTypedHooks } from 'easy-peasy';
import { StoreModel } from 'src/models';

// Provide our model to the helper      👇
const typedHooks = createTypedHooks<StoreModel>();

// 👇 export the typed hooks
export const useStoreActions = typedHooks.useStoreActions;
export const useStoreDispatch = typedHooks.useStoreDispatch;
export const useStoreState = typedHooks.useStoreState;
export const useStore = typedHooks.useStore;

这样easy-peasy就被简单的引入进来了。和redux 一样如果要支撑异步的action 是需要一个中间件支撑的, easy-peasy 支持Redux middleware 和Redux Dev Tools。这样直接引入redux 、redux-logger 等模块。

redux:yarn add redux redux-logger ,yarn add @types/redux-logger

store.ts 文件的代码:

import { createStore } from 'easy-peasy';
import { Middleware } from 'redux';
import logger from 'redux-logger';
import { storeModel } from 'src/models';

const middlewares:Middleware[] = [];
if (process.env.NODE_ENV === 'development') {
  middlewares.push(logger);
}
const store = createStore(storeModel, { middleware: middlewares });
export default store;

在根目录上新一个services文件夹,存放每一个model对应的service (model 对应的接口处理函数)文件。每个model 和service 命名规则如下:

在这里插入图片描述
在这里插入图片描述
文件:模块名称.(model/service).ts, 这样pages model service 一一对应,方便后期调试查找。
modal 文件定义的格式:

export interface GlobalModel {
  [key: string]: any;
}

export const globalModel: GlobalModel = {
  
}

这样整个easy-peasy 相关的文件都搭建好了。
但是在后期的开发过程中,你会发现models 里面定义了太多的ts 变量类型定义,会导致代码看起来有点乱,影响代码的阅读性。所以我推荐是在models 里面再建一个types 的单独类型的文件夹,对应model 的ts 类型定义可以单独存放到里面,命名为:模块名称.types.ts。

3.4 router 的定义和改造

react 是单页面运用,路由的作用是监控url 变化来对应的控制页面显示哪个模块。不同的页面可以会有相同的布局。所以这里的路由分两层,第一层控制切换不同的页面layout,第二层控制每个layout 显示哪个子页面。
安装react-router-dom

yarn add react-router-dom 
yarn add @types/react-router-dom

参照UmiJS 的动态路由设计,routers 文件里面分别创建一个 Router.ts RouterLayout.tsx PrivateRoute.tsx 以及RouterTypes.ts 等文件。
Router.ts 定义所有页面的路由;
RouterLayout.tsx 解析Router.ts并转化为router-router-dom 的router 格式的代码;
PrivateRoute.tsx 鉴权和重定向到登录页面;
RouterTypes 单独存放Router 定义需要的ts 类型。

代码的具体实现:
RouterTypes:

export interface RouteNode {
  path: string;
  name?: string;
  // icon?: React.ElementType | string;
  iconType?: string;
  /** 重定向不能与布局组件同时使用,同时使用时会忽略重定向(可以通过布局组件内部处理解决) */
  redirect?: string;
  /** 菜单布局组件会使用 */
  hideInMenu?: boolean;
  /** 是否是布局组件(默认 `false`)  */
  layout?: boolean;
  /** 当 `layout` 为 `true` 时,该组件会作为布局组件,接收 `router` (routes 别名) 及其它属性值,*/
  component?: React.ComponentType<any>;
  /** 用到的布局相关的子模块 */
  child?: React.ComponentType<any>;
  routes?: RouteNode[];
  /** 授权 */
  authority?: any[];
  /** 拥有子菜单 */
  hasSubMenu?: boolean;
  /** 预留自定义属性 */
  [otherProp: string]: any;
}

Router.ts :

import BasicLayout from 'src/layouts/BasicLayout';
import { RouteNode } from 'src/layouts/LayoutsTypes';
import { Roles } from 'src/models/modelTypes/global.types';
import Home from 'src/pages/Home';
import Login from 'src/pages/Login';
const router: RouteNode[] = [
  { path: '/login', name: '登录', component: Login },
  {
    path: '/home',
    name: '主页Layout',
    authority: [Roles.Admin, Roles.Guest],
    component: BasicLayout,
    layout: true,
    routes: [
      {
        path: '/home',
        name: '主页',
        authority: [Roles.Guest],
        component: Home,
      },
    ],
  },
  {
    path: '/',
    name: '首页',
    redirect: '/home',
  },
];
export default router;

RouterLayout.tsx

import _ from 'lodash';
import React from 'react';
import { Redirect, Switch } from 'react-router-dom';
import { RouteNode } from 'src/layouts/LayoutsTypes';
import PrivateRoute from 'src/routers/PrivateRoute';

interface RouteLayoutProps {
  router: RouteNode[];
}
const RouteLayout: React.FC<RouteLayoutProps> = ({ router }) => {
  return (
    <Switch>
      {router.map(({ path, layout, routes, component, redirect, ...otherProps }) => {
        return (
          <PrivateRoute
            key={path + 'layout'}
            path={path}
            render={(props: any) => {
              // 如果只是layout 没有子路由直接生成layout组件
              if (layout && !_.isEmpty(routes) && component) {
                return React.createElement(component, {
                  router: routes, // 传入layout下面的routes 数据
                  ...otherProps,
                  ...props,
                });
              }
              // 如果不是layout 页面,也不是重定向的配置,直接渲染当前页面,比如login登录页面
              return redirect ? (
                <Redirect to={redirect} />
              ) : (
                component && React.createElement(component, props)
              );
            }}
            {...otherProps}
          />
        );
      })}
    </Switch>
  );
};

export default RouteLayout;

PrivateRoute.tsx

import _ from 'lodash';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { useStoreState } from 'src/hooks/easyPeasyApi';
interface PrivateRouteProps {
  key: string;
  [key: string]: any;
}
/**
 * 路由权限的判断
 * @returns 没有权限就返回登录页面,有权限就返回相应的组件
 */
const PrivateRoute: React.FC<PrivateRouteProps> = ({ key, authority = [], ...otherProps }) => {
  const { userInfo } = useStoreState((state) => state.globalModel);
  // 没定义权限 或者 当前用户用户这个模块的权限 和 路由是 login 组件时, 直接渲染组件
  return _.isEmpty(authority) ||
    authority.includes(userInfo.role) ||
    otherProps.path == '/login' ? (
    <Route {...otherProps} key={key} />
  ) : (
    <Redirect to="/login" />
  );
};

export default PrivateRoute;

项目的入口文件index.tsx 修改为

import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import { StoreProvider } from 'easy-peasy';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import store from 'src/constants/store';
import router from 'src/routers/Router';
import RouteLayout from 'src/routers/RouterLayout';
import './index.css';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <ConfigProvider locale={zhCN}>
      <StoreProvider store={store}>
        <Router>
          <RouteLayout router={router || []} />
        </Router>
      </StoreProvider>
    </ConfigProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();


处理完第一层layout 的路由切换,接下来就需要处理layout 内部子模块的路由切换。如下图点击菜单栏触发绿色框里面的内容切换。
在这里插入图片描述
Layout 的具体实现:

import { CaretDownFilled } from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import _ from 'lodash';
import React from 'react';
import { Link, Redirect, Switch, useHistory, useLocation } from 'react-router-dom';
import { useStoreState } from 'src/hooks/easyPeasyApi';
import PrivateRoute from 'src/routers/PrivateRoute';
import { RouteNode } from 'src/routers/RouterTypes';
import styles from './BasicLayout.module.scss';
interface BasicLayoutProps {
  router: RouteNode[]; // 读取router 里面有layout 里面的routes 路由
}
const { Header, Content } = Layout;
const { SubMenu } = Menu;
const MenuItem = Menu.Item;
const BasicLayout: React.FC<BasicLayoutProps> = ({ router }) => {
  const { userInfo } = useStoreState((state) => state.globalModel);
  const history = useHistory();
  const location = useLocation();
  const { pathname } = location;
  const openKey = '/' + pathname.split('/')[1];

  return (
    <Layout className={styles.wrap}>
      <Header className={styles.header}>
        <div className={styles.left}>
          <Link className={styles.logo} to="/" />
          <span>{'系统名字'}</span>
        </div>
        <Menu
          className={styles.mid}
          mode="horizontal"
          selectedKeys={[openKey]}
          onSelect={(param) => {
            // if (param.key && param.key === 'invalid_route') return;
            const item = router.find((m) => m.path === param.key && m.redirect);
            if (item) {
              history.push(item.redirect!);
            } else {
              history.push(param.key + '');
            }
          }}
        >
          {router
            .filter((m) => !m.hideInMenu)
            .map((m) => {
              const condition =
                (m.routes &&
                  m.routes?.filter((v) => m.authList && m.authList?.includes(userInfo.role))
                    .length) ||
                0;
              if (m.hasSubMenu && !_.isEmpty(m.routes) && m.routes && condition >= 2) {
                // 只有一个菜单时,不显示下拉菜单选项
                return (
                  <SubMenu
                    key={m.path}
                    title={
                      <span>
                        {/* {m.icon && React.createElement(m.icon)} */}
                        <span>{m.name}</span>
                        <CaretDownFilled style={{ fontSize: 14, marginLeft: 7 }} />
                      </span>
                    }
                  >
                    {m.routes?.map((sm) =>
                      sm.authList && sm.authList?.includes(userInfo.role) ? (
                        <MenuItem key={sm.path}>{sm.name}</MenuItem>
                      ) : (
                        ''
                      ),
                    )}
                  </SubMenu>
                );
              }
              return <MenuItem key={m.path}>{m.name}</MenuItem>;
            })}
        </Menu>
      </Header>
      <Layout className={styles.content}>
        <Content>
          <Switch>
            {router.map((m) => {
              const {
                name,
                hideInMenu,
                path,
                routes,
                layout,
                redirect,
                component,
                ...otherProps
              } = m;
              // handle layout
              if (layout && component && !_.isEmpty(routes)) {
                return (
                  <PrivateRoute
                    key={path}
                    path={path}
                    render={(props: any) =>
                      React.createElement(component, { router: routes, ...props })
                    }
                    {...otherProps}
                  />
                );
              }
              if (routes && !_.isEmpty(routes)) {
                return routes.map((n) => <PrivateRoute key={n.path} {...n} />);
              }

              return (
                <PrivateRoute
                  key={path}
                  path={path}
                  render={(props: any) =>
                    redirect ? (
                      <Redirect to={redirect} />
                    ) : (
                      component && React.createElement(component, props)
                    )
                  }
                  {...otherProps}
                />
              );
            })}
          </Switch>
        </Content>
      </Layout>
    </Layout>
  );
};

export default BasicLayout;

这样路由相关的代码据基本完成。

3.5 request 请求模块的封装
request 封装自定义封装可以统一代码格式,这里我尝试用Axios模块进行二次封装。代码还未验证,如果有问题我后面再修改。

import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import _ from 'lodash';

export interface ReqInit extends AxiosRequestConfig {
  headers?: Record<string, string>;
  /** eg. ?a=1 */
  params?: { [key: string]: any };
  /** eg. /:id/.. */
  router?: { [key: string]: any };
  /** 超时时间,默认3000 */
  timeout?: number;
}
const MyAxiosInstance = axios.create({});
MyAxiosInstance.defaults = {
  ...axios.defaults,
  method: 'GET',
  headers: {
    Accept: '*/*',
  },
  mode: 'cors',
  timeout: 3000,
  // credentials: 'include' // send cookies
} as ReqInit;

async function request<T = unknown>(path: string, init: ReqInit = {}): Promise<T> {
  const mergeInit = {
    ...MyAxiosInstance.defaults,
    ...init,
    headers: { ...MyAxiosInstance.defaults.headers, ...init.headers },
  };
  const { params, router, data, headers } = mergeInit;
  const hasType = Reflect.has(headers, 'Content-Type') || Reflect.has(headers, 'content-type');
  let url = path;
  if (router) {
    url = path.replace(/:([A-Za-z]+)/g, (substring, p1: string) => router[p1]);
  }
  if (params) {
    url += _(_.reduce(params, (prev, val, key) => `${prev}${key}=${val}&`, '?')).trimEnd('&');
  }
  if (!hasType && typeof data === 'string') {
    Reflect.set(headers, 'Content-Type', 'application/json');
  }
  try {
    const response = MyAxiosInstance(url, mergeInit);
    return Promise.resolve((await response).data);
  } catch (err) {
    return Promise.reject(new Error(JSON.stringify({ err })));
  }
}

// 返回自定义的axios 实例
export function fetchInstance(): AxiosInstance {
  return MyAxiosInstance;
}

export default request;


参考:
1.https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions
2.https://code.visualstudio.com/docs/editor/userdefinedsnippets
3.https://ant.design/docs/react/use-with-create-react-app-cn
4.https://www.npmjs.com/package/easy-peasy
5.https://github.com/ctrlplusb/easy-peasy/blob/master/website/docs/docs/api/store-provider.md
6.https://blog.csdn.net/weixin_40599109/article/details/107848176
7.https://blog.csdn.net/ilovethesunshine/article/details/109679326
8.https://umijs.org/zh-CN/docs/convention-routing

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值