superset二次开发及汉化、图标等

superset 二次开发

一. 集成echarts图表,二次开发。

superset版本0.36.0,echarts版本4.7.0。

0.36.0版本是比较新的版本,代码结构相比 0.30以前的改动还是比较大的,主要是前端的代码结构变化比较大, superset 把前端的插件单独放在一个superset-ui的项目中;superset中的前端代码主要放在superset-frontend的目录中.

以集成echarts柱状折线图 mix-line-bar为例。

主要修改的地方:

1. superset-frontend/src/visualizations/ 目录下

1.1 新增文件夹MixLineBar

在新建的文件夹下(MixLineBar)新增四个JS文件

1.2 新建文件夹 images 放入新增图表的图片

图片在echarts 上可以下载
https://www.echartsjs.com/examples/zh/index.html

选择要接入的图表,然后右键另存为 就可以下载下来了
然后放在 images 文件下,可以转化为png

1.3 在MixLineBar下新增文件 MixLineBarChartPlugin.js
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';


const metadata = new ChartMetadata({
  name: t('Mix Line Bar'),
  description: '',
  credits: ['https://www.echartsjs.com/examples/en/editor.html?c=mix-line-bar'],
  thumbnail,
});

export default class MixLineBarChartPlugin extends ChartPlugin {
  constructor() {
    super({
      metadata,
      transformProps,
      loadChart: () => import('./ReactMixLineBar.js'), // 前端渲染逻辑
    });
  }
}

1.4 在MixLineBar下新增文件 ReactMixLineBar.js 注册
import reactify from '@superset-ui/chart/esm/components/reactify';
import Component from './MixLineBar';

export default reactify(Component);
1.5 在MixLineBar下新增文件 transformProps.js 前端后端数据转换
export default function transformProps(chartProps) {

    const {width, height, queryData, formData} = chartProps;
    // formData 前端页面的数据
    // queryData  后端返回的数据
    return {
        data: queryData.data,
        width,
        height,
        formData,
        legend: queryData.data.legend,
        x_data: queryData.data.x_data,
        series: queryData.data.data,
    };

}
1.6 在MixLineBar下新增文件 MixLineBar.js 前端渲染图表主要逻辑
import echarts from 'echarts';
import d3 from 'd3';
import PropTypes from 'prop-types';
import { CategoricalColorNamespace } from '@superset-ui/color';

// 数据类型检查
const propTypes = {
    data: PropTypes.object,
    width: PropTypes.number,
    height: PropTypes.number,
};

