QT控件 重写Qaiv库实现QHeaderView表头多选筛选功能

前言

最近需求遇到了需要实现QHeaderView表头筛选的功能,
这个需求让我想起最开始用QT做数据处理的时候,
那时候菜,
同样的一个Qaiv案例改成QHeaderView标题筛选简直没法看,只能用,都不能说是拆轮子了,纯粹的乱来。
现在时隔几年又重新研究了一下,感觉这次还行,至少还能保留了轮子本身的框架,后面有需求还能再已有的基础上追加。
一番耗时研究,这里写个总结。

关于Qaiv库

关于Qaiv库的作用描述

Qaiv (Qt Advanced Item Views) 是一个专门为 Qt框架 设计的、功能强大的开源扩展库。它的核心使命是极大地增强Qt原生的模型/视图(Model/View)组件(特别是QTableView)的数据处理与交互能力。(出自DeepSeek)

如图示:

其核心作用可以概括为以下三点:

1. 提供高级的筛选与排序功能

  • 类Excel的表头筛选: 这是Qaiv最知名的特性。它允许开发者轻松地为QHeaderView的每一列添加功能完备的筛选器,支持按值列表多选、通配符匹配、正则表达式等多种筛选方式,远超原生QSortFilterProxyModel的功能。
  • 强大的排序机制: 提供了比Qt原生更灵活的多列排序和自定义排序规则。

2. 实现便捷的数据分组与聚合

  • Qaiv可以将表格行按某一列或多列的值进行可视化分组,形成可折叠/展开的树形结构。
    在分组后,它还能自动为每个组计算聚合数据(如求和、平均值、计数等),并将结果显示在组标题行中,非常适合数据汇总和分析报表。

3. 简化复杂视图的开发

  • Qaiv通过提供一系列高度集成且易于使用的代理模型(如CheckableProxyModel, FilterProxyModel, GroupByProxyModel等),将上述复杂功能封装成简单的API。开发者无需从零开始实现这些高级特性,只需将源模型与Qaiv的代理模型链式连接,即可快速构建出功能丰富的专业级数据表格应用,显著提升开发效率。

源码下载

按理说,类似与QHeaderView表头多项筛选的功能应该在数据处理方面应用很多,但实际上类似的开源软件我就只找到这一个,只能说这个库太全面了。
出自:https://sourceforge.net/projects/qadvanceditemviews/
具体下载地址:https://master.dl.sourceforge.net/project/qadvanceditemviews/0.4/qaiv-0.4.0.zip?viasf=1
在GitHub上也有大佬进行修改的版本,可以做个参考
https://github.com/Alexpux/QAdvancedItemViews

编译源码

在这里插入图片描述
Qaiv库使用Qt4开发的,其中QT库的引用都是通过链接器加载,同时添加QtWidgets库的引用,包括头文件。
在这里插入图片描述

也可以使用QT Vs开发工具

  • 修改项目结构
  • 添加QtWidgets模块
  • 移除属性-》链接器中的lib引用
  • 添加mscv编译器

就能直接编译生成了。

在这里插入图片描述
如下图示修改:
在这里插入图片描述
同理,其他的模块也都一样修改。
其中example模块包含了Qaiv库中所有模块的示例,
QHeaderView表头多选筛选只需要 QAbstractFilterProxyModel 模型相关功能,
所以只对此Model进行了修改。

重新QHeaderView标题实现筛选

首先展示下效果:
这GIF示例中,勾选为空没有筛选是因为有些值为NULL,有些为Empty。本身筛选,包括多项筛选是没有问题的。

整个示例相当于将Qaiv库功能阉割了,
只保留了一项多项筛选功能。

重写QHeaderView类

- 完整源码:
QFilterHeaderView.h

#pragma execution_character_set("utf-8")
#include <QHeaderView>
#include <QObject>
#include <QWidget>
#include <QPainter>
#include <QToolBox>
#include <QToolButton>
#include <QAbstractItemModel>
#include <QDebug>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QtCore/QtGlobal>
#include <QPushButton>
#include <QScopedPointer>
#include <QSortFilterProxyModel>


