Axios 详解速览

Axios 详解速览

1. Axios 概述

Axios 是一个基于 Promise 的 HTTP 客户端,可以用在浏览器和 Node.js 中。它提供了简单易用的 API 来发送 HTTP 请求。

核心特性:

  • 基于 Promise:支持 async/await
  • 跨平台:浏览器和 Node.js 都支持
  • 拦截器:请求和响应拦截
  • 转换器:请求和响应数据转换
  • 取消请求:支持请求取消
  • 自动转换:JSON 数据自动转换
  • 客户端支持:支持 XSRF 保护

2. 基础安装和配置

2.1 安装

# npm
npm install axios

# yarn
yarn add axios

# CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

2.2 基本使用

// 引入 axios
import axios from 'axios';

// GET 请求
axios.get('/api/users')
    .then(response => {
        console.log(response.data);
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

// POST 请求
axios.post('/api/users', {
    name: 'John Doe',
    email: 'john@example.com'
})
.then(response => {
    console.log('创建成功:', response.data);
})
.catch(error => {
    console.error('创建失败:', error);
});

3. 请求方法详解

3.1 基本请求方法

// GET 请求
axios.get('/api/users')
    .then(response => console.log(response.data));

// 带参数的 GET 请求
axios.get('/api/users', {
    params: {
        page: 1,
        limit: 10
    }
});

// POST 请求
axios.post('/api/users', {
    name: 'John',
    email: 'john@example.com'
});

// PUT 请求
axios.put('/api/users/1', {
    name: 'John Updated',
    email: 'john.updated@example.com'
});

// DELETE 请求
axios.delete('/api/users/1');

// PATCH 请求
axios.patch('/api/users/1', {
    name: 'John Partial Update'
});

3.2 通用请求方法

// axios(config)
axios({
    method: 'get',
    url: '/api/users',
    params: { id: 123 }
});

// axios(url[, config])
axios('/api/users', {
    method: 'post',
    data: { name: 'John' }
});

// 完整配置示例
axios({
    method: 'post',
    url: '/api/users',
    data: {
        firstName: 'John',
        lastName: 'Doe'
    },
    params: {
        timestamp: Date.now()
    },
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    },
    timeout: 5000,
    responseType: 'json'
});

4. 请求配置详解

4.1 完整配置选项

const config = {
    // `url` 是用于请求的服务器 URL
    url: '/api/users',

    // `method` 是创建请求时使用的方法
    method: 'get', // default

    // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
    baseURL: 'https://api.example.com',

    // `transformRequest` 允许在向服务器发送前,修改请求数据
    transformRequest: [function (data, headers) {
        // 对 data 进行任意转换处理
        return data;
    }],

    // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
    transformResponse: [function (data) {
        // 对 data 进行任意转换处理
        return data;
    }],

    // `headers` 是即将被发送的自定义请求头
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    },

    // `params` 是即将与请求一起发送的 URL 参数
    params: {
        ID: 12345
    },

    // `paramsSerializer` 是一个负责 `params` 序列化的函数
    paramsSerializer: function (params) {
        return Qs.stringify(params, {arrayFormat: 'brackets'})
    },

    // `data` 是作为请求主体被发送的数据
    data: {
        firstName: 'John',
        lastName: 'Doe'
    },

    // `timeout` 指定请求超时的毫秒数
    timeout: 1000, // default is `0` (no timeout)

    // `withCredentials` 表示跨域请求时是否需要使用凭证
    withCredentials: false, // default

    // `adapter` 允许自定义处理请求,以使测试更轻松
    adapter: function (config) {
        /* ... */
    },

    // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
    auth: {
        username: 'janedoe',
        password: 's00pers3cret'
    },

    // `responseType` 表示服务器响应的数据类型
    responseType: 'json', // default

    // `responseEncoding` 表示用于解码响应的编码
    responseEncoding: 'utf8', // default

    // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
    xsrfCookieName: 'XSRF-TOKEN', // default

    // `xsrfHeaderName` 是 xsrf token 的值,被用作 http 头名
    xsrfHeaderName: 'X-XSRF-TOKEN', // default

    // `onUploadProgress` 允许为上传处理进度事件
    onUploadProgress: function (progressEvent) {
        // Do whatever you want with the native progress event
    },

    // `onDownloadProgress` 允许为下载处理进度事件
    onDownloadProgress: function (progressEvent) {
        // Do whatever you want with the native progress event
    },

    // `maxContentLength` 定义允许的响应内容的最大尺寸
    maxContentLength: 2000,

    // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise
    validateStatus: function (status) {
        return status >= 200 && status < 300; // default
    },

    // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
    maxRedirects: 5, // default

    // `socketPath` 定义 UNIX Socket 的路径
    socketPath: null, // default

    // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理
    httpAgent: new http.Agent({ keepAlive: true }),
    httpsAgent: new https.Agent({ keepAlive: true }),

    // `proxy` 定义代理服务器的主机名称和端口
    proxy: {
        host: '127.0.0.1',
        port: 9000,
        auth: {
            username: 'mikeymike',
            password: 'rapunz3l'
        }
    },

    // `cancelToken` 指定用于取消请求的 cancel token
    cancelToken: new CancelToken(function (cancel) {
    })
};