function MixLineBar(element, props) {

    const {
        width,
        height,
        data,
        formData,
        x_data,
        series,
        legend,
    } = props; // transformProps.js 返回的数据

    const fd = formData
    // 配置y轴显示信息
    const left_y_min = fd.leftYMIn
    const left_y_max = fd.leftYMax
    const left_y_interval = fd.leftYInterval
    const right_y_min = fd.rightYMin
    const right_y_max = fd.rightYMax
    const right_y_interval = fd.rightYInterval
    // y轴别名
    const y_axis_label = fd.yAxisLabel
    const y_axis_2_label = fd.yAxis2Label

    // 右边y轴 对应的 指标列
    const right_y_column = fd.rightYColumn
    // 为了适配颜色
    const colorFn = CategoricalColorNamespace.getScale(fd.colorScheme);
    var colors = []
    if (colorFn && colorFn.colors) {
        colors = colorFn.colors
    }
    const colors_len = colors.length

    // y轴配置格式
    var yAxis_1 = {
        type: 'value',
        name: 'Left_Y_Axis',
        axisLabel: {
            formatter: '{value}'
        }
    }

    var yAxis_2 = {
        type: 'value',
        name: 'Right_Y_Axis',
        axisLabel: {
            formatter: '{value}'
        }
    }

    if (left_y_min !== undefined) {
        yAxis_1['mix'] = left_y_min
    }
    if (left_y_max != undefined) {
        yAxis_1['max'] = left_y_max
    }
    if (left_y_interval != undefined) {
        yAxis_1['interval'] = left_y_interval
    }
    if (right_y_min != undefined) {
        yAxis_2['mix'] = right_y_min
    }
    if (right_y_max != undefined) {
        yAxis_2['max'] = right_y_max
    }
    if (right_y_interval != undefined) {
        yAxis_2['interval'] = right_y_interval
    }
    if (y_axis_label != undefined){
        yAxis_1['name'] = y_axis_label
    }
    if (y_axis_2_label != undefined){
        yAxis_2['name'] = y_axis_2_label
    }

    // 处理series 显示的数据 [{'name':xx, 'type':xx, 'data':xx, 'yAxisIndex':xx}]
    // 重新请求时, 默认展示左y,
    for (let i = 0; i < series.length; i++) {
        var serie = series[i]
        serie['yAxisIndex'] = 0
        if (right_y_column != undefined && right_y_column.indexOf(serie.name) >= 0) {
            serie['yAxisIndex'] = 1
        }
        if(colors_len>0){
            serie['itemStyle'] = {
                'color': colors[i%colors_len]
            }
        }
    }


    const div = d3.select(element);
    const sliceId = 'mix-bar-line-' + fd.sliceId;
    const html = '<div id='+ sliceId + ' style="height:' + height + 'px; width:' + width + 'px;"></div>';
    div.html(html);
    // init echarts,light 为制定主题,可以查看官方api
    
    var myChart = echarts.init(document.getElementById(sliceId), 'light');
    // echarts 渲染图表的数据格式 在官网可以查看
    
    var option = {
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'cross',
                crossStyle: {
                    color: '#999'
                }
            }
        },
        legend: {
            data: legend, //[] x轴的数据
        },
        xAxis: [
            {
                type: 'category',
                data: x_data,
                axisPointer: {
                    type: 'shadow'
                }
            }
        ],
        yAxis: [
            yAxis_1,
            yAxis_2,
        ],
        series: series,

    };

    myChart.setOption(option);
}

MixLineBar.displayName = 'Mix Line Bar';
MixLineBar.propTypes = propTypes;

export default MixLineBar;

注:下面的 2–7 步需要修改5个文件(MainPreset.js, VizTypeControl.jsx,controls.jsx,controls.jsx和viz.py),新增一个文件(controls.jsx)

2. 修改 superset-frontend/src/visualizations/presets/MainPreset.js

// 开头导入
import MixLineBarChartPlugin from '../MixLineBar/MixLineBarChartPlugin'

// 末尾添加
new MixLineBarChartPlugin().configure({ key: 'mix_line_bar' }),

3. 修改 superset-frontend/src/explore/components/controls/VizTypeControl.jsx

 //找到 DEFAULT_ORDER 这个变量 数组末尾 添加 新图表
 
 'mix_line_bar',

4. 新增 superset-frontend/src/explore/controlPanels/MixLineBar.js

页面前端布局

/**
 *   https://www.echartsjs.com/examples/zh/editor.html?c=mix-line-bar
 *   mix line bar
 */
import { t } from '@superset-ui/translation';


export default {
    requiresTime: true,
    controlPanelSections: [
        {
            label: t('Chart Options'),
            expanded: true,
            controlSetRows: [
                ['color_scheme', 'label_colors'],
            ],
        },

        {
            label: t('X Axis'),
            expanded: true,
            controlSetRows: [
                ['groupby'],
            ],
        },
        {
            label: t('Line Type'),
            expanded: true,
            controlSetRows: [
                ['line_metrics'],
            ],
        },
        {
            label: t('Bar Type'),
            expanded: true,
            controlSetRows: [
                ['bar_metrics'],
            ],
        },
        {
            label: t('Real Y Axis 2 Display Columns'),
            expanded: true,
            controlSetRows: [
                ['right_y_column'],
            ],
        },

        {
            label: t('Y Axis 1 Scale Value Setting'),
            expanded: true,
            controlSetRows: [
                ['left_y_min', 'left_y_max', 'left_y_interval'],
                ['y_axis_label']
            ],
        },
        {
            label: t('Y Axis 2 Scale Value Setting'),
            expanded: true,
            controlSetRows: [
                ['right_y_min', 'right_y_max', 'right_y_interval'],
                ['y_axis_2_label']
            ],
        },
        {
            label: t('Query'),
            expanded: true,
            controlSetRows: [
                ['adhoc_filters'],
            ],
        },

    ],
    controlOverrides: {

    },
};

