Qt——实现信号分析界面


前言

基于VS2019、Qt5.15.5,纯代码实现信号读取与分析界面。


效果

QT——信号读取与分析

步骤

利用信号的发生时间的唯一性,根据时间来找到以时间命名的文件,读取文件,画出时域图,在进行FFT,获取频域图。

一、报警信号的创建

1. 思路

首先,使用定时器产生报警数据,报警数据是自定义的结构。其次,创建表格,设置表格的上下文菜单策略为自定义。最后,创建动作,连接信号与动作。

2. 代码

2.1 alarmMessageTab.h
#pragma once

#include "ui_alarmMessageTab.h"
#include "qtCommon.h"
#include "alarmSignalWin.h"

class alarmMessageTab : public QWidget
{
    Q_OBJECT

public:
    alarmMessageTab(QWidget *parent = nullptr);
    ~alarmMessageTab();

    QList<AlarmPoint> alarmPoints;
    QTimer* timer = nullptr;
    QTableWidget* tableWidget = nullptr; // Declare QTableWidget as a private member variable
    QMenu* popMenu = nullptr; //右键菜单
    QAction* actionCheckSignalInfo = nullptr;

private:
    Ui::alarmMessageTabClass ui;
    int timerCount = 0;; // 记录定时器触发次数
private slots:
    void generateAlarmPoint();	//产生报警数据
    void slotContextMenu(QPoint pos);//右键菜单响应函数
    void showSelectedAlarmInfo(int row);//展示所选择行数的信号
};

2.2 alarmMessageTab.cpp
#include "alarmMessageTab.h"

alarmMessageTab::alarmMessageTab(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    this->resize(1200, 600);
    
    // Initialize the timer for generating alarm points
    timer = new QTimer(this);
    //connect(timer, SIGNAL(timeout()), this, SLOT(generateAlarmPoint()));
    connect(timer, &QTimer::timeout, this, &alarmMessageTab::generateAlarmPoint);
    timer->start(2000); // Generate alarm point every second

    // Create and set up the table widget
    tableWidget = new QTableWidget(this);
    tableWidget->setColumnCount(4); // 4 columns for time, location, type, channel
    QStringList headers;
    headers << "Time" << "Location" << "Type" << "Channel";
    tableWidget->setHorizontalHeaderLabels(headers);
    tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // Stretch columns to fill the table
    tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // Disable editing of table cells
    
    //设置tableWidget的上下文菜单策略为自定义。这意味着我们将会自己创建一个上下文菜单,并在需要时显示它。
    tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);

    popMenu = new QMenu(tableWidget);//创建一个新的上下文菜单对象popMenu,并将其与tableWidget关联。
    actionCheckSignalInfo = new QAction();
    actionCheckSignalInfo->setText(QString::fromLocal8Bit("查看扰动信号"));
    popMenu->addAction(actionCheckSignalInfo);
    
    connect(tableWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotContextMenu(QPoint)));
    connect(actionCheckSignalInfo, &QAction::triggered, this, [this]() {
        int row = tableWidget->currentRow();
        showSelectedAlarmInfo(row);
        });
        
    QVBoxLayout* layout = new QVBoxLayout(this);
    layout->addWidget(tableWidget);
    setLayout(layout);
}