axios(config);

5. 响应结构

5.1 响应对象

axios.get('/api/users')
    .then(response => {
        // 响应数据
        console.log(response.data);
        
        // HTTP 状态码
        console.log(response.status); // 200
        
        // HTTP 状态信息
        console.log(response.statusText); // 'OK'
        
        // 响应头
        console.log(response.headers);
        
        // 请求配置
        console.log(response.config);
        
        // 请求对象
        console.log(response.request);
    });

5.2 错误处理

axios.get('/api/users')
    .catch(error => {
        if (error.response) {
            // 请求已发出,但服务器响应的状态码不在 2xx 范围内
            console.log('响应错误:', error.response.data);
            console.log('状态码:', error.response.status);
            console.log('响应头:', error.response.headers);
        } else if (error.request) {
            // 请求已发出,但没有收到响应
            console.log('请求错误:', error.request);
        } else {
            // 其他错误
            console.log('错误:', error.message);
        }
        console.log('配置:', error.config);
    });

6. 拦截器

6.1 请求拦截器

// 添加请求拦截器
axios.interceptors.request.use(
    function (config) {
        // 在发送请求之前做些什么
        console.log('发送请求:', config);
        
        // 添加认证 token
        const token = localStorage.getItem('token');
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        
        // 添加时间戳
        config.params = {
            ...config.params,
            _t: Date.now()
        };
        
        return config;
    },
    function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

// 移除拦截器
const myInterceptor = axios.interceptors.request.use(() => {});
axios.interceptors.request.eject(myInterceptor);

6.2 响应拦截器

// 添加响应拦截器
axios.interceptors.response.use(
    function (response) {
        // 对响应数据做点什么
        console.log('收到响应:', response);
        
        // 统一处理数据格式
        return response.data;
    },
    function (error) {
        // 对响应错误做点什么
        if (error.response) {
            switch (error.response.status) {
                case 401:
                    // 未授权,跳转到登录页
                    window.location.href = '/login';
                    break;
                case 403:
                    // 禁止访问
                    console.error('禁止访问');
                    break;
                case 404:
                    // 资源不存在
                    console.error('资源不存在');
                    break;
                case 500:
                    // 服务器错误
                    console.error('服务器错误');
                    break;
                default:
                    console.error('请求失败');
            }
        }
        return Promise.reject(error);
    }
);

6.3 自定义拦截器实例

// 创建实例并添加拦截器
const apiClient = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 10000
});

// 为实例添加拦截器
apiClient.interceptors.request.use(
    config => {
        // 实例特定的请求处理
        config.headers['X-App-Version'] = '1.0.0';
        return config;
    },
    error => Promise.reject(error)
);

apiClient.interceptors.response.use(
    response => response,
    error => {
        // 实例特定的错误处理
        console.error('API 客户端错误:', error);
        return Promise.reject(error);
    }
);

7. 取消请求

7.1 使用 CancelToken

// 使用 CancelToken.source 工厂方法创建 cancel token
const source = axios.CancelToken.source();

axios.get('/api/users', {
    cancelToken: source.token
}).catch(function (thrown) {
    if (axios.isCancel(thrown)) {
        console.log('请求已取消:', thrown.message);
    } else {
        // 处理错误
        console.error('请求失败:', thrown);
    }
});

// 取消请求
source.cancel('用户取消了请求');

7.2 使用 executor 函数创建 CancelToken

let cancel;

axios.get('/api/users', {
    cancelToken: new axios.CancelToken(function executor(c) {
        // executor 函数接收一个 cancel 函数作为参数
        cancel = c;
    })
});

// 取消请求
cancel('操作被用户取消');

7.3 实际应用:搜索自动完成

class SearchService {
    constructor() {
        this.cancelToken = null;
    }