5. 修改 superset-frontend/src/explore/controls.jsx 新增的一些自定义组件.

  line_metrics: {
    ...metrics, // 继承
    multi: true, // 多选
    clearable: true, // 是否可调用, true当作sql
    validators: [], // 是否可以为空
    label: t('Line Type Metrics'),
    description: t('Metrics for which line type are to be displayed'),
  },

  bar_metrics: {
    ...metrics,
    multi: true,
    clearable: true,
    validators: [],
    label: t('Bar Type Metrics'),
    description: t('Metrics for which bar type are to be displayed'),
  },

  y_metrics_2: {
    ...metrics, 
    multi: true,
    validators: [],
    default:null,
    label: t('Y Axis 2 Columns'),
    description: t('Select the numeric columns to display in Right-Y-Axis'),
  },
    left_y_min: {
    type: 'TextControl', //文本输入
    label: t('Left Y Min'),
    renderTrigger: true,
    isInt: true,
    description: t('Left Y Min'),
  },
  left_y_max: {
    type: 'TextControl',
    label: t('Left Y Max'),
    renderTrigger: true,
    isInt: true,
    description: t('Left Y Max'),
  },
  left_y_interval: {
    type: 'TextControl',
    label: t('Left Y Interval'),
    renderTrigger: true,
    isInt: true,
    description: t('Left Y Interval'),
  },
  right_y_min: {
    type: 'TextControl',
    label: t('Right Y Min'),
    renderTrigger: true,
    isInt: true,
    description: t('Right Y Min'),
  },
  right_y_max: {
    type: 'TextControl',
    label: t('Right Y Max'),
    renderTrigger: true,
    isInt: true,
    description: t('Right Y Max'),
  },
  right_y_interval: {
    type: 'TextControl',
    label: t('Right Y Interval'),
    renderTrigger: true,
    isInt: true,
    description: t('Right Y Interval'),
  },
  y_axis_2_label: {
    type: 'TextControl',
    label: t('Y Axis 2 Label'),
    renderTrigger: true,
    default: '',
  },
  
 right_y_column: {
    type: 'SelectControl',
    freeForm: true,
    renderTrigger: true,
    multi: true,
    label: t('Y Axis 2 Column'),
    description: t('Choose or add metrics (label) to display in right y axis'),
  },

6. 修改 superset-frontend/src/setup/setupPlugins.ts

// 开头引入
import MixLineBar from '../explore/controlPanels/MixLineBar';

// 末尾注册
.registerValue('mix_line_bar', MixLineBar)

7. 修改后端文件superset/viz.py

图表处理的逻辑都在这个文件中

7.1 找到 METRIC_KEYS 数组后 添加2个字符串(自定义的组件)
"line_metrics", "bar_metrics",
7.2 新增新图表后端逻辑

注意找对位置添加(不能加在末尾,因为末尾有个注册)