alarmMessageTab::~alarmMessageTab()
{
    delete timer;
    timer = nullptr;

    delete actionCheckSignalInfo;
    actionCheckSignalInfo = nullptr;
    delete popMenu;
    popMenu = nullptr;

    delete tableWidget;
    tableWidget = nullptr;
   
}
void alarmMessageTab::generateAlarmPoint() {
   
    AlarmPoint newAlarm;
    //这里时间格式是为了在表格中显示好看,下面有个转换,是方便存储和读取文件,因为文件命名不能有冒号
    newAlarm.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    newAlarm.location = 110.1;
    newAlarm.type = 2;
    newAlarm.channel = 3;

    alarmPoints.prepend(newAlarm);

    // Update the UI to display the alarm points
    tableWidget->setRowCount(alarmPoints.size());
    for (int i = 0; i < alarmPoints.size(); ++i) {
        const AlarmPoint& alarm = alarmPoints[i];
        QTableWidgetItem* itemTime = new QTableWidgetItem(alarm.time);
        QTableWidgetItem* itemLocation = new QTableWidgetItem(QString::number(alarm.location));
        QTableWidgetItem* itemType = new QTableWidgetItem(QString::number(alarm.type));
        QTableWidgetItem* itemChannel = new QTableWidgetItem(QString::number(alarm.channel));

        tableWidget->setItem(i, 0, itemTime);
        tableWidget->setItem(i, 1, itemLocation);
        tableWidget->setItem(i, 2, itemType);
        tableWidget->setItem(i, 3, itemChannel);
    }
    // 更新定时器触发次数
    ++timerCount;

    // 如果触发次数达到2次,则关闭定时器.也就是产生2条数据
    if (timerCount >= 2) {
        timer->stop();
    }
}
void alarmMessageTab::slotContextMenu(QPoint pos)//这个槽函数是为了显示右键菜单
{
    //根据传入的鼠标点击位置 pos 在 tableWidget 上获取对应的索引 index。这个索引表示了鼠标点击位置所在的表格行列。
    auto index = tableWidget->indexAt(pos);
    //检查获取到的索引是否有效。如果索引有效,表示鼠标点击位置在表格内部,可以显示右键菜单。
    if (index.isValid())
    {
        //调用 popMenu 的 exec 函数来显示上下文菜单。QCursor::pos() 返回当前鼠标光标的全局位置,作为菜单出现的位置。
        //这意味着右键菜单将显示在鼠标当前位置附近。
        popMenu->exec(QCursor::pos()); 
    }
}

void alarmMessageTab::showSelectedAlarmInfo(int row)
{
    if (row >= 0 && row < alarmPoints.size())
    {
        AlarmPoint& selectedAlarm = alarmPoints[row];
        // 下面这句代码,可以用在实时程序,本人已经试过。
        //QString Time = QDateTime::fromString(selectedAlarm.time, "yyyy-MM-dd hh:mm:ss").toString("yyyy-MM-dd_hh-mm-ss"); // 将原始时间戳转换为适合作为文件名的格式
        //由于没有实时产生的数据,自己找了一段数据,命名为:2024-03-07_08-45-54
        QString Time = "2024-03-07_08-45-54";
       
        // 这里是不要创建alarmViewer为 成员变量
        alarmSignalWin* alarmViewer = new alarmSignalWin(Time);//这是另一个界面类,下面分析
        if (!alarmViewer)
        {
            // 调试信息:确认 alarmViewer 对象创建失败
            //qDebug() << "alarmViewer create failed!";
            return;
        }

        // 调试信息:确认 alarmViewer 对象成功创建
        //qDebug() << "alarmViewer create successful!";
        
        alarmViewer->setAttribute(Qt::WA_DeleteOnClose);//界面关闭时,自动销毁对象
        alarmViewer->show(); // 显示 AlarmViewer 界面

        
        //下面connect 一方面是为了检查是否销毁。另一方面也可以做其他处理,我暂时还没有其他处理。
        connect(alarmViewer, &alarmSignalWin::destroyed, this, [&]() {
            //qDebug() << "alarmViewer destroyed!";
			//我本来是想在打开界面后,对应行关掉右键菜单功能,界面销毁销毁后对应行恢复右键菜单功能,可惜未实现。
            });
    }
}

二、报警信号的展示

1. 思路

根据传入的时间参数,找到文件,读取文件,做FFT,显示时域和频域图。

2. 代码

2.1 alarmSignalWin.h
#pragma once

#include "ui_alarmSignalWin.h"
#include <qtCommon.h>

class alarmSignalWin : public QWidget
{
	Q_OBJECT

public:
	alarmSignalWin(QString& time, QWidget *parent = nullptr);
	~alarmSignalWin();

	void showAlarmInfo(QString& time);//初始化显示

	// 创建布局管理器
	QVBoxLayout* mainLayout = nullptr;
	