//! 私有类 实现相关功能
class QFilterHeaderViewPrivate;

//! 自定义表格视图
class QFilterHeaderView : public QHeaderView
{
    Q_OBJECT
public:
    QFilterHeaderView(Qt::Orientation orientation, QWidget* parent = nullptr);
    ~QFilterHeaderView();

    //! 重写绘图
    void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const override;

    //! 刷新列
    void updateSection(int logicalIndex);

protected:
    bool event(QEvent* event) override;
    void mousePressEvent(QMouseEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* event) override;
private:
    QScopedPointer<QFilterHeaderViewPrivate> d_ptr;
    Q_DECLARE_PRIVATE(QFilterHeaderView)

};

QFilterHeaderView.cpp:

#include "QFilterHeaderView.h"
#include "qabstractfilterproxymodel.h"
#include <QApplication>
#include <QDesktopWidget>
#include <QFontMetrics>
#include "FontIconTool.h"


class QFilterHeaderViewPrivate
{
    QFilterHeaderView* q_ptr;
    Q_DECLARE_PUBLIC(QFilterHeaderView)
public:
    QFilterHeaderViewPrivate();

    //! 获取每列图标对应位置
    QRect FilterRect(int logicalIndex);

    //! 点击时选中的列
    int selectLogicalIndex = -1;
    bool IsSelectItem = false;
};

///////////////////////////// CustomHeaderViewPrivate /////////////////////////////

QFilterHeaderViewPrivate::QFilterHeaderViewPrivate()
{
    selectLogicalIndex = -1;
    IsSelectItem = false;
}

QRect QFilterHeaderViewPrivate::FilterRect(int logicalIndex)
{
    int pos = q_ptr->sectionViewportPosition(logicalIndex);  // 起始位置(视口坐标)
    int size = q_ptr->sectionSize(logicalIndex);             // 宽度(水平表头)或高度(垂直表头)
    int Position = q_ptr->sectionPosition(logicalIndex);
    QRect rect;
    if (q_ptr->orientation() == Qt::Horizontal) {
        rect = QRect(pos, 0, size, q_ptr->height());
    }
    else {
        rect = QRect(0, pos, q_ptr->width(), size);
    }
    //! 固定筛选字符串
    QString text = QChar(FontIconTool::FontIconId::fa_filter);
    QFont font = q_ptr->font();
    QFontMetrics fm(font);
    //! 字体符号对应外边距
    QRect textRect = fm.boundingRect(text);
    int x = rect.x() + rect.width() - textRect.width() - 10;
    int y = (rect.y() + rect.height() - textRect.height()) / 2;
    //! 扩大 尽量标准
    textRect = QRect(x, y, textRect.width(), textRect.height());
    QRect backrect = textRect.adjusted(-3, -2, 3, 2);
    return backrect;
}



///////////////////////////// CustomHeaderView /////////////////////////////

QFilterHeaderView::QFilterHeaderView(Qt::Orientation orientation, QWidget* parent)
    :QHeaderView(orientation, parent), d_ptr(new QFilterHeaderViewPrivate)
{
    d_ptr->q_ptr = this;

    QFont font = FONTICONTOOL->getFont();
    this->setFont(font);
    show();
}
QFilterHeaderView::~QFilterHeaderView()
{

}

void QFilterHeaderView::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const
{
    QHeaderView::paintSection(painter, rect, logicalIndex);
    painter->restore();

    // 获取 section 的位置和大小
    int pos = sectionViewportPosition(logicalIndex);  // 起始位置(视口坐标)
    int size = sectionSize(logicalIndex);             // 宽度(水平表头)或高度(垂直表头)
    int Position = sectionPosition(logicalIndex);

    QAbstractFilterProxyModel* filtermodel = static_cast<QAbstractFilterProxyModel*>(model());
    if (filtermodel!= nullptr)
    {

        if (filtermodel->IsHaveFilter(logicalIndex))
        {
            //! 范围内
            QString text = QChar(FontIconTool::FontIconId::fa_filter);
            QFontMetrics fm(this->font());
            QRect textRect = fm.boundingRect(text);
            int x = rect.x() + rect.width() - textRect.width() - 10;
            int y = (rect.y() + rect.height() - textRect.height()) / 2;
            textRect = QRect(x, y, textRect.width(), textRect.height());

            QBrush backGround = painter->background();
            if (filtermodel->IsActivateFilter(logicalIndex))
                backGround = QBrush(QColor("#009600"));

            painter->fillRect(textRect, backGround);
            QRect backrect = textRect.adjusted(-3, -2, 3, 2);
            painter->fillRect(backrect, backGround);
            painter->drawRect(backrect);
            painter->drawText(textRect.bottomLeft(), text);
        }
    }
    painter->save();

}