class MixLineBarViz(NVD3Viz):
    """ mix line bar"""
    viz_type = "mix_line_bar"
    verbose_name = _("Mix Line Bar")
    # 是否排序
    sort_series = False
    # 是否对time 做处理 _timestamp
    is_timeseries = False

    def query_obj(self):
        # check bar column, line column 是否重复
        bar_metrics = self.form_data.get('bar_metrics')
        line_metrics = self.form_data.get('line_metrics')
        if not bar_metrics and not line_metrics:
            raise Exception(_("Please choose metrics on line or bar type"))
        bar_metrics = [] if not bar_metrics else bar_metrics
        line_metrics = [] if not line_metrics else line_metrics
        intersection = [m for m in bar_metrics if m in line_metrics]
        if intersection:
            raise Exception(_("Please choose different metrics on line and bar type"))
        d = super().query_obj()
        return d

    def to_series(self, df, classed=""):
        """
         拼接 前端渲染需要的数据
        :param df:
        :param classed:
        :return: {'legend':[], 'bar':[], 'line':[]}
        """
        cols = []
        for col in df.columns:
            if col == "":
                cols.append("N/A")
            elif col is None:
                cols.append("NULL")
            else:
                cols.append(col)
        df.columns = cols
        series = df.to_dict("series")
        # [{}]
        bar_metrics = self.form_data.get('bar_metrics', [])
        bar_metrics = [] if not bar_metrics else bar_metrics
        line_metrics = self.form_data.get('line_metrics', [])
        line_metrics = [] if not line_metrics else line_metrics

        metrics = self.all_metrics
        legend, data = [], []
        for mt in metrics:
            m_label = utils.get_metric_name(mt)
            ys = series[m_label]
            if df[m_label].dtype.kind not in "biufc":
                continue
            legend.append(m_label)
            info = {
                "name": m_label,
                "data": [
                    ys.get(ds, None) for ds in df.index
                ],
                "type": ''
            }
            if mt in bar_metrics:
                info['type'] = 'bar'
            elif mt in line_metrics:
                info['type'] = 'line'
            else:
                continue
            data.append(info)
        chart_data = {
            'legend': legend,
            'data': data,
            'x_data': [str(ds) if not isinstance(ds, tuple) else ','.join(map(str, ds)) for ds in df.index]
        }

        return chart_data

    def get_data(self, df: pd.DataFrame):
        # 后端返回的数据
        df = df.pivot_table(index=self.groupby, values=self.metric_labels)
        chart_data = self.to_series(df)
        return chart_data

8. 最后

8.1 安装echarts模块
#命令行执行
npm install echarts@4.7.0
8.2 加载和初始化
superset init 
superset run 
npm run build
npm run dev

二. 汉化

声明:还有一些界面和字段没有汉化。

1. 基本的汉化

修改config.py文件

# 之前是en
BABEL_DEFAULT_LOCALE = 'zh'

然后编译messages.po文件

# 对应tranlastions文件夹的路径。
pybabel compile -d /root/superset/incubator-superset/superset/translations

2. 针对个别的汉化

上面的汉化可以汉化大部分的界面和字段,但是对于某些界面和字段还不可以汉化。

对图表类型和控制界面字段的汉化,在superset/translate/zh/LC_MESSAGES/下的message.json。

添加修改对应字段的中英文即可。这里可以汉化所有的图表类型和控制组件字段。

对于个别按钮和添加图表界面的汉化还在探索中。

3. 个别按钮的汉化,new,filter list,refresh等等。

<了解flask-babel模块>

1.首先需要找到对应的按钮或者需要个别汉化的内容在后端的那个文件中(py,html,js,jsx),都有可能。

(可以参考的方法:在当前页面,审查元素,定位到需要汉化的位置,然后复制当前节点或者父节点的的各种属性。在vscode中搜索,需要把整个项目添拖到vscode中)

这几个的汉化用的方法是flask-babel中,需要导入

from flask_babel import gettext as __, lazy_gettext as _
#汉化语法为
_('需要汉化的字符')

#按钮需要加上{{  }}才可行。
{{_('需要汉化的按钮字符')}}

new 和SQL Query在 templates/appbuilder/gengralnavbar_right.html中。
filter list和Refresh在templates/appbuilder/gengral/widgets/search.html中。

对没有汉化处理的进行汉化处理,_(‘需要汉化的字符’)
然后在message.po中添加。

#: superset/templates/appbuilder/navbar_right.html:35
msgid "New"
msgstr "新建"

#: superset/templates/appbuilder/general/widgets/search.html:24
msgid "Filter List"
msgstr "筛选列表"

#: superset/templates/appbuilder/general/widgets/search.html:57
msgid "Refresh"
msgstr "刷新"

在superset文件夹下编译pybabel compile -d translations即可成功。

New 和 SQL Query 还在superset-frontend/src/components/Menu/NewMenu.jsx中存在需要汉化,这个jsx是渲染首页的。

控制界面的Run Query, Save, Stop的汉化
通过审查元素查看,字体在superset-frontend/src/explore/components/QueryAndSaveBtns.jsx文件中。
这里的汉化用的不是flask-babel模块,需要导入的模块是

import { t } from '@superset-ui/translation';
#汉化语法为
t('需要汉化的字符')

#按钮需要加上{}
t{'需要汉化的字符'}