	QTableWidget* tableWidget = nullptr; 
	QHBoxLayout* plotLayout = nullptr;

	QCustomPlot* m_time_plot = nullptr;
	QVBoxLayout* freqVLayout = nullptr;

	QCustomPlot* m_freq_plot = nullptr;
	QHBoxLayout* controlHLayout = nullptr;

	QLabel* rangeLabel = nullptr;
	QLineEdit * rangeInput1 = nullptr;
	QLabel * dashLabel = nullptr;
	QLineEdit * rangeInput2 = nullptr;
	QLabel * unitLabel = nullptr;
	QPushButton * confirmButton = nullptr;

	MatrixXd_RowMajor* file_data = nullptr;//获取的文件数据
	MatrixXd_RowMajor* fft_data = nullptr;//文件数据后的FFT数据
	MatrixXd_RowMajor* freq_data = nullptr;//频率段数据
	void plotTimeGraph(QCustomPlot* m_plot,MatrixXd_RowMajor* file_data);//画时域图函数
	void plotFreqGraph(QCustomPlot* m_plot, MatrixXd_RowMajor* fft_data, MatrixXd_RowMajor* freq_data, double range1, double range2);//画频域图
	void cal_fft_Matrix(MatrixXd_RowMajor* input_data, MatrixXd_RowMajor* output_data);//fft变换

public slots:
	//void on_confirmButton_clicked();//这里需要注意,如果自定义的按钮等控件,这种写法,调试运行时,控制台会有警告,但是程序可运行。
	void confirmButtonclicked();//为了不想有警告,自己写槽函数函数
private:
	Ui::alarmSignalWinClass ui;
};

2.2 alarmSignalWin.cpp
#include "alarmSignalWin.h"

alarmSignalWin::alarmSignalWin(QString& time, QWidget *parent)
	: QWidget(parent)
{
	ui.setupUi(this);
	// 调整整体界面大小
	this->resize(1400, 650);
	
	tableWidget = new QTableWidget(this);//后期将一些信号分析的结果放在表格
	tableWidget->setColumnCount(4); // 4 columns for time, location, type, channel
	tableWidget->setRowCount(1);
	QStringList headers;
	headers << QString::fromLocal8Bit( "最高幅值(时域)") << 
		QString::fromLocal8Bit("最高能量(所属频带)") << 
		QString::fromLocal8Bit("峰值频率及其幅值大小") << 
		QString::fromLocal8Bit("重心频率");
	tableWidget->setHorizontalHeaderLabels(headers);
	tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // Stretch columns to fill the table
	tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // Disable editing of table cells
	// Set the title of the tableWidget to "特征"
	tableWidget->setWindowTitle(QString::fromLocal8Bit("特征"));
	// 隐藏表格的行标
	tableWidget->verticalHeader()->setVisible(false);
	// 设置表格固定大小
	tableWidget->setFixedHeight(80);

	m_time_plot = new QCustomPlot();
	m_freq_plot = new QCustomPlot();
	//m_time_plot->yAxis->setRange(-5, 5);//设置Y轴范围
	m_time_plot->xAxis->setLabel("Time (s)");
	m_time_plot->yAxis->setLabel("Amplitude");
	m_time_plot->plotLayout()->insertRow(0); // 插入一行用于标题
	m_time_plot->plotLayout()->addElement(0, 0, new QCPTextElement(m_time_plot, "Time Domain", QFont("sans", 12, QFont::Bold)));

	m_freq_plot->xAxis->setLabel("Frequency (Hz)");
	m_freq_plot->yAxis->setLabel("Amplitude");
	m_freq_plot->plotLayout()->insertRow(0); // 插入一行用于标题
	m_freq_plot->plotLayout()->addElement(0, 0, new QCPTextElement(m_freq_plot, "Frequency Spectrum", QFont("sans", 12, QFont::Bold)));

	file_data = new MatrixXd_RowMajor(1, 110000);//设置file_data 长度
	
	// 添加选择频率范围的标签、输入框、按钮
	rangeLabel = new QLabel(QString::fromLocal8Bit("选择频率范围:"));
	rangeInput1 = new QLineEdit("0");
	dashLabel = new QLabel(" - ");
	rangeInput2 = new QLineEdit("120");
	unitLabel = new QLabel("Hz");
	confirmButton = new QPushButton(QString::fromLocal8Bit("确定"));

	controlHLayout = new QHBoxLayout();
	controlHLayout->addWidget(rangeLabel);
	controlHLayout->addWidget(rangeInput1);
	controlHLayout->addWidget(dashLabel);
	controlHLayout->addWidget(rangeInput2);
	controlHLayout->addWidget(unitLabel);
	controlHLayout->addWidget(confirmButton);

	freqVLayout = new QVBoxLayout();
	freqVLayout->addLayout(controlHLayout);
	freqVLayout->addWidget(m_freq_plot);

	plotLayout = new QHBoxLayout();
	// 将两个图形部件添加到水平布局中
	plotLayout->addWidget(m_time_plot, 1); // 设置拉伸因子为1,使左侧部件可以拉伸
	plotLayout->addLayout(freqVLayout, 1); // 设置拉伸因子为1,使右侧部件可以拉伸
	

	// 创建布局管理器
	mainLayout = new QVBoxLayout(this);
	// 将表格添加到主布局中
	mainLayout->addWidget(tableWidget);
	// 将水平布局添加到主布局中
	mainLayout->addLayout(plotLayout);
	// 设置主布局
	setLayout(mainLayout);
	
	connect(confirmButton, &QPushButton::clicked, this, &alarmSignalWin::confirmButtonclicked);

	showAlarmInfo(time);
}

