xUtils3带进度条下载(切换界面不会中断下载)

本文介绍如何使用xUtils3库实现文件下载进度的通知功能。通过创建自定义的广播接收者来监听下载状态,并在下载过程中更新UI显示进度。文章详细展示了如何注册和注销广播接收者,以及设置下载参数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

xUtils3的下载方法很方便提供了下载进度,首先,我们需要用代码注册一个广播接受者,用于接收下载进度,然后在xUtils3的下载方法里发送进度广播即可。

广播接收者:

private ProgressReceiver progressReceiver;
    /**
     * 用于通知游戏下载进度的广播接收者
     */
    private class ProgressReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            /*if (TextUtils.equals(intent.getAction(), "progress")) {

            }*/
            // 辨别正在下载的游戏是不是当前界面展示的游戏
            String downloadingGameId = intent.getStringExtra("gameId");
            if (TextUtils.equals(gameId, downloadingGameId)) {
                if (mpb_download_progress.getVisibility() != View.VISIBLE) {
                    mpb_download_progress.setVisibility(View.VISIBLE);
                }

                // 进度
                int progress = intent.getIntExtra("progress", 0);
                mpb_download_progress.setProgress(progress);

                if (progress == 100) {
                    // 下载完成
                    mpb_download_progress.setVisibility(View.GONE);
                    view_step_to_two.setVisibility(View.VISIBLE);
                    btn_open_game.setEnabled(true);
                    btn_open_game.setTextColor(getResources().getColor(R.color.white));

                    //下载完成
                    onDownloadFinished();
                }
            }
        }
    }

注册:

progressReceiver = new ProgressReceiver();
        IntentFilter progressFilter = new IntentFilter("progress");
        registerReceiver(progressReceiver, progressFilter);

销毁:

if (progressReceiver != null) {
            unregisterReceiver(progressReceiver);
        }

下载方法:

private final Executor executor = new PriorityExecutor(2, true);
    /**
     * 下载游戏,这个方法在Activity销毁之后仍会继续执行
     */
    private void downloadGame() {
        if (TextUtils.isEmpty(gameUrl)) {
            ToastUtils.showShort(mActivity, "游戏下载地址为空");
            return;
        }

        if (!TextUtils.isEmpty(gameLocalPath)) {
            org.xutils.http.RequestParams params = new org.xutils.http.RequestParams(gameUrl);
            params.setAutoRename(true);
            params.setAutoResume(true);
            params.setAutoResume(true);
            params.setAutoRename(false);
            params.setSaveFilePath(gameLocalPath);
            params.setExecutor(executor);
            params.setCancelFast(true);

            x.http().get(params, new Callback.ProgressCallback<File>() {

                @Override
                public void onWaiting() {

                }

                @Override
                public void onStarted() {

                }

                @Override
                public void onLoading(long total, long current, boolean isDownloading) {
                    // 下载进度
                    Intent intent = new Intent("progress");
                    intent.putExtra("progress", (int)(current*100/total));
                    intent.putExtra("gameId", gameId);
                    sendBroadcast(intent);
                }

                @Override
                public void onSuccess(File result) {

                }

                @Override
                public void onError(Throwable ex, boolean isOnCallback) {

                }

                @Override
                public void onCancelled(CancelledException cex) {

                }

                @Override
                public void onFinished() {

                }
            });
        }
    }