bool QFilterHeaderView::event(QEvent* event)
{
    return QHeaderView::event(event);
}

void QFilterHeaderView::mousePressEvent(QMouseEvent* event)
{
    QHeaderView::mousePressEvent(event);

    QAbstractFilterProxyModel* filtermodel = static_cast<QAbstractFilterProxyModel*>(model());
    if (filtermodel == nullptr)
         return;
    int logicalIndex = logicalIndexAt(event->pos());
    if (logicalIndex >= 0 && filtermodel->IsHaveFilter(logicalIndex)) {
        d_ptr->selectLogicalIndex = logicalIndex;
        if (d_ptr->FilterRect(logicalIndex).contains(event->pos()))
        {
            d_ptr->IsSelectItem = true;
        }
    }
}

void QFilterHeaderView::mouseReleaseEvent(QMouseEvent* event)
{
    QHeaderView::mouseReleaseEvent(event);

    QAbstractFilterProxyModel* filtermodel = static_cast<QAbstractFilterProxyModel*>(model());
    if (filtermodel == nullptr)
        return;
    // 获取点击位置的逻辑索引(第几个section)
    int logicalIndex = logicalIndexAt(event->pos());
    if (logicalIndex >= 0 && filtermodel->IsHaveFilter(logicalIndex)) {
        if (d_ptr->IsSelectItem && d_ptr->selectLogicalIndex == logicalIndex &&
            d_ptr->FilterRect(logicalIndex).contains(event->pos()))
        {
            int pos = sectionViewportPosition(logicalIndex);  // 起始位置(视口坐标)
            //int size = sectionSize(logicalIndex);             // 宽度(水平表头)或高度(垂直表头)
            filtermodel->showPopup(logicalIndex, this->mapToGlobal(QPoint(pos-1, this->height()-1)));
        }
    }
    d_ptr->IsSelectItem = false;
}

void QFilterHeaderView::updateSection(int logicalIndex)
{
    QHeaderView::updateSection(logicalIndex);
}

重写QAbstractFilterProxyModel类

QAbstractFilterProxyModel.h


#include <QObject>
#include <QSize>
#include <QSortFilterProxyModel>

//! 重写筛选类
class QAbstractFilterProxyModelPrivate;
class QAbstractFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
public:
    QAbstractFilterProxyModel(QObject* parent = 0);

    ~QAbstractFilterProxyModel();

    //! 创建一个筛选器
    QWidget* createfilter(QWidget* parent, const QModelIndex index, int type);

    //! 设置数据源
    void setSourceModel(QAbstractItemModel* sourceModel);

    //! 是否有筛选器
    bool IsHaveFilter(int column);

    //! 是否应用筛选
    bool IsActivateFilter(int column);

    //! 弹出筛选窗
    void showPopup(int column, QPoint pos);

    //! 清理数据
    void ClearData();
signals:
    /**
      * 此信号会在滤波电流过滤功能失效之前发出。
      * @see resultChanged()
      */
    void resultAboutToChange();
    /**
      * 此信号是在过滤过程完成后发出的。
      * @see resultChanged(), resultCountChanged()
      */
    void resultChanged();
    /**
      * 每当过滤后的结果集中的行数发生变化时,就会发出此信号。
      */
    void resultCountChanged(int filteredRows, int unfilteredRows);

public slots:
    void updateResult();