alarmSignalWin::~alarmSignalWin()
{
	//析构函数也得注意,释放的顺序好像有讲究,先释放单个的,在组合的,大概是这样吧
	delete freq_data;
	freq_data = nullptr;
	delete fft_data;
	fft_data = nullptr;
	delete file_data;
	file_data = nullptr;

	delete rangeLabel;
	rangeLabel = nullptr;
	delete rangeInput1;
	rangeInput1 = nullptr;
	delete rangeInput2;
	rangeInput2 = nullptr;
	delete dashLabel;
	dashLabel = nullptr;
	delete confirmButton;
	confirmButton = nullptr;

	delete controlHLayout;
	controlHLayout = nullptr;
	delete m_freq_plot;
	m_freq_plot = nullptr;

	delete freqVLayout;
	freqVLayout = nullptr;
	delete m_time_plot;
	m_time_plot = nullptr;
	
	delete tableWidget;
	tableWidget = nullptr;
	delete plotLayout;
	plotLayout = nullptr;

	delete mainLayout;
	mainLayout = nullptr;

}

void alarmSignalWin::showAlarmInfo(QString& time)
{
	//QString filename = "E:/code_fiOTDR/other_code/test_matlab/    .dat";
	// 根据时间参数拼接文件名!!这里是关键
	QString filename = "C:/code_fiOTDR/1/" + time + ".dat";
	qDebug() << "filenameTime = " << filename;
	QFile file(filename);   //以用户给定的文件名创建文件设备file
	if (file.open(QIODevice::ReadOnly)) //若设备只写打开成功
	{
		QDataStream stream(&file);
		stream.setByteOrder(QDataStream::LittleEndian);  // 设置字节顺序为小端序
		int numCols = file_data->cols();//110000  file_data->cols()
		int colIndex = 0;
		while (!stream.atEnd() && colIndex < numCols)
		{
			double value;
			stream >> value;
			(*file_data)(0, colIndex) = value;
			colIndex++;
		}
		file.close();
		// 裁剪 Input_data 到实际读取的数据长度(40000)
		file_data->conservativeResize(1, colIndex);
		//std::cout << "列数 = " << colIndex << std::endl;
		
		//画时域图
		plotTimeGraph(m_time_plot,file_data);
		
		//画频域图
		// 设置采样频率和采样时间【后期需要读取 配置脉冲周期】,我的数据是这个采样率
		double T = 100e-6;
		double fs = 1 / T;

		int N_test = file_data->cols();					//原始数据长度
		int fft_num = floor(file_data->cols() / 2) + 1;	//fft长度

		//分配fft_data内存
		fft_data = new MatrixXd_RowMajor(1, fft_num);
		fft_data->setZero();
		
		//傅里叶变换
		cal_fft_Matrix(file_data, fft_data);

		//分配freq_data内存
		freq_data = new MatrixXd_RowMajor(1, fft_num);
		freq_data->setZero();

		//这里多说一句,fft后,想找到需要的频率是需要计算的
		// 计算频率轴,为了找到频率对应的索引值
		for (int i = 0; i < fft_num; i++) {
			(*freq_data)(0, i) = i * (fs / N_test);
		}
		
		//获取初始设置的频率范围,并画图
		double range1 = rangeInput1->text().toDouble();
		double range2 = rangeInput2->text().toDouble();
		plotFreqGraph(m_freq_plot, fft_data, freq_data, range1, range2);
	}
}