<template> <div class="app-container"> <el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect" active-text-color="#2479D2" > <el-menu-item index="1">标注任务</el-menu-item> <el-menu-item index="2">质检任务</el-menu-item> </el-menu> <el-card v-if="checkStatus == 1"> <el-row :gutter="10" class="mb8"> <right-toolbar @queryTable="handleSelect(1)"></right-toolbar> </el-row> <el-table :data="checkData1" style="width: 100%"> <el-table-column label="任务名称" align="center"> <template #default="{ row }"> {{ row.taskName }} </template> </el-table-column> <el-table-column label="状态" prop="taskStatus" align="center" :formatter="taskProFormat" > </el-table-column> <el-table-column label="任务类型" align="center" :formatter="taskTypeFormat" > </el-table-column> <el-table-column label="创建人" align="center"> <template #default="{ row }"> {{ row.createBy }} </template> </el-table-column> <el-table-column label="已标/合格" align="center"> <template #default="{ row }"> {{ finishDone(row) }} </template> </el-table-column> <el-table-column label="可获取数/总数" align="center"> <template #default="{ row }"> {{ gainTotal(row) }} </template> </el-table-column> <el-table-column label="创建时间" align="center"> <template #default="{ row }"> {{ row.createTime }} </template> </el-table-column> <el-table-column label="领取单位" align="center"> <template #default="{ row }"> {{ distributionDesc(row.distributionMethod) }} </template> </el-table-column> <el-table-column label="操作" align="center"> <template v-slot="scope"> <!-- <el-button @click="editProgect(scope.row)" type="text" size="small" >开始任务<span v-if=" scope.row.jobRole === &#39;1&#39; && scope.row.withCheckToProducerMappingFlag === &#39;1&#39; " >(有分配人员)</span > <span v-if=" scope.row.jobRole === &#39;1&#39; && scope.row.withCheckToProducerMappingFlag === &#39;2&#39; " >(有分配人员)</span > </el-button> --> <el-button @click="editProgect(scope.row)" type="text" size="small" >开始任务 </el-button> <el-button @click="taskToDetail(scope.row)" type="text" size="small" >详情 </el-button> </template> </el-table-column> </el-table> </el-card> <el-card v-if="checkStatus == 2"> <el-row :gutter="10" class="mb8"> <right-toolbar @queryTable="handleSelect(2)"></right-toolbar> </el-row> <el-table :data="checkData2" style="width: 100%"> <el-table-column label="任务名称" align="center"> <template #default="{ row }"> {{ row.taskName }} </template> </el-table-column> <el-table-column label="状态" prop="taskStatus" align="center" :formatter="taskProFormat" > </el-table-column> <el-table-column label="任务类型" align="center" :formatter="taskTypeFormat" > </el-table-column> <el-table-column label="创建人" align="center"> <template #default="{ row }"> {{ row.createBy }} </template> </el-table-column> <el-table-column label="已质检" align="center"> <template #default="{ row }"> {{ row.processedUnits }} </template> </el-table-column> <el-table-column label="质检合格率" align="center"> <template #default="{ row }"> {{ onTest(row) }} </template> </el-table-column> <el-table-column label="最低合格率" align="center"> <template #default="{ row }"> {{ lowerTest(row) }} </template> </el-table-column> <el-table-column label="可获取数/总数" align="center"> <template #default="{ row }"> {{ gainTotal(row) }} </template> </el-table-column> <el-table-column label="创建时间" align="center"> <template #default="{ row }"> {{ row.createTime }} </template> </el-table-column> <el-table-column label="领取单位" align="center"> <template #default="{ row }"> {{ distributionDesc(row.distributionMethod) }} </template> </el-table-column> <el-table-column label="操作" align="center"> <template v-slot="scope"> <!-- <el-button @click="editProgect(scope.row)" type="text" size="small" >开始任务<span v-if=" scope.row.jobRole === &#39;1&#39; && scope.row.withCheckToProducerMappingFlag === &#39;1&#39; " >(有分配人员)</span > <span v-if=" scope.row.jobRole === &#39;1&#39; && scope.row.withCheckToProducerMappingFlag === &#39;2&#39; " >(有分配人员)</span > </el-button> --> <el-button @click="goSpotCheck(scope.row)" type="text" size="small" >去质检 </el-button> </template> </el-table-column> </el-table> </el-card> <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="handleProducePAG" /> <!-- 质检人员分配 --> <el-dialog title="需质检人员" :visible.sync="dialogProduce" width="30%" :before-close="handleClose" > <el-table :data="produceData" style="width: 100%"> <el-table-column prop="producerName" label="标注人员" align="center"> </el-table-column> <el-table-column label="操作" align="center"> <template v-slot="scope"> <el-button @click="startQuality(scope.row)" type="text" size="small" >开始质检</el-button > </template> </el-table-column> </el-table> <span slot="footer" class="dialog-footer"> <el-button @click="dialogProduce = false">取 消</el-button> <el-button type="primary" @click="dialogProduce = false" >确 定</el-button > </span> </el-dialog> </div> </template> <script> import { getuserProTask, getuserTimeInfo } from "@/api/produce/produce.js"; import { getMappingInfo } from "@/api/img/img.js"; export default { name: "Produce", data() { return { activeIndex: "1", checkForm: { checkName: "", }, checkData1: [], checkData2: [], state: "", dialogProduce: false, produceData: [], taskOptions: [], qualitytaskId: "", // 状态 type: { 0: "info", 1: "", 4: "info", 5: "success", 6: "", 7: "warning", 8: "success", }, checkStatus: 1, // 查询参数 queryParams: { pageNum: 1, pageSize: 10, }, // 总条数 total: 10, interVal: null, taskTypeOptions: [], }; }, mounted() { this.handleSelect(); this.getDicts("pro_task_status").then((response) => { this.taskOptions = response.data; }); this.getDicts("task_type").then((response) => { this.taskTypeOptions = response.data; }); }, beforeRouteLeave(to, from, next) { this.$destroy(); next(); }, beforeDestroy() { if (this.interVal) { clearInterval(this.interVal); // 关闭 this.interVal = null; } }, methods: { taskToDetail(row) { if (row.taskType == "0") { this.$router.push({ path: "/business/checkDetail", query: { taskId: row.taskId, taskType: row.taskType, }, }); } else if (row.taskType == "1") { this.$router.push({ path: "/business/checkDetail", query: { taskId: row.taskId, userId: row.userId, taskType: row.taskType, }, }); } else if (row.taskType == "6") { this.$router.push({ path: "/business/checkDetail", query: { taskId: row.taskId, userId: row.userId, taskType: row.taskType, }, }); } }, taskProFormat(row, column) { return this.selectDictLabel(this.taskOptions, row.taskStatus); }, taskTypeFormat(row, column) { return this.selectDictLabel(this.taskTypeOptions, row.taskType); }, // availableUnits: "12" 物料单位可获取数 // processedUnits: "0" 已标注数 // realTimeQualifiedUnits: "0" 实时合格数 // taskId: "ProTask-0e4dd186-5f6a-41f6-afbe-7c250b82e1a1" // totalMaterialUnits: "13" 物料单位总数 // 调取标注任务 async handleSelect(key) { this.checkStatus = key ? key : 1; let that = this; if (this.interVal) { clearInterval(this.interVal); this.interVal = null; } if (key === undefined || key === "1") { const res1 = await getuserProTask(this.queryParams, "LABEL"); this.total = res1.total; const taskIdsARR1 = []; res1.rows.forEach((item) => { if (item.jobRole === "0") { taskIdsARR1.push(item.taskId); } }); const data1 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR1, jobRole: "0", }) ); res1.rows.forEach((item) => { data1.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); this.checkData1 = res1.rows.filter((item) => item.jobRole === "0"); this.interVal = setInterval(function () { that.$nextTick(async () => { const data1 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR1, jobRole: "0", }) ); res1.rows.forEach((item) => { data1.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); that.checkData1 = res1.rows.filter((item) => item.jobRole === "0"); }); }, 10000); } else if (key === "2") { const res2 = await getuserProTask(this.queryParams, "CHECK"); this.total = res2.total; const taskIdsARR2 = []; res2.rows.forEach((item) => { if (item.jobRole === "1") { taskIdsARR2.push(item.taskId); } }); const data2 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR2, jobRole: "1", }) ); res2.rows.forEach((item) => { data2.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); this.checkData2 = res2.rows.filter((item) => item.jobRole === "1"); this.interVal = setInterval(function () { that.$nextTick(async () => { const data2 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR2, jobRole: "1", }) ); res2.rows.forEach((item) => { data2.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); that.checkData2 = res2.rows.filter((item) => item.jobRole === "1"); }); }, 10000); } }, // 分页 async handleProducePAG() { let that = this; if (this.interVal) { clearInterval(this.interVal); this.interVal = null; } if (this.checkStatus == 1) { const res1 = await getuserProTask(this.queryParams, "LABEL"); this.total = res1.total; const taskIdsARR1 = []; res1.rows.forEach((item) => { if (item.jobRole === "0") { taskIdsARR1.push(item.taskId); } }); const data1 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR1, jobRole: "0", }) ); res1.rows.forEach((item) => { data1.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); this.checkData1 = res1.rows.filter((item) => item.jobRole === "0"); this.interVal = setInterval(function () { that.$nextTick(async () => { const data1 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR1, jobRole: "0", }) ); res1.rows.forEach((item) => { data1.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); that.checkData1 = res1.rows.filter((item) => item.jobRole === "0"); }); }, 10000); } else if (this.checkStatus == 2) { const res2 = await getuserProTask(this.queryParams, "CHECK"); this.total = res2.total; const taskIdsARR2 = []; res2.rows.forEach((item) => { if (item.jobRole === "1") { taskIdsARR2.push(item.taskId); } }); const data2 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR2, jobRole: "1", }) ); res2.rows.forEach((item) => { data2.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); this.checkData2 = res2.rows.filter((item) => item.jobRole === "1"); this.interVal = setInterval(function () { that.$nextTick(async () => { const data2 = await getuserTimeInfo( JSON.stringify({ taskIds: taskIdsARR2, jobRole: "1", }) ); res2.rows.forEach((item) => { data2.data.forEach((list) => { if (item.taskId == list.taskId) { item.availableUnits = list.availableUnits; item.processedUnits = list.processedUnits; item.realTimeQualifiedUnits = list.realTimeQualifiedUnits; item.totalMaterialUnits = list.totalMaterialUnits; } }); }); that.checkData2 = res2.rows.filter((item) => item.jobRole === "1"); }); }, 10000); } },vue路由跳转下一个界面为什么会去发送上一个界面的请求
最新发布
07-08
<think>我们正在解决用户的问题:当使用Vue路由进行页面跳转时,为什么下一个界面会发送上一个界面的请求?根据之前的讨论,核心原因在于异步请求在组件销毁时未被正确取消。现在,我们将结合用户提供的站内引用,深入分析Vue Router的工作原理,并给出解决方案。 ### 问题分析 根据引用[1]和引用[3],Vue Router通过路由表管理路由配置,并利用Vue的动态组件机制实现页面渲染。当路由跳转时,Vue会销毁旧组件并创建新组件。然而,如果旧组件中发起的异步请求(如axios请求)未在组件销毁前取消,这些请求将继续在后台运行。当请求完成时,它们可能会尝试更新已销毁组件的状态,导致以下问题: - 控制台报错(如“Cannot read properties of undefined”) - 新页面意外渲染旧数据(如果使用全局状态管理) 引用[2]提到,Vue Router的hash和history模式都不会自动取消请求。因此,我们需要手动管理请求的生命周期。 ### 解决方案 #### 方法1:使用axios取消令牌(CancelToken) 在组件销毁前(`beforeDestroy`或`beforeUnmount`钩子),取消专属请求: ```javascript export default { data() { return { cancelTokenSource: null } }, created() { // 创建取消令牌源 const source = axios.CancelToken.source(); this.cancelTokenSource = source; // 发起请求,传入取消令牌 axios.get(&#39;/api/data&#39;, { cancelToken: source.token }).catch(error => { if (axios.isCancel(error)) { console.log(&#39;请求已取消&#39;, error.message); } else { // 处理其他错误 } }); }, beforeUnmount() { // 组件销毁前取消请求 if (this.cancelTokenSource) { this.cancelTokenSource.cancel(&#39;路由跳转取消请求&#39;); } } } ``` #### 方法2:全局请求管理(推荐) 通过axios拦截器和路由守卫统一管理: 1. **创建全局请求队列**: ```javascript // utils/request.js import axios from &#39;axios&#39;; const pendingRequests = new Map(); // 生成请求标识(方法+URL) const getRequestKey = (config) => { return [config.method, config.url].join(&#39;&&#39;); }; // 请求拦截器:将请求加入队列 axios.interceptors.request.use(config => { const key = getRequestKey(config); if (pendingRequests.has(key)) { // 重复请求则取消前一个 pendingRequests.get(key).cancel(&#39;重复请求被取消&#39;); } config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.set(key, { cancel }); }); return config; }); // 响应拦截器:请求完成后移出队列 axios.interceptors.response.use(response => { const key = getRequestKey(response.config); pendingRequests.delete(key); return response; }, error => { if (axios.isCancel(error)) { console.log(&#39;请求取消:&#39;, error.message); } else { // 处理其他错误 } return Promise.reject(error); }); ``` 2. **在路由守卫中取消所有请求**: ```javascript // router/index.js router.beforeEach((to, from, next) => { pendingRequests.forEach((request, key) => { request.cancel(`路由跳转取消请求:${key}`); pendingRequests.delete(key); }); next(); }); ``` ### 数学表达优化分析 设页面跳转时未完成请求数为$n$,每个请求平均占用资源为$r$,则跳转后资源浪费总量为: $$W = \sum_{i=1}^{n} r_i$$ 通过全局取消机制,可将$W$降至$0$,避免资源泄漏[^1][^3]。 ### 总结 Vue Router跳转时发送上一个页面请求的原因是**异步请求未随组件销毁而终止**。解决方案包括: 1. **组件级取消**:在`beforeUnmount`中取消专属请求 2. **全局管理**:结合axios拦截器和路由守卫统一取消所有pending请求 ### §§相关问题§§ 1. Vue Router的导航守卫执行顺序是怎样的? 2. 如何避免Vue组件中重复的axios请求? 3. Vuex状态管理与路由跳转如何协同工作? 4. 在Vue3中如何使用Composition API取消请求? 5. 路由跳转时如何实现请求进度条(如NProgress)? [^1]: Vue Router实现路由跳转和页面渲染的原理 [^2]: Vue Router的hash和history模式区别 [^3]: Vue Router路由配置示例</think>### Vue Router 页面跳转后仍执行前一个页面请求的原因分析 当使用 Vue Router 进行页面跳转时,上一个页面的数据请求仍在执行,主要源于以下核心机制: #### 1. **组件生命周期与异步请求的时序问题** - Vue Router 在切换路由时,会**销毁前一个组件实例**(触发 `beforeUnmount` 和 `unmounted` 钩子) - 但**异步请求不受组件生命周期控制**:一旦请求已发出,即使组件被销毁,请求仍会继续执行直到完成 - 数学表达:设请求发起时间为 $t_0$,组件销毁时间为 $t_d$,若 $t_d < t_0 + \text{请求耗时}$,则请求会在销毁后继续执行 #### 2. **全局状态管理的副作用** - 当使用 Vuex/Pinia 时,请求可能由 store 发起而非组件 - 路由跳转**不会自动重置 store 中的异步操作**,导致请求继续运行 - 示例:在 store action 中发起的请求,即使路由切换不会中断 ```javascript // store 示例(Vuex) actions: { async fetchData({ commit }) { const res = await axios.get(&#39;/api/data&#39;) // 路由跳转后仍会继续 commit(&#39;SET_DATA&#39;, res.data) } } ``` #### 3. **路由模式的影响** - **Hash 模式**(`createWebHashHistory`) 仅改变 URL 中 # 后的部分,**不触发页面刷新**,JS 执行环境保持连续[^2] - **History 模式**(`createWebHistory`) 通过 `pushState()` 改变 URL,同样**不刷新页面**,异步任务保持运行[^2] #### 4. **未正确使用请求取消机制** - 未在组件销毁前调用请求取消方法: ```javascript export default { setup() { const controller = new AbortController(); // 创建取消控制器 onMounted(async () => { try { const res = await fetch(&#39;/api/data&#39;, { signal: controller.signal // 绑定取消信号 }) } catch (e) { if (e.name === &#39;AbortError&#39;) return // 捕获取消异常 } }) onBeforeUnmount(() => { controller.abort() // 组件销毁前取消请求 }) } } ``` ### 解决方案 #### ✅ 方法 1:组件级请求取消 ```javascript // 使用 axios CancelToken export default { data() { return { cancelToken: null } }, mounted() { const source = axios.CancelToken.source() this.cancelToken = source axios.get(&#39;/api/data&#39;, { cancelToken: source.token }) }, beforeUnmount() { if (this.cancelToken) { this.cancelToken.cancel(&#39;路由跳转取消请求&#39;) } } } ``` #### ✅ 方法 2:路由守卫全局取消 ```javascript // 全局请求队列 let pendingRequests = [] // axios 拦截器 axios.interceptors.request.use(config => { config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.push(cancel) }) return config }) // 路由守卫 router.beforeEach((to, from, next) => { pendingRequests.forEach(cancel => cancel(&#39;路由跳转终止请求&#39;)) pendingRequests = [] next() }) ``` #### ✅ 方法 3:智能请求管理 ```javascript // 使用 vue-request 等工具库 import { useRequest } from &#39;vue-request&#39; export default { setup() { const { data, loading } = useRequest(&#39;/api/data&#39;, { manual: false, // 自动在组件卸载时取消请求 unloadingRefetch: false }) return { data, loading } } } ``` ### 核心原理总结 | 关键因素 | 影响机制 | 解决方案 | |---------|---------|---------| | **组件销毁时机** | 销毁后异步回调仍可执行 | `beforeUnmount` 中取消请求 | | **路由模式** | Hash/History 模式均不刷新页面 | 显式请求管理 | | **全局状态** | Store 请求不受路由控制 | 结合路由守卫清理 | | **请求状态** | 网络层独立于 JS 运行时 | 使用 AbortController/CancelToken | 通过合理运用组件生命周期钩子、路由守卫和请求取消机制,可确保路由跳转时精确控制异步请求的生命周期[^1][^3]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ithouse

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

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

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

打赏作者

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

抵扣说明:

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

余额充值