protected:
    void emitResultCountChanged();
    bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;

private:
    QScopedPointer<QAbstractFilterProxyModelPrivate> d_ptr;
    Q_DECLARE_PRIVATE(QAbstractFilterProxyModel)
};


#include "qabstractfilter.h"
#include <QDebug>
#include "qautofilter.h"

class QAbstractFilterProxyModelPrivate
{
    QAbstractFilterProxyModel* q_ptr;
    Q_DECLARE_PUBLIC(QAbstractFilterProxyModel)
public:
    QAbstractFilterProxyModelPrivate();
    ~QAbstractFilterProxyModelPrivate();

    //! 筛选组合
    QList<QAbstractFilter*> FilterGroup;
    //! 最后一次数据改变的个数
    int lastResultCount;
};



///////////////////////////// QAbstractFilterProxyModelPrivate /////////////////////////////

QAbstractFilterProxyModelPrivate::QAbstractFilterProxyModelPrivate()
{

}
QAbstractFilterProxyModelPrivate::~QAbstractFilterProxyModelPrivate()
{
}


///////////////////////////// QAbstractFilterProxyModel /////////////////////////////
QAbstractFilterProxyModel::QAbstractFilterProxyModel(QObject* parent) :
    QSortFilterProxyModel(parent), d_ptr(new QAbstractFilterProxyModelPrivate())
{
    d_ptr->q_ptr = this;


}

QAbstractFilterProxyModel::~QAbstractFilterProxyModel()
{

}

void QAbstractFilterProxyModel::setSourceModel(QAbstractItemModel* sourceModel)
{
    QSortFilterProxyModel::setSourceModel(sourceModel);
    for (int i = 0; i < d_ptr->FilterGroup.count(); i++)
        d_ptr->FilterGroup[i]->setModelData(sourceModel );
}

QWidget* QAbstractFilterProxyModel::createfilter(QWidget* parent, const QModelIndex index, int type)
{
    QVariantMap properties;
    properties["column"] = index.column();
    properties["row"] = index.row();
    properties["type"] = QAutoFilter::Type;
    QAbstractFilter* filter = new QAutoFilter(index.row(), index.column());
    d_ptr->FilterGroup.append(filter);
    filter->setEnabled(true);
    return filter->createEditor(parent, this);
}

bool QAbstractFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
    // qDebug()<<"filterAcceptsRow--> "<<source_row;
    bool rr = true;
    int fc = 0;
    int t = -1; //! 简化了类结构,此处无用
    Q_FOREACH(QAbstractFilter * f, d_ptr->FilterGroup) {
        //! 有一个不符合条件就退出
        if (!f->matches(sourceModel()->data(sourceModel()->index(source_row, f->column())), t)) {
            return false;
        }
    }
    return rr;
}


void QAbstractFilterProxyModel::updateResult()
{
    // reset last result count
    d_ptr->lastResultCount = -1;
    emit resultAboutToChange();
    // invalidate filter
    qDebug() << "invalidateFilter -->start;";
    invalidateFilter();
    qDebug() << "invalidateFilter -->end;";
    //
    emit resultChanged();
    // emit result count changed
    emitResultCountChanged();
}

void QAbstractFilterProxyModel::emitResultCountChanged()
{
    if (rowCount() != d_ptr->lastResultCount) {
        d_ptr->lastResultCount = rowCount();
        emit resultCountChanged(d_ptr->lastResultCount, sourceModel()->rowCount());
    }
}

bool QAbstractFilterProxyModel::IsHaveFilter(int column)
{
    Q_FOREACH(QAbstractFilter * f, d_ptr->FilterGroup) {
        if (f->column() == column)
            return true;
    }
    return false;
}

void QAbstractFilterProxyModel::showPopup(int column, QPoint pos)
{
    Q_FOREACH(QAbstractFilter * f, d_ptr->FilterGroup) {
        if (f->column() == column)
            f->showPopup(pos);
    }
}

void QAbstractFilterProxyModel::ClearData()
{
   d_ptr->FilterGroup.clear();
   invalidate();
   delete sourceModel();

}