void alarmSignalWin::plotTimeGraph(QCustomPlot* m_plot,MatrixXd_RowMajor* file_data)
{
	// 添加图层
	m_plot->addGraph();
	// 获取画图数据
	QVector<double> x(file_data->cols()), y(file_data->cols());
	for (int k = 0; k < file_data->cols(); ++k)
	{
		y[k] = (*file_data)(0, k);
		x[k] = k;
	}
	//qDebug() << "number of x[i]:" << x.size();
	m_plot->graph()->setData(x, y);

	// 设置Y轴范围
	m_plot->xAxis->setRange(0, file_data->cols());
	
	// 获取最大值和最小值 为了设置Y轴范围
	double max_val = file_data->maxCoeff();
	double min_val = file_data->minCoeff();
	m_plot->yAxis->setRange(min_val, max_val);

	//画图
	m_plot->replot();
}

void alarmSignalWin::plotFreqGraph(QCustomPlot* m_plot, MatrixXd_RowMajor* fft_data, MatrixXd_RowMajor* freq_data, double range1, double range2)
{
	//找到 range1和range2对应的索引
	// 找到第一个大于等于range1的值的索引
	int range1_index = -1;
	for (int i = 0; i < fft_data->cols(); i++) {
		if ((*freq_data)(0, i) >= range1) {
			range1_index = i;
			break;
		}
	}
	if (range1_index != -1) {
		std::cout << "第一个大于等于range1的值的索引为: " << range1_index << std::endl;
	}
	else {
		std::cout << "未找到大于等于range1的值" << std::endl;
	}

	// 找到第一个大于range2的值的索引,然后取前一个,就是最大的小于range2的值的索引
	int range2_index = -1;
	for (int i = 0; i < fft_data->cols(); i++) {
		if ((*freq_data)(0, i) > range2) {
			range2_index = i - 1;
			break;
		}
	}
	if (range2_index != -1) {
		std::cout << "第一个大于range2的值的索引为: " << range2_index << std::endl;
	}
	else {
		std::cout << "未找到大于range2的值" << std::endl;
	}

	//利用qcustomplot 画出range1_index和range2_index 的test_fft的图,横轴为单位Hz
	//清空原来的图层
	m_plot->clearGraphs();
	// 添加图层
	m_plot->addGraph();

	// 设置数据
	QVector<double> xData, yData;
	for (int i = range1_index; i <= range2_index; i++) {
		xData.push_back((*freq_data)(0, i));
		yData.push_back((*fft_data)(0, i));
	}
	// 添加数据到图层
	m_plot->graph()->setData(xData, yData);

	// 设置横纵坐标轴的范围
	m_plot->xAxis->setRange(range1, range2); // 设置横坐标轴范围为 range1 到 range2
	// 获取最大值和最小值
	double max_val = fft_data->maxCoeff();
	m_plot->yAxis->setRange(0, max_val);

	// 显示图表
	m_plot->replot();
}
void alarmSignalWin::confirmButtonclicked() {
	double range1 = rangeInput1->text().toDouble();
	double range2 = rangeInput2->text().toDouble();
	plotFreqGraph(m_freq_plot, fft_data, freq_data,range1, range2);
}