    async search(query) {
        // 取消之前的请求
        if (this.cancelToken) {
            this.cancelToken.cancel('新的搜索请求');
        }

        // 创建新的 cancel token
        this.cancelToken = axios.CancelToken.source();

        try {
            const response = await axios.get('/api/search', {
                params: { q: query },
                cancelToken: this.cancelToken.token
            });
            return response.data;
        } catch (error) {
            if (axios.isCancel(error)) {
                console.log('搜索请求被取消:', error.message);
                return null;
            }
            throw error;
        }
    }
}

// 使用示例
const searchService = new SearchService();

// 用户输入时调用
document.getElementById('searchInput').addEventListener('input', async (e) => {
    const query = e.target.value;
    if (query.length > 2) {
        const results = await searchService.search(query);
        if (results) {
            displayResults(results);
        }
    }
});

8. 实例创建和配置

8.1 创建实例

// 创建实例
const instance = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: {
        'Content-Type': 'application/json',
        'X-Custom-Header': 'foobar'
    }
});

// 实例方法
instance.get('/users')
    .then(response => console.log(response.data));

instance.post('/users', { name: 'John' })
    .then(response => console.log(response.data));

8.2 实例配置覆盖

// 创建基础实例
const api = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 10000,
    headers: {
        'Content-Type': 'application/json'
    }
});

// 特定请求覆盖配置
api.get('/users', {
    timeout: 5000, // 覆盖实例的超时设置
    headers: {
        'Authorization': 'Bearer token123' // 覆盖实例的头部
    }
});

8.3 多个实例管理

// 不同服务的实例
const userService = axios.create({
    baseURL: 'https://api.user-service.com',
    headers: { 'X-Service': 'user' }
});

const productService = axios.create({
    baseURL: 'https://api.product-service.com',
    headers: { 'X-Service': 'product' }
});

const authService = axios.create({
    baseURL: 'https://api.auth-service.com',
    headers: { 'X-Service': 'auth' }
});

// 使用不同实例
userService.get('/profile');
productService.get('/products');
authService.post('/login', { username, password });

9. 请求和响应转换

9.1 请求转换器

// 全局请求转换器
axios.defaults.transformRequest = [
    function (data, headers) {
        // 处理 FormData
        if (data instanceof FormData) {
            return data;
        }
        
        // 处理普通对象
        if (typeof data === 'object') {
            headers['Content-Type'] = 'application/json';
            return JSON.stringify(data);
        }
        
        return data;
    }
];

// 实例请求转换器
const api = axios.create({
    transformRequest: [
        function (data, headers) {
            // 自定义转换逻辑
            if (data && typeof data === 'object' && !(data instanceof FormData)) {
                return JSON.stringify({
                    ...data,
                    timestamp: Date.now()
                });
            }
            return data;
        }
    ]
});

9.2 响应转换器

// 全局响应转换器
axios.defaults.transformResponse = [
    function (data) {
        // 尝试解析 JSON
        if (typeof data === 'string') {
            try {
                data = JSON.parse(data);
            } catch (e) {
                /* 忽略解析错误 */
            }
        }
        
        // 统一处理数据格式
        if (data && typeof data === 'object' && data.data) {
            return data.data;
        }
        
        return data;
    }
];

// 实例响应转换器
const api = axios.create({
    transformResponse: [
        function (data) {
            // 添加时间戳
            return {
                data: data,
                receivedAt: new Date().toISOString()
            };
        }
    ]
});

10. 文件上传和下载

10.1 文件上传

// 上传单个文件
function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', '文件描述');

    return axios.post('/api/upload', formData, {
        headers: {
            'Content-Type': 'multipart/form-data'
        },
        onUploadProgress: function (progressEvent) {
            // 处理上传进度
            if (progressEvent.lengthComputable) {
                const percentCompleted = Math.round(
                    (progressEvent.loaded * 100) / progressEvent.total
                );
                console.log('上传进度:', percentCompleted + '%');
            }
        }
    });
}

// 上传多个文件
function uploadMultipleFiles(files) {
    const formData = new FormData();
    
    Array.from(files).forEach((file, index) => {
        formData.append(`files[${index}]`, file);
    });

    return axios.post('/api/upload-multiple', formData, {
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    });
}

10.2 文件下载

// 下载文件
function downloadFile(fileId) {
    return axios({
        method: 'get',
        url: `/api/download/${fileId}`,
        responseType: 'blob', // 重要:设置响应类型为 blob
        onDownloadProgress: function (progressEvent) {
            // 处理下载进度
            if (progressEvent.lengthComputable) {
                const percentCompleted = Math.round(
                    (progressEvent.loaded * 100) / progressEvent.total
                );
                console.log('下载进度:', percentCompleted + '%');
            }
        }
    }).then(response => {
        // 创建下载链接
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', 'filename.ext');
        document.body.appendChild(link);
        link.click();
        
        // 清理
        link.remove();
        window.URL.revokeObjectURL(url);
    });
}