bool QAbstractFilterProxyModel::IsActivateFilter(int column)
{
    Q_FOREACH(QAbstractFilter * f, d_ptr->FilterGroup) {
        if (f->column() == column)
            return f->IsActivateFilter();
    }
    return false;
}

重写QAbstractFilter筛选单项基类

QAbstractFilter.h:

#include <QMap>
#include <QMenu>
#include <QStyleOptionViewItem>
#include <QSortFilterProxyModel>
#include <QVariant>

//! 私有方法实现
class QAbstractFilterPrivate;

//! 筛选基类
class QAbstractFilter
{
public:
    //! 类型
    static const int UserType = 65536;
    enum {
        Type = -1
    };

    QAbstractFilter();
    QAbstractFilter(const QMap<QString, QVariant>& properties);
    ~QAbstractFilter();


    //!-----常用属性-----

    int column() const;
    int row() const;
    QColor highlightColor() const;
    bool isEnabled() const;
    bool isValid() const;
    QVariant property(const QString& key, const QVariant& defaultValue = QVariant()) const;
    QMap<QString, QVariant> properties() const;
    void setHighlightColor(const QColor& color);
    void setEnabled(bool on);
    void setProperty(const QString& name, const QVariant& value);
    int type() const;

    //!-----相关接口-----
    
    //! 创建筛选窗体
    virtual QWidget* createEditor(QWidget* parent, QSortFilterProxyModel* model) = 0;
    //! 弹出窗体
    virtual void showPopup(QPoint pos) = 0;
    //! 返回默认值
    virtual QVariant data(int role = Qt::DisplayRole) const = 0; 
    //! 匹配内容
    virtual bool matches(const QVariant& value, int type = -1) const = 0; 
    //!设置默认选项
    virtual void setEditorData(QVariantList data) = 0; 
    ///设置源模式数据源
    virtual void setModelData(QAbstractItemModel* model) = 0;
    /// 是否有激活筛选项
    virtual bool IsActivateFilter() = 0;

protected:
    QAbstractFilter(int type, int row, int column);
private:
    friend class QAbstractFilterModel;
    void setRow(int row);

    QAbstractFilterPrivate* d;
    

};

重写QAutoFilter类

QAutoFilter.h


#include <QObject>
#include <QScopedPointer>
#include "QAbstractFilter.h"

class QAutoFilterPrivate;

class QAutoFilter : public QAbstractFilter
{
public:
    enum {
        Type = 0
    };

	QAutoFilter(QObject *parent);
	~QAutoFilter();

    QAutoFilter(int row, int column);


    //!-----实现相关接口-----

    QWidget* createEditor(QWidget* parent, QSortFilterProxyModel* model);

    void showPopup(QPoint pos);

    QVariant data(int role = Qt::DisplayRole) const;
    
    bool matches(const QVariant& value, int type) const;

    void setEditorData(QVariantList);

    void setModelData(QAbstractItemModel* model);

    bool IsActivateFilter();
private:
    QScopedPointer<QAutoFilterPrivate> d_ptr;
    Q_DECLARE_PRIVATE(QAutoFilter)
};

QAutoFilter.cpp

#include <QDebug>
#include "qautofilterwidget.h"
#include "qabstractfilterproxymodel.h"



class QAutoFilterPrivate
{
    QAutoFilter* q_ptr;
    Q_DECLARE_PUBLIC(QAutoFilter)
public:
    QAutoFilterPrivate();
    ~QAutoFilterPrivate();

    //! 筛选弹窗
    QAutoFilterWidget* filterWidget = nullptr;

};

///////////////////////////// QAutoFilterPrivate /////////////////////////////

QAutoFilterPrivate::QAutoFilterPrivate()
{


}
QAutoFilterPrivate::~QAutoFilterPrivate()
{
}




///////////////////////////// QAutoFilter /////////////////////////////

QAutoFilter::QAutoFilter(int row, int column) :
    QAbstractFilter(QAutoFilter::Type, row, column), d_ptr(new QAutoFilterPrivate)
{
}

QAutoFilter::~QAutoFilter()
{
}