void alarmSignalWin::cal_fft_Matrix(MatrixXd_RowMajor* input_data, MatrixXd_RowMajor* output_data)
{
	int fft_num = input_data->cols();//表示输入数据的列数,即要进行FFT的数据点个数
	double* fft_Data = new double[input_data->cols()];//创建了一个大小为input_data->cols()的double类型的数组fft_Data
	memcpy(fft_Data, input_data->data(), sizeof(double) * fft_num);//并使用memcpy()函数将input_data中的数据复制到fft_Data数组中
	int num = floor((double)input_data->cols() / 2) + 1;
	fftw_complex* out_cpx;//用于存储FFT结果的复数形式,FFTW_DEFINE_API(FFTW_MANGLE_DOUBLE, double, fftw_complex)  fftw3.h文件中
	fftw_plan fft;
	//为什么是(floor((double)input_data->cols() / 2) + 1)?利用FFT的对称性质,只取一半的频谱(正频率部分或负频率部分),从而将结果表示为实数形式
	//通过调用fftw_malloc()函数分配了足够的内存空间,大小为(floor((double)input_data->cols() / 2) + 1)个fftw_complex结构体
	out_cpx = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * (num));
	fft = fftw_plan_dft_r2c_1d(fft_num, fft_Data, out_cpx, FFTW_ESTIMATE);//FFTW_MEASURE调用fftw_plan_dft_r2c_1d()函数创建一个一维实数到复数的FFT计划,FFTW_ESTIMATE表示使用估算方法来优化计算
	fftw_execute(fft);//调用fftw_execute(fft)函数执行FFT计算

	//计算每个频率分量的【幅值】,并除以输入数据的列数,然后将结果存储到output_data中的第1行
	for (int i = 0; i < (num); i++)
	{
		(*output_data)(0, i) = std::sqrt(out_cpx[i][0] * out_cpx[i][0] + out_cpx[i][1] * out_cpx[i][1]) ;//取模
	}

	fftw_destroy_plan(fft);
	fftw_free(out_cpx);
	delete[] fft_Data;
}

三、主函数和解决方案资源管理器

#include "alarmMessageTab.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    alarmMessageTab w;
    w.show();
    return a.exec();
}


在这里插入图片描述


其他设置

为了代码能跑起来,下面是我的一些设置,请参考。

qcustomplot设置

  1. 先将代码qcustomplot.h 和 qcustomplot.cpp 加入到项目中。

  2. 头文件 qtCommon.h 去包含。

  3. 将箭头所指的模块加进去。
    在这里插入图片描述

  4. 包含qcustomplot.h 和 qcustomplot.cpp所在文件夹的路径
    在这里插入图片描述

fftw3设置

  1. 和上图一样将 fftw3 文件夹所在路径包含
  2. 头文件 qtCommon.h 去包含。
  3. 附加依赖项设置
    在这里插入图片描述

Eigen设置

  1. 和上上面的图一样 包含eigen-3.4.0 文件夹路径
  2. 头文件 qtCommon.h 去包含。

调试设置

  1. 为了看qDebug信息,设置控制台输出
    在这里插入图片描述

代码和数据下载

我已经将我的代码上传,免费下载!支持开源!!第一次上传资源,本来就想免费的,可是要等待审核。
先放百度网盘的链接,有库和数据文件方便测试,自取哈!
链接:https://pan.baidu.com/s/1ijsoxiR65UBt-3DNb8WZDw?pwd=yyds
提取码:yyds
–来自百度网盘超级会员V6的分享

总结

敲代码的过程遇到的问题:

  1. 信号的显示界面应该怎么设计?
    代码说了,为了动态的展示界面,尽量不要将对象设置成员变量。
  2. 画图的处理
    更改频率范围后,为了让信号更快的呈现,FFT不要每次都做一遍。图的显示,没有设置X和Y的范围不会显示数据。图的标题等一些固定不变的属性,在构造函数内先设置好。点击确定按钮后,重新画图,要删除原来的画布。

简单技巧:

  1. 将需要用的共用的头文件,宏定义等放在一个头文件。

Go on!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值