修改过superset-frontend任何js,jsx文件都需要重新打包。

#在superset-frontend目录下执行
npm run build

修改好js或者jsx文件后,执行npm run build,然后 1.在messages.po文件里加上对应的汉化,**2.还需要在messages.json中添加对应字段的汉化。**之后在 3.superset文件夹下编译pybabel compile -d translations即可。

另外,在superset目录下编译babel.cfg文件可以生成messages.pot文件,messages.po文件就是根据messages.pot文件生成的。但是要是从新根据pot文件生成需要手动添加的翻译太多,补充需要的翻译到po文件中也是一样的。编译命令:

pybabel extract -F translations\babel.cfg -k _ -k __ -k t -k tn -k tct -o translations\messages.pot .

#前面的一些参数-k _ -k __ -k t -k tn -k tct可能可以省略一些。

编译时有两个文件会报错,需要修改。

C:\Users\user\Desktop\superset相关\incubator-superset36\superset\datasets\commands\exceptions.py
#修改相关的table_name

C:\Users\user\Desktop\superset相关\incubator-superset36\superset\views\core.py
#修改相关的validator

#就是添加_('')或者删除_('')。

0714.还有的汉化
首页dashbord, Creator, MOdified的汉化。
位于superset-frontend/src/welcome/DashboardTable.jsx,修改方法同上。

语言选择的去除。
两个位置:
1.在templates/appbuilder/navbar_right.html中删除或者注释掉44-96行,({% if documentation_url %}…{% endif %},注释方法 ),这个是修改后面页面的,不包括初始页。

2.在superset-frontend/src/components/Menu/Menu.jsx中删除或者注释掉94-99行

#在{}内注释才有用(/*    */)。
          {/*navbarRight.show_language_picker && (
            <LanguagePicker
              locale={navbarRight.locale}
              languages={navbarRight.languages}
            />
          )*/}

Slug, Published, Datasource Name,没有完全解决

  1. Slug, Published,在superset-frontend/src/views/dashboardList/DashboardList.tsx,修改方式同上,记得修改mmessages.json和message.po文件和编译po文件。(修改之后slug有用但是publish没变,可能是找的文件内不对)

  2. Datasource Name在superset-frontend/src/views/chartList/ChartList.tsx,修改方式同上,记得修改mmessages.json和message.po文件和编译po文件。(修改之后Datasource Name没变,可能是找的文件内不对)

注意: 前端文件重新打包,会改变static/assets中的文件,所以修改图标要重新修改。重新换一下图片和添加ehulu.png和xxxx.gif

三. 图标的修改

1.修改config.py文件

修改APP_NAME字段,(是浏览器页面显示的名字)。

修改 FAVICONS = [{“href”: “/static/assets/images/xxxx.png”]

还有 APP_ICON

修改superset/templates/superset下的base.html文件。这个是浏览器栏上的图表,名字和images中的图片名字对应即可 把href="/static/assets/images/favicon.png"的值修改为xxxx.png

修改superset/templates/superset下的basic.html文件。 名字和images中的动态图片名字对应即可 27行src="/static/assets/images/loading.gif"的值改为xxxx.gif

前提是需要在superset/static/assets/images文件夹下面存入xxxx.png和xxxx.gif。

2.添加更换图片。

在superset/static/assets/images目录下。

更换superset-logo@2x.png文件,添加xxxx.png图片。

四,修改权限,发布画好的图表和看板

  • 修改config.py中的PUBLIC_ROLE_LIKE_GAMMA 为 True
  • 在superset首页下的安全角色列表在public中添加
导出图表
can explore on Superset

导出图表json
can explore json on Superset

访问所有数据库权限
all database access on all_database_access

all datasource access on all_datasource_access
  • 在config.py中修改
    • WTF_CSRF_ENABLED = False, 把True变为False
    • WTF_CSRF_EXEMPT_LIST = [“superset.views.core.log”,“endpoints”],加入endpoints。

五,本地配置VSCODE远程debug调试

  • 安装vscode

  • 在vscode中安装Remote-SSH和Remote Development插件。

  • 链接远程主机时配置config文件,并将config文件写到Remote-SSH的extension settings的config file中

  • 安装python插件。

  • 找到程序的入口(是bin目录下的superset),F5以debug的模式运行。

  • 第一次debug运行需要配置lanuch.json文件(位于隐藏文件夹.vscode下) (可以在vscode的run 下create a launch.json file)

  • 配置文件config 地址:C:\Users\user.ssh

  • 报错< Host key verification failed. > 过程试图写入的管道不存在。> 删除上面路径下的known_hosts原服务器信息全部删掉(根据IP地址删除)

# config 文件

Host superset_dev
  HostName 10.2.111.193
  User root

修改 lanuch.json Flask_app 为 superset 和添加参数

#lanuch.json的配置
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Flask",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "superset",
                "FLASK_ENV": "development",
                "FLASK_DEBUG": "0"
            },
            "args": [
                "run",
                "--debugger",
                "--reload",
                "-h",
                "0.0.0.0",
                "-p",
                "8088"
            ],
            "jinja": true
        }
    ]
}