QWidget* QAutoFilter::createEditor(QWidget* parent, QSortFilterProxyModel* model)
{
    d_ptr->filterWidget = new QAutoFilterWidget(parent);
    QAbstractFilterProxyModel* p = qobject_cast<QAbstractFilterProxyModel*>(model);
    setModelData(p->sourceModel());
    QObject::connect(d_ptr->filterWidget, &QAutoFilterWidget::UpdataEditorData, [&](QVariantList data) {
        setEditorData(data);
        });
    QObject::connect(d_ptr->filterWidget, &QAutoFilterWidget::modeChanged, p, &QAbstractFilterProxyModel::updateResult);
    return d_ptr->filterWidget;
}

void QAutoFilter::showPopup(QPoint pos)
{
    d_ptr->filterWidget->move(pos);
    d_ptr->filterWidget->show();
}

QVariant QAutoFilter::data(int role) const
{
    if (role == Qt::DisplayRole) {
        if (property("mode").toInt() == 0) {
            if (property("selectedValues").toList().isEmpty()) {
                return QObject::tr("<none>");
            }
            else {
                if (property("selectedValues").toList().size() == 1) {
                    return QString(QObject::tr("%1 entry")).arg(property("selectedValues").toList().size());
                }
                else {
                    return QString(QObject::tr("%1 entries")).arg(property("selectedValues").toList().size());
                }
            }
        }
        else if (property("mode").toInt() == 1) {
            return QObject::tr("Empty");
        }
        else if (property("mode").toInt() == 2) {
            return QObject::tr("Not Empty");
        }
    }
    return QVariant();
}

bool QAutoFilter::matches(const QVariant& value, int type) const
{
    Q_UNUSED(type);
    if (property("mode").toInt() == 1) {
        return value.toString().isEmpty();
    }
    else if (property("mode").toInt() == 2) {
        return !value.toString().isEmpty();
    }
    //! 没有勾选默认全部
    if (property("selectedValues").toList().count() == 0)
        return true;
    return property("selectedValues").toList().contains(value);
}

void QAutoFilter::setEditorData(QVariantList data)
{
    setProperty("selectedValues", data);
}

void QAutoFilter::setModelData(QAbstractItemModel* model)
{
    setProperty("selectedValues", QVariantList());
    d_ptr->filterWidget->setSourceModel(model, column());
    d_ptr->filterWidget->setSelectedValues(property("selectedValues").toList());
   
}

bool QAutoFilter::IsActivateFilter()
{
    return property("selectedValues").toList().count() > 0;
}

完整调用:

本身Qaiv库的筛选是多个控件的组合实现,重写了单元的ItemDelegate 委托,实现筛选项的内容编辑,重写了bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) 方法实现了多项筛选条件的判断。
这里我进行了删减,移除了不需要的部分,只保留了基本的调用

    //! 数据源
    //! QStandardItemModel* m_model;
    QAbstractFilterProxyModel* filtermodel = new QAbstractFilterProxyModel();
    filtermodel->setSourceModel(m_model);
    ui->tableView->setModel(filtermodel);
    //添加筛选表头
    QFilterHeaderView* view = new  QFilterHeaderView(Qt::Horizontal, ui->tableView);
    view->setModel(filtermodel);
    //!绑定表标题,隐藏行标题
    ui->tableView->setHorizontalHeader(view);
    ui->tableView->verticalHeader()->hide();
    //! 添加筛选项
    //ui.tableView_Statistics->horizontalHeader()->show();
    for (int i = 0; i <m_model->columnCount(); i++)
    {
        filtermodel->createfilter(view, m_model->index(0, i), (int)QAutoFilter::Type);
    }
    
    connect(filtermodel, &QAbstractFilterProxyModel::resultChanged, this, [&, filtermodel]() {
        qDebug()<<"Filter Update: "<<filtermodel->rowCount();
    });

最近感觉需要点柚子叶去去霉运,毫无精神。
具体的说明懒得写了,请看源码,
功能本身不复杂,只是需要花点时间。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

得鹿梦鱼、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值