// 下载并获取文件信息
async function downloadWithInfo(fileId) {
    try {
        const response = await axios({
            method: 'get',
            url: `/api/download/${fileId}`,
            responseType: 'blob'
        });

        // 从响应头获取文件名
        const contentDisposition = response.headers['content-disposition'];
        let filename = 'downloaded-file';
        if (contentDisposition) {
            const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
            if (filenameMatch && filenameMatch[1]) {
                filename = filenameMatch[1];
            }
        }

        // 下载文件
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', filename);
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.URL.revokeObjectURL(url);

        return { success: true, filename };
    } catch (error) {
        console.error('下载失败:', error);
        return { success: false, error: error.message };
    }
}

11. 并发请求

11.1 Promise.all

// 并发执行多个请求
function fetchUserData(userId) {
    return axios.get(`/api/users/${userId}`);
}

function fetchUserPosts(userId) {
    return axios.get(`/api/users/${userId}/posts`);
}

function fetchUserComments(userId) {
    return axios.get(`/api/users/${userId}/comments`);
}

// 使用 Promise.all
async function fetchUserDetails(userId) {
    try {
        const [userResponse, postsResponse, commentsResponse] = await Promise.all([
            fetchUserData(userId),
            fetchUserPosts(userId),
            fetchUserComments(userId)
        ]);

        return {
            user: userResponse.data,
            posts: postsResponse.data,
            comments: commentsResponse.data
        };
    } catch (error) {
        console.error('获取用户详情失败:', error);
        throw error;
    }
}

11.2 axios.all 和 axios.spread

// 使用 axios.all 和 axios.spread
function fetchAllData() {
    return axios.all([
        axios.get('/api/users'),
        axios.get('/api/posts'),
        axios.get('/api/comments')
    ]).then(axios.spread((users, posts, comments) => {
        return {
            users: users.data,
            posts: posts.data,
            comments: comments.data
        };
    }));
}

12. 实际应用示例

12.1 API 客户端封装

// API 客户端封装
class ApiClient {
    constructor(baseURL, options = {}) {
        this.client = axios.create({
            baseURL,
            timeout: options.timeout || 10000,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });

        this.setupInterceptors();
    }

    setupInterceptors() {
        // 请求拦截器
        this.client.interceptors.request.use(
            config => {
                // 添加认证 token
                const token = this.getToken();
                if (token) {
                    config.headers.Authorization = `Bearer ${token}`;
                }

                // 添加请求 ID 用于追踪
                config.headers['X-Request-ID'] = this.generateRequestId();
                
                return config;
            },
            error => Promise.reject(error)
        );

        // 响应拦截器
        this.client.interceptors.response.use(
            response => {
                // 统一处理响应数据
                return response.data || response;
            },
            error => {
                // 统一错误处理
                this.handleError(error);
                return Promise.reject(error);
            }
        );
    }

    getToken() {
        return localStorage.getItem('authToken');
    }

    generateRequestId() {
        return 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    }

    handleError(error) {
        if (error.response) {
            const { status, data } = error.response;
            
            switch (status) {
                case 401:
                    this.handleUnauthorized();
                    break;
                case 403:
                    console.error('权限不足:', data.message);
                    break;
                case 404:
                    console.error('资源不存在:', data.message);
                    break;
                case 422:
                    console.error('数据验证失败:', data.errors);
                    break;
                case 500:
                    console.error('服务器内部错误');
                    break;
                default:
                    console.error('请求失败:', data.message || '未知错误');
            }
        } else if (error.request) {
            console.error('网络错误,请检查网络连接');
        } else {
            console.error('请求配置错误:', error.message);
        }
    }

    handleUnauthorized() {
        // 清除本地存储的 token
        localStorage.removeItem('authToken');
        // 重定向到登录页
        window.location.href = '/login';
    }

    // 基础方法
    get(url, config = {}) {
        return this.client.get(url, config);
    }

    post(url, data = {}, config = {}) {
        return this.client.post(url, data, config);
    }

    put(url, data = {}, config = {}) {
        return this.client.put(url, data, config);
    }

    delete(url, config = {}) {
        return this.client.delete(url, config);
    }

    patch(url, data = {}, config = {}) {
        return this.client.patch(url, data, config);
    }
}

// 使用示例
const api = new ApiClient('https://api.example.com');

// API 调用
api.get('/users')
    .then(users => console.log(users))
    .catch(error => console.error(error));

api.post('/users', { name: 'John', email: 'john@example.com' })
    .then(user => console.log(user))
    .catch(error => console.error(error));