六,加载官方例子

  • superset load_examples 加载官方例子的时候因为官方例子网站的挂掉了,所以不能够加载成功。
  • 在github上下载官方例子文件 网址:https://github.com/apache-superset/examples-data
  • npm安装http-server
npm install http-server

#为http-server创建软连接,根据实际地址创建

ln -s /usr/local/node/bin/http-server /usr/bin/http-server
  • 在examples-data所在的文件下下开启服务,即:http-server
  • 修改superset/examples/helpers.py文件,
#根据实际网址和地址填写

BASE_URL = "http://10.2.111.193:8080/examples-data-master/"
  • 最后在执行superset load_examples即可

七. 上传excel

  • 修改 config.py 文件
#在ALLOWED_EXTENSIONS中添加xls和xlsx
ALLOWED_EXTENSIONS = {"csv", "tsv","xls","xlsx"}
  • 修改superset\db_engine_specs\base.py文件

    • 仿照csv_to_df函数添加excel_to_df函数

      @staticmethod
      def excel_to_df(**kwargs: Any) -> pd.DataFrame:
          """ Read excel into Pandas DataFrame
          :param kwargs: params to be passed to DataFrame.read_excel
          :return: Pandas DataFrame containing data from excel
          """
          # kwargs["encoding"] = "utf-8"
          # kwargs["iterator"] = True
          df = pd.read_excel(**kwargs)
          # df = pd.concat(chunk for chunk in chunks)
          return df
      
    • 在create_table_from_csv函数中修改
      *

# 原文件477-491修改为:

 csv_or_excel = os.path.splitext(filename)[1].lower()[1:]

        if csv_or_excel == "csv":
              csv_to_df_kwargs = {
                "filepath_or_buffer": filename,
                "sep": form.sep.data,
                "header": form.header.data if form.header.data else 0,
                "index_col": form.index_col.data,
                "mangle_dupe_cols": form.mangle_dupe_cols.data,
                "skipinitialspace": form.skipinitialspace.data,
                "skiprows": form.skiprows.data,
                "nrows": form.nrows.data,
                "skip_blank_lines": form.skip_blank_lines.data,
                "parse_dates": form.parse_dates.data,
                "infer_datetime_format": form.infer_datetime_format.data,
                "chunksize": 10000,
            }
            df = cls.csv_to_df(**csv_to_df_kwargs)
        
        if csv_or_excel == "xlsx" or csv_or_excel == "xls":
            excel_to_df_kwargs = {
                "io": filename,
                # "sep": form.sep.data,
                "header": form.header.data if form.header.data else 0,
                "index_col": form.index_col.data,
                # "mangle_dupe_cols": form.mangle_dupe_cols.data,
                # "skipinitialspace": form.skipinitialspace.data,
                "skiprows": form.skiprows.data,
                # "nrows": form.nrows.data,
                # "skip_blank_lines": form.skip_blank_lines.data,
                "date_parser": form.parse_dates.data,
                # "infer_datetime_format": form.infer_datetime_format.data,
                # "chunksize": 10000,
            }
            df = cls.excel_to_df(**excel_to_df_kwargs)
  • 在安装环境中安装python的xlrd包 pip install xlrd
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值