<转载> javascript模版引擎-tmpl的bug修复与性能优化

本文介绍了一款名为tmpl的JavaScript模板引擎,通过对原版的改进,包括修复转义字符处理、提高ID判断准确性及弃用with语句等,大幅提升其性能。同时提供了调试技巧和使用建议。

一篇非常有用的关于js模板的文章.备份之.

原文地址:  http://yiheng.iteye.com/blog/1577360

javascript模版引擎-tmpl的bug修复与性能优化

 

在平时编码中,经常要做拼接字符串的工作,如把json数据用HTML展示出来,以往字符串拼接与逻辑混在在一起会让代码晦涩不堪,加大了多人协作与维护的成本。而采用前端模板机制就能很好的解决这个问题。

精妙的 tmpl

前端模板类开源的不少,但最属 jQuery 作者 John Resig 开发的 “javascript micro templating” 最为精妙,寥寥几笔便实现了模板引擎核心功能。

它的介绍与使用方式请看作者博客:http://ejohn.org/blog/javascript-micro-templating/

让我们先看看他的源码:

 

Javascript代码 
  1.  (function(){  
  2.   var cache = {};  
  3.   
  4.   this.tmpl =  function (str, data){  
  5.     var fn = !/\W/.test(str) ?  
  6.       cache[str] = cache[str] ||  
  7.         tmpl(document.getElementById(str).innerHTML) :  
  8.   
  9.       new Function("obj",  
  10.         "var p=[],print=function(){p.push.apply(p,arguments);};" +  
  11.         "with(obj){p.push('" +  
  12.   
  13.         str  
  14.           .replace(/[\r\t\n]/g, " ")  
  15.           .split("<%").join("\t")  
  16.           .replace(/((^|%>)[^\t]*)'/g, "$1\r")  
  17.           .replace(/\t=(.*?)%>/g, "',$1,'")  
  18.           .split("\t").join("');")  
  19.           .split("%>").join("p.push('")  
  20.           .split("\r").join("\\'")  
  21.       + "');}return p.join('');");  
  22.   
  23.     return data ? fn( data ) : fn;  
  24.   };  
  25. })();  

 

麻雀虽小,五脏俱全,除了基本的数据附加外,还拥有缓存机制、逻辑支持。现在,若要我评出一个javascript 最节能的自定义函数排名,第一名是 $ 函数(document.getElementById 简版),而第二名就是 tmpl 了。

当然,它并非完美,我使用过程中发现了一些问题:

tmpl 美中不足

一、无法正确处理转义字符,如:

 

tmpl('<%=name%>\\<%=id%> ', {name:'糖饼', id: '1987'});

 

它就会报错。若正常工作,它应该输出:糖饼\1987

实际上解决起来很简单,添加一行正则对转义符进行转义:

str.replace(/\\/g, "\\\\")

二、它有时候无法正确区分第一个参数是ID还是模板。

假若页面模板ID带有下划线,如 tmpl-photo-thumb 它不会去查找这个名称的模板,会认为这传入的是原始模板直接编译输出。

原始模板与元素id最直观的区别就是是否含有空格,因此改动下正则表达式即可:

!/\s/.test(str)

三、它内部还残有一处测试用的代码,可删除。

print=function(){p.push.apply(p,arguments);}

tmpl 效率的疑惑

直到前段时间看了百度mux一篇介绍 YayaTemplate 的软文,原文作者对各大流行的模板引擎进行了效率测试,最终得出 YayaTemplate 是最快的一个。 虽然测试结果 tmpl 不敌 YayaTemplate ,但也让我打消了对性能的顾虑,实际应用中与传统的字符串拼接差不多。它们只有进行超大规模的解析才会有较大的性能差距。(超大规模?javascript本身就不适合干这事。若哪天程序员一次性给浏览器插入上千条列表数据而其慢无比的时候,不用怀疑:问题出在了这个程序员身上,他不会爱惜用户的浏览器。)

若说到引擎效率排名问题,我倒不觉得这是不能是衡量模板引擎的首要标准,模板语法也是重要的一环,这时候 YayaTemplate 的模板语法就显得晦涩多了,它为了节省几个正则表达式而在模板语法上耍了小聪明。

先展示 YayaTemplate 的源码:

 

Javascript代码 
  1. //author:yaya,jihu  
  2. //uloveit.com.cn/template  
  3. //how to use?  YayaTemplate("xxx").render({});  
  4. var YayaTemplate = YayaTemplate || function(str){  
  5.     //核心分析方法  
  6.     var  _analyze=function(text){  
  7.         return text.replace(/{\$(\s|\S)*?\$}/g,function(s){   
  8.             return s.replace(/("|\\)/g,"\\$1")  
  9.                     .replace("{$",'_s.push("')  
  10.                     .replace("$}",'");')  
  11.                     .replace(/{\%([\s\S]*?)\%}/g, '",$1,"')  
  12.         }).replace(/\r|\n/g,"");  
  13.     };  
  14.     //中间代码  
  15.     var _temp = _analyze(document.getElementById(str)?document.getElementById(str).innerHTML:str);  
  16.     //返回生成器render方法  
  17.     return {  
  18.         render : function(mapping){  
  19.             var _a = [],_v = [],i;  
  20.             for (i in mapping){  
  21.                     _a.push(i);  
  22.                     _v.push(mapping[i]);  
  23.             }  
  24.             return (new Function(_a,"var _s=[];"+_temp+" return _s;")).apply(null,_v).join("");  
  25.         }  
  26.     }  
  27. };  

 

若把性能问题上升到一个“学术问题”的高度尝试去解决,为什么 tmpl 会比 YayaTemplate 慢?

语法解析?虽然 YayaTemplate 使用了一个新颖的 javascript 包裹 html 的方式作为模板语法,但最终都需要用正则表达式解析成标准的 javascript 语法,这里正则的效率不会有太大的差异,并且双方都使用了缓存机制确保只对原始模板仅进行一次解析。

数据转换?模板引擎会把数据最终以变量的形式保存在闭包中,以好让模板获取到。这里先对比下一下双方的变量声明机制:

YayaTemplate 使用传统传递参数的形式实现。它通过遍历数据对象,把对象的名值分离,然后分别把对象成员名称作为new Function的参数名(即变量名),然后使用函数的appley调用方式传给那些参数。

tmpl 则使用了javascript不常用的 with 语句实现。 实现方式很简洁,省去了var这个关键字。

tmpl 性能问题就出在 with 上面。javascript 提供的 with 语句,本意是想用来更快捷的访问对象的属性。不幸的是,with语句在语言中的存在,就严重影响了 javascript 引擎的速度,因为它阻止了变量名的词法作用域绑定。

优化 tmpl

tmpl 若去掉 with 语句,而改用传统的传参性能立即大提升,经过实测在24万条数据下 firefox 能提高 5 倍,chrome 2.4 倍,opera 1.84倍,safari 2.1倍,IE6 1.1倍,IE9 1.35倍,最终与 YayaTemplate 不分上下。

测试地址:http://www.planeart.cn/demo/tmpl/tmpl.html

tmpl 优化版最终代码:

 

Javascript代码 
  1. /** 
  2.  * 微型模板引擎 tmpl 0.2 
  3.  * 
  4.  * 0.2 更新: 
  5.  * 1. 修复转义字符与id判断的BUG 
  6.  * 2. 放弃低效的 with 语句从而最高提升3.5倍的执行效率 
  7.  * 3. 使用随机内部变量防止与模板变量产生冲突 
  8.  * 
  9.  * @author  John Resig, Tang Bin 
  10.  * @see     http://ejohn.org/blog/javascript-micro-templating/ 
  11.  * @name    tmpl 
  12.  * @param   {String}    模板内容或者装有模板内容的元素ID 
  13.  * @param   {Object}    附加的数据 
  14.  * @return  {String}    解析好的模板 
  15.  * 
  16.  * @example 
  17.  * 方式一:在页面嵌入模板 
  18.  * <script type="text/tmpl" id="tmpl-demo"> 
  19.  * <ol title="<%=name%>"> 
  20.  *  <% for (var i = 0, l = list.length; i < length; i ++) { %> 
  21.  *      <li><%=list[i]%></li> 
  22.  *  <% } %> 
  23.  * </ol> 
  24.  * </script> 
  25.  * tmpl('tmpl-demo', {name: 'demo data', list: [202, 96, 133, 134]}) 
  26.  *  
  27.  * 方式二:直接传入模板: 
  28.  * var demoTmpl = 
  29.  * '<ol title="<%=name%>">' 
  30.  * + '<% for (var i = 0, l = list.length; i < length; i ++) { %>' 
  31.  * +    '<li><%=list[i]%></li>' 
  32.  * + '<% } %>' 
  33.  * +'</ol>'; 
  34.  * var render = tmpl(demoTmpl); 
  35.  * render({name: 'demo data', list: [202, 96, 133, 134]}); 
  36.  *  
  37.  * 这两种方式区别在于第一个会自动缓存编译好的模板, 
  38.  * 而第二种缓存交给外部对象控制,如例二中的 render 变量。 
  39.  */  
  40.   
  41. var tmpl = (function (cache, $) {  
  42. return function (str, data) {  
  43.     var fn = !/\s/.test(str)  
  44.     ? cache[str] = cache[str]  
  45.         || tmpl(document.getElementById(str).innerHTML)  
  46.   
  47.     : function (data) {  
  48.         var i, variable = [$], value = [[]];  
  49.         for (i in data) {  
  50.             variable.push(i);  
  51.             value.push(data[i]);  
  52.         };  
  53.         return (new Function(variable, fn.$))  
  54.         .apply(data, value).join("");  
  55.     };  
  56.   
  57.     fn.$ = fn.$ || $ + ".push('"   
  58.     + str.replace(/\\/g, "\\\\")  
  59.          .replace(/[\r\t\n]/g, " ")  
  60.          .split("<%").join("\t")  
  61.          .replace(/((^|%>)[^\t]*)'/g, "$1\r")  
  62.          .replace(/\t=(.*?)%>/g, "',$1,'")  
  63.          .split("\t").join("');")  
  64.          .split("%>").join($ + ".push('")  
  65.          .split("\r").join("\\'")  
  66.     + "');return " + $;  
  67.   
  68.     return data ? fn(data) : fn;  
  69. }})({}, '$' + (+ new Date));  


 

模板引擎依赖 Function 构造器实现,它与 eval 一样提供了使用文本访问 javascript 解析引擎的方法,这也会让性能显著的降低,但此时 javascript 中已别无他法。

使用 Function 构造器还会对参数名称有所限制,所以导致数据成员命名必须与 javascript 变量名规范保持一致,否则会报错。好在这个错误可以在运行的时候立马被发现,而不会成为一颗地雷。

tmpl 使用小窍门

一、缓存优化。

tmpl 默认对嵌入到页面中的模板进行了缓存优化(即第一个参数为ID的时候),它只会对模板进行一次分析。若原始模板是直接传入到 tmpl 第一个参数中,且需要多次使用的话,建议用公用变量缓存起来,需要解析数据的时候再使用,以获得相同的优化效果。如:

// 生成模板缓存
var render = tmpl(listTmpl);

// 可多次调用模板
elem.innerHTML = render(data1);
elem.innerHTML = render(data2);
...

二、避免未定义的变量引起系统崩溃。

若模板中定义了一个变量输出,而且传入数据却少了这个项目就会出现变量未定义的错误,从而引起整个程序的崩溃。如果无法确保数据完整性,仍然有方法可以对对其成员进行探测。原版中暗含变量保存了原始传入的数据,即 obj ;而在我的升级版本中则是关键字 this,如:

<% if (this.dataName !== undefined) { %>
      <%=dataName %>
<% } %>

三、调试模板。

由于模板引擎是用文本的调用的 javascript 引擎,调试工具无法定位到出错的行。在 升级版本 中你可以用调试工具输出编译好的模板缓存。例如调试这个模板:

<script id="tmpl" type="text/tmpl">
<ul>
	<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li><%=list[i].index%>. 用户: <%=list[i].user%>; 网站:<%=list[i].site%></li>

	<% } %>
</ul>

输出缓存:

window.console(tmpl('tmpl').$);

日志结果:

"$1318348744541.push(' 
<ul> '); for (var i = 0, l = list.length; i < l; i ++) { $1318348744541.push(' 
<li>',list[i].index,'. 用户: ',list[i].user,'; 网站:',list[i].site,'</li>

 '); } $1318348744541.push(' </ul>

 ');return $1318348744541"

现在你可以看到模板引擎编译好的javascript语句,可以对照这检查模板是否存在错误。($1318348744541是一个随机名称的临时数组,可忽略)

最后非常感谢 tmpl 原作者 与 YayaTemplate 作者的付出,正因为此我才有机会深入分析实现机制,解决问题并从中受益。独乐不如众乐,分享之。

<div class="fm-tmpl-listview-common fm-tmpl-listview-card"> <div class="fm-tmpl-listview-content"> <!-- 题目类型作为标题显示 --> <div class="fm-tmpl-row"> <div class="fm-tmpl-col"> <div class="fm-tmpl-label"> <div class="fm-tmpl-label-text" style="font-weight: bold;"> <!-- 只显示题目类型的数据 --> {{$formatUtils.enum(props.item.questionType,{enumData:[{"disabled":false,"name":"单选题","value":"单选题"},{"disabled":false,"name":"多选题","value":"多选题"},{"disabled":false,"name":"判断题","value":"判断题"}]})}} </div> </div> </div> </div> <!-- 题目题干 --> <div class="fm-tmpl-row"> <div class="fm-tmpl-col"> <div class="fm-tmpl-label"> <div class="fm-tmpl-label-text"> 题目题干:{{props.item.questionvarchar2}} </div> </div> </div> </div> <!-- 根据题目类型动态显示选项 --> <div class="fm-tmpl-row"> <div class="fm-tmpl-col"> <div class="fm-tmpl-label"> <div class="fm-tmpl-label-text"> <!-- 单选题 --> <template v-if="props.item.questionType === '单选题'"> <label> <input type="radio" :name="'option_' + props.item.id" value="A"> 选项A:{{props.item.optionA}} </label><br> <label> <input type="radio" :name="'option_' + props.item.id" value="B"> 选项B:{{props.item.optionB}} </label><br> <label> <input type="radio" :name="'option_' + props.item.id" value="C"> 选项C:{{props.item.optionC}} </label><br> <label> <input type="radio" :name="'option_' + props.item.id" value="D"> 选项D:{{props.item.optionD}} </label> </template> <!-- 多选题 --> <template v-else-if="props.item.questionType === '多选题'"> <label> <input type="checkbox" :name="'option_' + props.item.id" value="A"> 选项A:{{props.item.optionA}} </label><br> <label> <input type="checkbox" :name="'option_' + props.item.id" value="B"> 选项B:{{props.item.optionB}} </label><br> <label> <input type="checkbox" :name="'option_' + props.item.id" value="C"> 选项C:{{props.item.optionC}} </label><br> <label> <input type="checkbox" :name="'option_' + props.item.id" value="D"> 选项D:{{props.item.optionD}} </label> </template> <!-- 判断题 --> <template v-else-if="props.item.questionType === '判断题'"> <label> <input type="radio" :name="'option_' + props.item.id" value="A"> 正确 </label><br> <label> <input type="radio" :name="'option_' + props.item.id" value="B"> 错误 </label> </template> </div> </div> </div> </div> </div> </div> 把选择的选项传过去 持久化 然后再回到次条数据的时候再获取 如果首次打开次数据则默认什么都没选中import { BaseDataService, LoadDataService } from '@farris/mobile-command-services'; import { ViewModelContext, HttpClient } from '@farris/mobile-devkit'; import { switchMap } from 'rxjs/operators'; import { map } from 'rxjs/operators'; export class ExpenseMfrmControllerService extends BaseDataService { private httpClient: HttpClient; private loadDataService: LoadDataService; private s: number = 0; // 初始化 s 的值为 0 private res: any[] = []; // 初始化 res 为空数组 constructor(viewModelContext: ViewModelContext) { super(viewModelContext); this.httpClient = new HttpClient(); this.loadDataService = this.getService<LoadDataService>(LoadDataService); } /** * 自定义取数 * @remarks * @returns s.returnValue.result[0] */ public myLoadData() { return this.loadDataService.loadForList().pipe( switchMap(response => { this.res = response; // 存储响应数据 const myFilters = [{ Compare: 0, Expresstype: 0, FilterField: "ID", Lbracket: "", Rbracket: "", Relation: 0, Value: this.res[this.s].newData.id }]; // 取数 const filters = this.loadDataService.mergeFilters(myFilters, true); console.log(this.res[this.s].newData.id, '423423234242343242filters'); return this.loadDataService.loadForList(filters); }) ); } /** * 增加 s 的值 */ public incrementS() { if (this.s < this.res.length - 1) { // 确保 s 不超过数组长度减一 this.s++; } const myFilters = [{ Compare: 0, Expresstype: 0, FilterField: "ID", Lbracket: "", Rbracket: "", Relation: 0, Value: this.res[this.s].newData.id }]; const filters = this.loadDataService.mergeFilters(myFilters, true); return this.loadDataService.loadForList(filters); } /** * 减少 s 的值 */ public decrementS() { if (this.s > 0) { // 确保 s 大于 0 this.s--; } const myFilters = [{ Compare: 0, Expresstype: 0, FilterField: "ID", Lbracket: "", Rbracket: "", Relation: 0, Value: this.res[this.s].newData.id }]; const filters = this.loadDataService.mergeFilters(myFilters, true); return this.loadDataService.loadForList(filters); } }
最新发布
05-13
<think>好的,我现在需要帮助用户解决在Vue或类似框架中实现考试答题APP的选项持久化问题。用户提到需要保存用户的选择,并在重新加载题目时恢复状态,同时可能需要结合前端状态管理和后端接口存储。让我先仔细分析这个问题。 首先,用户的需求是在考试过程中,用户选择的答案能够被保存,即使他们离开当前页面或重新进入,也能恢复之前的选项。这涉及到前端的状态管理和后端的数据存储。我需要考虑如何在Vue中管理这些状态,以及如何后端API交互来持久化数据。 根据用户提供的引用,特别是引用[1]提到使用Vue.js构建前端,Flask作为后端,MySQL数据库。所以技术栈应该是Vue.js + Flask + MySQL。用户可能需要结合这些技术来实现持久化。 接下来,我需要考虑前端的状态管理。Vue本身有响应式数据,但如果是单页应用,页面刷新或跳转可能会导致状态丢失。所以可能需要使用Vuex来管理全局状态,或者结合浏览器的本地存储(如localStorage或sessionStorage)临时保存数据。不过,考虑到考试场景可能需要防止用户作弊,本地存储可能不够可靠,所以还是需要后端存储来保证数据的持久化和一致性。 然后,用户提到需要结合后端接口存储。这意味着每次用户选择答案时,前端需要发送请求到后端保存答案,同时在加载题目时从后端获取已保存的答案。这涉及到API的设计,比如保存答案的POST请求和获取答案的GET请求。 另外,用户可能希望实时保存,而不是等到最后提交。这样即使网络中断或用户不小心关闭页面,数据也不会丢失。这需要前端在用户操作时自动触发保存,比如使用防抖函数来减少请求次数,避免性能问题。 在恢复状态方面,当用户重新进入考试或刷新页面时,前端需要从后端获取已保存的答案,并更新到对应的题目选项中。这可能需要前端在组件挂载时(如mounted钩子)调用获取答案的API,并将答案填充到对应的表单项中。 还需要考虑数据模型的设计。前端可能需要维护一个答案对象,键是题目ID,值是用户的答案。这个对象会被Vue的响应式系统跟踪,任何变化都会触发保存操作。同时,后端需要有相应的数据表来存储用户的答题记录,比如用户之前的回答提到的record_detail表,包含用户ID、考试ID、题目ID、用户答案等字段。 另外,关于状态管理库的选择,如果应用复杂度高,使用Vuex或Pinia可以帮助集中管理状态,方便跨组件共享数据。不过对于简单的场景,可能使用组合式API(Composition API)配合提供/注入(provide/inject)也可以满足需求。 还需要考虑网络延迟或失败的情况。前端可能需要实现重试机制,或者提示用户保存失败,确保数据的可靠性。同时,在提交试卷时,需要确认所有答案都已成功保存到后端,避免数据丢失。 最后,测试环节也很重要。需要模拟各种场景,如网络中断、页面刷新、重新进入考试等,确保持久化机制可靠。同时,性能方面要考虑频繁的API请求是否会影响用户体验,可能需要优化请求频率,比如使用防抖或节流技术。 总结一下,实现步骤可能包括: 1. 前端使用响应式状态管理用户答案。 2. 用户操作时自动触发保存到后端。 3. 页面加载时从后端获取已保存答案并恢复状态。 4. 使用Vuex或Pinia管理全局状态(可选)。 5. 结合本地存储作为临时备份(可选)。 6. 后端设计合适的API和数据库表来存储答题记录。 7. 处理网络错误和性能优化。</think>### 考试答题APP选项持久化实现方案 #### 一、前端状态管理实现(Vue.js) ```javascript // 使用组合式API管理答题状态 const useExamStore = () => { const answers = ref({}); // { questionId: selectedAnswer } // 自动保存防抖处理(500ms间隔) const autoSave = debounce(async (questionId, answer) => { await saveAnswerToServer(questionId, answer); }, 500); // 用户选择答案时触发 const selectAnswer = (questionId, answer) => { answers.value[questionId] = answer; autoSave(questionId, answer); }; // 初始化时加载历史答案 onMounted(async () => { const res = await fetchUserAnswers(examId.value); answers.value = res.data.reduce((acc, cur) => { acc[cur.questionId] = cur.userAnswer; return acc; }, {}); }); return { answers, selectAnswer }; } ``` #### 二、后端接口设计(SpringBoot示例) ```java // 答题记录保存接口 @PostMapping("/api/exam/save-answer") public ResponseEntity<?> saveAnswer( @RequestBody AnswerDTO answerDTO, @AuthenticationPrincipal User user) { recordDetailService.saveAnswer( user.getId(), answerDTO.getExamId(), answerDTO.getQuestionId(), answerDTO.getUserAnswer() ); return ResponseEntity.ok().build(); } // 答案加载接口 @GetMapping("/api/exam/answers/{examId}") public List<AnswerVO> getAnswers( @PathVariable Long examId, @AuthenticationPrincipal User user) { return recordDetailService.getUserAnswers( user.getId(), examId ); } ``` #### 三、数据库设计优化(补充字段) ```sql ALTER TABLE record_detail ADD COLUMN save_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; ALTER TABLE exam_record ADD COLUMN last_question_id INT COMMENT '最后操作的题目ID'; ``` #### 四、实现效果保障机制 1. **双保险存储策略**: ```javascript // 本地临时存储+服务端持久化 const saveAnswer = async (questionId, answer) => { // 优先本地存储 localStorage.setItem(`tempAnswer_${questionId}`, answer); try { await api.saveAnswer({ questionId, answer }); } catch (err) { console.error('自动保存失败,保留本地副本'); } } ``` 2. **断点续答功能**: ```sql SELECT last_question_id FROM exam_record WHERE user_id = #{userId} AND exam_id = #{examId} ``` 3. **数据同步验证**: ```javascript // 定时同步检查(每5分钟) setInterval(async () => { const localHashes = calculateLocalAnswerHashes(); const serverHashes = await fetchAnswerHashes(); if (!deepEqual(localHashes, serverHashes)) { showSyncWarning(); } }, 300000); ``` #### 五、典型组件实现(单选题示例) ```vue <template> <div v-for="q in questions" :key="q.id"> <h3>{{ q.content }}</h3> <div v-for="opt in q.options" :class="{ 'selected': answers[q.id] === opt.key }" @click="selectAnswer(q.id, opt.key)"> {{ opt.text }} </div> </div> </template> <script setup> const { answers, selectAnswer } = useExamStore(); </script> ``` ### 架构设计要点 1. **状态同步时序控制**: ```mermaid sequenceDiagram 前端->>后端: 初始化加载历史答案 前端->>用户: 渲染题目和已选答案 用户->>前端: 选择新答案 前端->>本地: 立即更新UI状态 前端->>后端: 异步保存答案(防抖处理) 后端->>DB: 写入record_detail表 ``` 2. **性能优化方案**: - 批量保存接口设计 - 答案差异对比更新 - 索引优化(`(record_id, question_id)`联合索引) 3. **异常处理机制**: - 自动重试策略(指数退避算法) - 离线存储队列(恢复网络后同步) - 版本冲突检测(乐观锁机制)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值