12.2 带缓存的 API 客户端

// 带缓存的 API 客户端
class CachedApiClient extends ApiClient {
    constructor(baseURL, options = {}) {
        super(baseURL, options);
        this.cache = new Map();
        this.cacheTimeout = options.cacheTimeout || 300000; // 5分钟
    }

    async get(url, config = {}) {
        // 检查是否启用缓存
        if (config.cache !== false) {
            const cacheKey = this.generateCacheKey(url, config);
            const cached = this.cache.get(cacheKey);

            if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
                console.log('从缓存获取数据:', url);
                return cached.data;
            }
        }

        try {
            const response = await this.client.get(url, config);
            
            // 缓存响应数据
            if (config.cache !== false) {
                const cacheKey = this.generateCacheKey(url, config);
                this.cache.set(cacheKey, {
                    data: response,
                    timestamp: Date.now()
                });
            }

            return response;
        } catch (error) {
            // 如果请求失败但有缓存,返回缓存数据
            if (config.cacheFallback && config.cache !== false) {
                const cacheKey = this.generateCacheKey(url, config);
                const cached = this.cache.get(cacheKey);
                if (cached) {
                    console.warn('请求失败,使用缓存数据:', url);
                    return cached.data;
                }
            }
            throw error;
        }
    }

    generateCacheKey(url, config) {
        return url + JSON.stringify(config.params || {});
    }

    clearCache() {
        this.cache.clear();
    }

    removeCache(url) {
        for (const key of this.cache.keys()) {
            if (key.startsWith(url)) {
                this.cache.delete(key);
            }
        }
    }
}

// 使用带缓存的客户端
const cachedApi = new CachedApiClient('https://api.example.com');

// 启用缓存的请求
cachedApi.get('/users', { cache: true });

// 禁用缓存的请求
cachedApi.get('/users', { cache: false });

// 启用缓存且失败时使用缓存的请求
cachedApi.get('/users', { cache: true, cacheFallback: true });

12.3 上传进度组件

// 上传进度组件
class UploadProgress {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.setupUI();
    }

    setupUI() {
        this.container.innerHTML = `
            <div class="upload-area">
                <input type="file" id="fileInput" multiple>
                <div id="progressContainer" style="display: none;">
                    <div class="progress-bar">
                        <div class="progress-fill"></div>
                    </div>
                    <div class="progress-text">0%</div>
                </div>
                <div id="result"></div>
            </div>
        `;

        this.fileInput = this.container.querySelector('#fileInput');
        this.progressContainer = this.container.querySelector('#progressContainer');
        this.progressFill = this.container.querySelector('.progress-fill');
        this.progressText = this.container.querySelector('.progress-text');
        this.result = this.container.querySelector('#result');

        this.fileInput.addEventListener('change', (e) => {
            this.handleFileSelect(e.target.files);
        });
    }

    async handleFileSelect(files) {
        if (files.length === 0) return;

        for (let i = 0; i < files.length; i++) {
            await this.uploadFile(files[i]);
        }
    }

    async uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);

        try {
            this.showProgress();

            const response = await axios.post('/api/upload', formData, {
                headers: {
                    'Content-Type': 'multipart/form-data'
                },
                onUploadProgress: (progressEvent) => {
                    if (progressEvent.lengthComputable) {
                        const percentCompleted = Math.round(
                            (progressEvent.loaded * 100) / progressEvent.total
                        );
                        this.updateProgress(percentCompleted);
                    }
                }
            });

            this.uploadComplete(response.data);
        } catch (error) {
            this.uploadError(error);
        }
    }

    showProgress() {
        this.progressContainer.style.display = 'block';
        this.updateProgress(0);
    }

    updateProgress(percent) {
        this.progressFill.style.width = percent + '%';
        this.progressText.textContent = percent + '%';
    }

    uploadComplete(data) {
        this.updateProgress(100);
        this.result.innerHTML = `<div class="success">上传成功: ${data.filename}</div>`;
        setTimeout(() => {
            this.progressContainer.style.display = 'none';
            this.result.innerHTML = '';
        }, 2000);
    }

    uploadError(error) {
        this.progressContainer.style.display = 'none';
        this.result.innerHTML = `<div class="error">上传失败: ${error.message}</div>`;
        setTimeout(() => {
            this.result.innerHTML = '';
        }, 3000);
    }
}

// 使用上传组件
const uploader = new UploadProgress('uploadContainer');

Axios 是一个功能强大且易用的 HTTP 客户端,通过合理配置和使用,可以大大简化前端的网络请求处理。在实际项目中,建议根据具体需求进行适当的封装和定制。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值