Vue-Codemirror代码高亮显示,form表单v-model双向绑定

本文介绍如何在Vue项目中利用Codemirror实现代码高亮显示,并通过自定义组件完成form表单的v-model双向绑定。包括安装配置、主题设置、不同文件类型的样式切换等步骤。

Vue中使用Codemirror实现代码高亮,form表单v-model双向绑定

需求:eladmin-ui中使用Codemirror高亮显示代码,并实现form表单的双向绑定。

1.安装插件Codemirror

npm install vue-codemirror --save

2.main.js

在项目的main.js中引入我们需要的vue-codemirror

import VueCodeMirror from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
Vue.use(VueCodeMirror)

3.自定义组件MyCodeMirror

这里我自定义了一个组件MyCodeMirror,代码如下:

<template>
  <div>
    <codemirror v-model="newConfigFile" :options="options" />
  </div>
</template>

<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/theme/ambiance.css'
require('codemirror/mode/javascript/javascript')
// 核心样式
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/addon/selection/active-line'
// 引入主题后还需要在 options 中指定主题才会生效
import 'codemirror/theme/paraiso-light.css'
import 'codemirror/theme/paraiso-dark.css'
import 'codemirror/theme/rubyblue.css'
import 'codemirror/mode/python/python.js'
import 'codemirror/keymap/sublime'
import 'codemirror/mode/css/css'
import 'codemirror/theme/monokai.css'
import 'codemirror/mode/yaml/yaml.js'
import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter.js'
import 'codemirror/mode/xml/xml.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
import 'codemirror/theme/base16-light.css'
import 'codemirror/mode/properties/properties.js'
import 'codemirror/theme/eclipse.css'
export default {
  components: { codemirror },
  props: {
    configFile: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      // 默认配置
      options: {
        tabSize: 2, // 缩进格式
        mode: 'yaml',
        theme: 'rubyblue',
        lineNumbers: true, // 显示行号
        line: true,
        styleActiveLine: true, // 高亮选中行
        matchBrackets: true, // 括号匹配
        hintOptions: {
          completeSingle: true // 当匹配只有一项的时候是否自动补全
        }
      }
    }
  },
  computed: {
    newConfigFile: {
      get() {
        return this.configFile
      },
      set(val) {
        this.$emit('parseCMValue', val)
      }
    }
  },
  methods: {
    onOptions(value) {
      if (value === 1) { // text
        this.options.mode = 'text/javascript'
        this.options.theme = 'monokai'
      } else if (value === 4) { // yaml
        this.options.mode = 'yaml'
        this.options.theme = 'rubyblue'
      } else if (value === 3) { // xml
        this.options.mode = 'xml'
        this.options.theme = 'ambiance'
      } else if (value === 2) { // json
        this.options.mode = 'application/json'
        this.options.theme = 'eclipse'
      } else if (value === 5) { // html
        this.options.mode = 'text/html'
        this.options.theme = 'base16-light'
      } else if (value === 6) { // Properties
        this.options.mode = 'properties'
        this.options.theme = 'ambiance'
      }
    }
  }
}
</script>

<style scoped>

</style>

4.使用自定义组件

先在页面中注册自定义组件

import MyCodeMirror from '@/components/MyCodeMirror/index'
export default {
	components: { MyCodeMirror }
}

这里的需求是根据不同的文件类型,显示不同的样式和主题

<el-form-item label="配置文件格式">
 <el-radio-group v-model="form.configFileType" @change="onOptions">
   <el-radio :label="1">TEXT</el-radio>
   <el-radio :label="4">YAML</el-radio>
   <el-radio :label="3">XML</el-radio>
   <el-radio :label="2">JSON</el-radio>
   <el-radio :label="5">HTML</el-radio>
   <el-radio :label="6">Properties</el-radio>
 </el-radio-group>
</el-form-item>
<el-form-item label="配置文件">
  <div style="width: 590px">
    <MyCodeMirror ref="MyCM" :config-file="form.configFile" @parseCMValue="parseCMValue" />
  </div>
</el-form-item>

主题和样式监听事件:onOptions

onOptions(value) {
      this.$refs.MyCM.onOptions(value)
    },

实现的效果图如下:
在这里插入图片描述

5.v-model双向绑定

这里最主要的还是双向绑定提交表单的问题。在我们的页面中有这样的代码:

:config-file="form.configFile" @parseCMValue="parseCMValue"

在自定义组件中有定义的v-model代码:

<codemirror v-model="newConfigFile" :options="options" />

组件中以configFile接受页面中的:config-file。这里需要注意的是直接v-model绑定报错,如图,子组件中不能直接修改父组件传的prop,会报错
在这里插入图片描述
我们使用computed计算属性重新赋值;将接收的configFile赋值父组件:v-model="newConfigFile"。再以parseCMValue赋值给子组件:@parseCMValue="parseCMValue"

computed: {
    newConfigFile: {
      get() {
        return this.configFile
      },
      set(val) {
        this.$emit('parseCMValue', val)
      }
    }
  },

接下来就是将取到的parseCMValue赋值到form表单的configFile属性:

parseCMValue(value) {
      this.form.configFile = value
    }

至此,我们的需求代码高亮及双向绑定就完成了。

monacoEditor在vue中使用 <template> <!-- 编辑设备配置文件 --> <!-- monacoEditor 编辑器 --> <div class="edit_devices_profile"> <base-drawer ref="baseDrawer" width="100%" :title="titleName" @closeDialog="onCloseDialog" :submitText="customSubmitText" @submitForm="onSubmitDrawer" > <el-button type="primary" class="button left-button" @click=" showDeviceList = true; openList(); " >选择矿卡</el-button ><br /> <div class="selected-devices"> 已选择矿卡:<span>{{ localTruckNames.join(', ')||'无' }}</span> </div> <!-- 配置格式单选框 --> <el-radio-group v-model="selectedFormat" class="config-format-radio-group" > <el-radio label="toml">TOML</el-radio> <el-radio label="xml">XML</el-radio> <el-radio label="TEXT">TEXT</el-radio> </el-radio-group> <div ref="monacoContainer" id="monacoContainer"></div> <list-receiver ref="listReceiver" @selectedTrucksChanged="updateSelectedTruckNames" ></list-receiver> </base-drawer> <!-- codemirror 对比编辑器 --> <base-dialog ref="baseDialog" title="内容比较" width="1300px" dialogStyle="mirrorDialog" @submitForm="onSubmitDialog" > <div class="mirrorTitle"> <span class="title">当前值</span> <span class="title">原始值</span> </div> <div id="mirrorEditor"></div> </base-dialog> </div> </template> <script> import * as monaco from "monaco-editor"; import CodeMirror from "codemirror"; import "codemirror/theme/neat.css"; import "codemirror/lib/codemirror.css"; import "codemirror/addon/merge/merge.js"; import "codemirror/addon/merge/merge.css"; import DiffMatchPatch from "diff-match-patch"; const ListReceiver = () => import("../../components/devices/ListReceiver"); window.diff_match_patch = DiffMatchPatch; window.DIFF_DELETE = -1; window.DIFF_INSERT = 1; window.DIFF_EQUAL = 0; export default { name: "EditDevicesProfile", props: { selectedTruckNames: { type: Array, default: () => [], }, }, components: { ListReceiver, }, data() { return { truckName: "", selectedDeviceNames: [], localTruckNames: [...this.selectedTruckNames], selectedDevices: [], deviceList: [], selectedFormat: "toml", customSubmitText: "发布", titleName: "", // 标题 monacoEditor: null, // 编辑器 newMonacoValue: null, // monacoEditor修改后的数据 form: { id: null, // 行id editorValue: null, // 最终发布时的数据 deviceList: [], // 矿卡id // TODO:多选后的矿卡 }, objTest: { // 测试数据 TODO: 接口完成后删除 id: "19", dataId: "1101", group: "DEFAULT_GROUP", // "content": "\r\n[[hmi]]\r\nname = \"hmi\"\r\nenable = true\r\nlib_path = \"libhmi.so\"\r\nclass_name = \"HmiComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[scenario]]\r\nname = \"scenario\"\r\nenable = true\r\nlib_path = \"libscenarios.so\"\r\nclass_name = \"ScenarioComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[map]]\r\nname = \"map\"\r\nenable = true\r\nlib_path = \"libhdmap.so\"\r\nclass_name = \"MapComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[hello_pub]]\r\nname = \"helloPub\"\r\nenable = true\r\nlib_path = \"libhelloPub_comp.so\"\r\nclass_name = \"HelloComponentPub\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[hello_sub]]\r\nname = \"helloSub\"\r\nenable = true\r\nlib_path = \"libhelloSub_comp.so\"\r\nclass_name = \"HelloComponentSub\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[sub_test]]\r\nname = \"sub_test\"\r\nenable = true\r\nlib_path = \"libPnc_test.so\"\r\nclass_name = \"Pnc_test\"\r\ntype = \"timer\"\r\ninterval = 100\r\n \r\n[[hello_comp]]\r\nname = \"helloComp\"\r\nenable = true\r\nlib_path = \"libhello_comp.so\"\r\nclass_name = \"HelloComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[lidar_comp]]\r\nname = \"LidarComp\"\r\nenable = true\r\nlib_path = \"liblidar.so\"\r\nclass_name = \"LidarComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[lidar_pub]]\r\nname = \"LidarComp\"\r\nenable = true\r\nlib_path = \"liblidar.so\"\r\nclass_name = \"LidarComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[lidar_sub]]\r\nname = \"LidarComp\"\r\nenable = true\r\nlib_path = \"liblidar.so\"\r\nclass_name = \"LidarComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[sensor]]\r\nname = \"gps\"\r\nenable = true\r\nlib_path = \"libgps.so\"\r\nclass_name = \"GpsComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[test_log]]\r\nname = \"helloComp\"\r\nenable = true\r\nlib_path = \"libtestglog_comp.so\"\r\nclass_name = \"TestComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[cloud]]\r\nname = \"cloud\"\r\nenable = true\r\nlib_path = \"libcloud.so\"\r\nclass_name = \"CloudComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[tnt]]\r\nname = \"cloud\"\r\nenable = true\r\nlib_path = \"libcloud.so\"\r\nclass_name = \"CloudComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[tnt]]\r\nname = \"hmi\"\r\nenable = true\r\nlib_path = \"libhmi.so\"\r\nclass_name = \"HmiComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[tnt]]\r\nname = \"scenario\"\r\nenable = true\r\nlib_path = \"libscenarios.so\"\r\nclass_name = \"ScenarioComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[tnt]]\r\nname = \"map\"\r\nenable = true\r\nlib_path = \"libhdmap.so\"\r\nclass_name = \"MapComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[rcd]]\r\nname = \"rcd\"\r\nenable = true\r\nlib_path = \"librcdclient.so\"\r\nclass_name = \"rcdClientComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[pnp]]\r\nname = \"planning\"\r\nenable = true\r\nlib_path = \"libplanning.so\"\r\nclass_name = \"PlanningComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[pnp]]\r\nname = \"prediction\"\r\nenable = true\r\nlib_path = \"libprediction.so\"\r\nclass_name = \"PredictionComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[cnc]]\r\nname = \"control\"\r\nenable = true\r\nlib_path = \"libcontrol.so\"\r\nclass_name = \"ControlComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[cnc]]\r\nname = \"can_adaptor\"\r\nenable = true\r\nlib_path = \"libcan_adaptor.so\"\r\nclass_name = \"CanAdaptorComponent\"\r\ntype = \"timer\"\r\ninterval = 50\r\n\r\n[[pnc]]\r\nname = \"planning\"\r\nenable = true\r\nlib_path = \"libplanning.so\"\r\nclass_name = \"PlanningComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[pnc]]\r\nname = \"control\"\r\nenable = true\r\nlib_path = \"libcontrol.so\"\r\nclass_name = \"ControlComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[pnc]]\r\nname = \"can_adaptor\"\r\nenable = true\r\nlib_path = \"libcan_adaptor.so\"\r\nclass_name = \"CanAdaptorComponent\"\r\ntype = \"timer\"\r\ninterval = 50\r\n\r\n\r\n[[pnc]]\r\nname = \"prediction\"\r\nenable = true\r\nlib_path = \"libprediction.so\"\r\nclass_name = \"PredictionComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n\r\n[[prediction]]\r\nname = \"prediction\"\r\nenable = true\r\nlib_path = \"libprediction.so\"\r\nclass_name = \"PredictionComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[can_adaptor]]\r\nname = \"can_adaptor\"\r\nenable = true\r\nlib_path = \"libcan_adaptor.so\"\r\nclass_name = \"CanAdaptorComponent\"\r\ntype = \"timer\"\r\ninterval = 50\r\n\r\n[[can]]\r\nname = \"can_adaptor\"\r\nenable = true\r\nlib_path = \"libcan_adaptor.so\"\r\nclass_name = \"CanAdaptorComponent\"\r\ntype = \"timer\"\r\ninterval = 50\r\n\r\n[[sim]]\r\nname = \"sim_platform\"\r\nenable = true\r\nlib_path = \"libsim_platform.so\"\r\nclass_name = \"BusinessManagerComponent\"\r\ntype = \"timer\"\r\ninterval = 50\r\n\r\n[[radar]]\r\nname = \"radar\"\r\nenable = true\r\nlib_path = \"libradar.so\"\r\nclass_name = \"RadarComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[csv_logger]]\r\nname = \"csv_logger\"\r\nenable = true\r\nlib_path = \"libcsv_logger.so\"\r\nclass_name = \"CsvLoggerComponent\"\r\ntype = \"timer\"\r\ninterval = 100\r\n\r\n[[lidar_back]]\r\nname = \"lidar_back\"\r\nenable = true\r\nlib_path = \"liblidar.so\"\r\nclass_name = \"LidarComponent\"\r\ntype = \"timer\"\r\ninterval = 1000\r\n\r\n[[diagnose]]\r\nname = \"diagnose\"\r\nenable = true\r\nlib_path = \"libdiagnose_service.so\"\r\nclass_name = \"DiagnoseServiceNode\"\r\ntype = \"timer\"\r\ninterval = 1000\r\n\r\n", content: '## general\r\npath_interval = 0.2\r\nplan_path_length = 80.0 # 规划路径长度,单位:m\r\npreview_dist = 15.0\r\nvehicle_width_expand_dist = 0.5 # 车辆宽度安全距离\r\nvehicle_length_expand_dist = 3.5 # 车辆长度安全距离\r\nenable_bussiness = false\r\nenable_diagnose = true # 故障诊断开关\r\nrecord_data_types = ["normal_data","planning_path","obstacles","vehicle_pose"] # 记录csv的数据内容\r\n # 常规周期性数据: "normal_data"\r\n # 规划路径: "planning_path"\r\n # 障碍物: "obstacles"\r\n # 车辆位姿: "vehicle_pose"\r\n\r\n\r\n## cloud-vehicle interaction\r\navoidance_apply_distance = 100.0\r\nmax_avoidance_length = 30.0\r\nmin_avoidance_index =10\r\n\r\n## parking\r\nparking_velocity = 5.0 # 泊车速度,单位:km/h\r\nmax_parking_search_time = 5.0 # 泊车规划最大时间,单位:second\r\nif_smooth = false\r\nplan_area_type = 0 # 泊车规划区域\r\nmax_search_num = 15\r\ndetection_offset = 1\r\n\r\n## drive_in\r\ndrive_in_strategy = ["HybridAStarRS","ReedShepp"]\r\nmin_forward_dist = 7.0 # 停车点最小前移距离,单位:m\r\nmax_forward_dist = 100.0 # 停车点最大前移距离,单位:m\r\nstep_forward_dist = 10.0 # 停车点前移步长,单位:m\r\nextend_straight_dist = 1.0 # 向前延长距离,单位:m\r\nextend_straight_dist_for_waiting_mode = 0.4 # 进入等待位模式后,向前延长距离,单位:m\r\nis_waiting_mode = false # 等待位模式\r\n\r\n# drive_out\r\ndrive_out_strategy = ["Dubins"]\r\nif_stitch_to_main_path = true\r\nmax_sample_dist = 100.0', md5: "319430c2ca8749768b254ced462b9652", encryptedDataKey: "", tenant: "dev", appName: "", type: "toml", createTime: 1735117388453, modifyTime: 1735117388453, createUser: "nacos", createIp: "172.16.41.17", desc: "测试toml文件", use: null, effect: null, schema: null, configTags: null, }, }; }, mounted() { this.fetchDeviceList(); this.initMonaco(); // 初始化编辑器语言及主题 }, watch: { selectedTruckNames(newVal) { this.localTruckNames = [...newVal]; // 监听props变化,更新本地副本 } }, methods: { openList(data) { this.$refs.listReceiver.toggleDialog(true); }, fetchDeviceList() { // 假设有一个 API 调用获取矿卡列表 }, updateSelectedTruckNames(selectedTrucks) { this.localTruckNames = selectedTrucks; // 修改本地变量 this.$emit('update:selectedTruckNames', selectedTrucks); }, // // 初始化编辑器 initMonaco() { // 注册 TOML 语言 monaco.languages.register({ id: "toml", extensions: [".toml"], aliases: ["TOML"], mimetypes: ["text/toml"], }); // 设置属性高亮 monaco.languages.setMonarchTokensProvider("toml", { tokenizer: { root: [ [/^\[.*\]/, "delimiter"], [/^[a-zA-Z_]\w*/, "identifier"], [/=\s*/, "operator"], [/".*?"/, "string"], [/'.*?'/, "string"], [/\s*#\s*(.*)$/, "comment"], [/true|false/, "boolean"], [/\d+\.\d+|\d+/, "number"], ], }, }); // 设置属性颜色 monaco.editor.defineTheme("myTheme", { base: "vs-dark", inherit: true, rules: [ { token: "delimiter", foreground: "eee8aa" }, { token: "identifier", foreground: "74b0df" }, { token: "operator", foreground: "d4d4d4" }, { token: "string", foreground: "ce9178" }, { token: "comment", foreground: "608b4e" }, { token: "boolean", foreground: "3dc9b0" }, { token: "number", foreground: "b5cea8" }, ], colors: { "editor.background": "#1e1e1e", // "editor.foreground": "#d4d4d4", // "editorCursor.foreground": "#d4d4d4", // "editor.lineHighlightBackground": "#3c3c3c", // "editorLineNumber.foreground": "#d4d4d4", // "editor.selectionBackground": "#3c3c3c", }, }); }, // 创建编辑器 createMonaco(data) { this.monacoEditor = monaco.editor.create(this.$refs.monacoContainer, { theme: "vs-dark", // 主题 value: this.objTest.content, // TODO: 列表传过来的值 language: this.objTest.type, }); // 编辑器改变主题 monaco.editor.setTheme("myTheme"); let _this = this; // 内容改变 this.monacoEditor.onDidChangeModelContent(function (e) { _this.editorChange(e); const changes = e.changes.map((change) => { return { range: change.range, text: change.text, }; }); }); }, // 编辑器改变 editorChange(event) { this.newMonacoValue = this.monacoEditor.getValue(); // 更新表单数据 this.form.editorValue = this.newMonacoValue; console.log('monaco编辑器改变',this.form.editorValue); }, // 接收矿卡详情 showDetails(data) { this.titleName = `配置文件 - ${data.fileNameDisplay}`; this.drawerShow(); this.$nextTick(() => { this.createMonaco(data); }); }, // 关闭弹窗 onCloseDialog() { console.log("关闭弹窗"); }, // 确认弹窗 async onSubmitDrawer(ev) { console.log("弹窗确定", ev); await this.$refs.baseDialog.toggleDialog(); this.showMirrorEditor(); }, // 展示/隐藏弹窗 drawerShow() { this.$refs.baseDrawer.toggleDialog(); }, // codemirror showMirrorEditor() { // 创建 CodeMirror MergeView 实例 const mergeView = CodeMirror.MergeView( document.getElementById("mirrorEditor"), { theme: "neat", // 主题 value: this.newMonacoValue || this.objTest.content, // 左侧内容 origRight: null, orig: this.objTest.content, // 右侧内容 lineNumbers: true, // 显示行号 mode: this.objTest.type, highlightDifferences: true, // 高亮 connect: "align", readOnly: false, // 只读 scrollLock: true, // 锁定 } ); // 监听改变 // 获取左侧编辑器实例 const leftEditor = mergeView.editor(); // 添加监听器以监听左侧编辑器内容的变化 leftEditor.on("change", (instance, change) => { // 获取当前左侧编辑器的值 this.form.editorValue = instance.getValue(); const changeDetails = { from: change.from, to: change.to, text: change.text, }; }); }, onSubmitDialog() { console.log("最终提交"); // 提交编辑器内容 this.submitEditorContent(); }, selectDevice(device) { this.selectedDevices.push(device); // 假设存在一个数组存储选中的矿卡对象 this.selectedDeviceNames.push(device.name); // 将矿卡名称添加到显示数组中 this.showDeviceList = false; // 关闭矿卡选择列表 }, submitEditorContent() { console.log("提交给后端的内容:"); //关闭弹窗 this.$refs.baseDialog.toggleDialog(); console.log(this.form); }, }, beforeDestroy() { if (this.monacoEditor) { this.monacoEditor.dispose(); } }, }; </script> <style lang="scss"> .selected-devices { margin: 15px 0; color: #ffffff; font-size: 14px; } .config-format-radio-group { margin-bottom: 10px; // margin-top:10px; } #monacoContainer { width: 100%; height: calc(100% - 30px); margin-top: 10px; overflow: hidden; body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, button { font-family: Consolas, "Courier New", monospace; } } .mirrorTitle { height: 30px; display: flex; justify-content: space-between; align-items: center; color: $whiteOpacity7; text-align: center; .title { width: 47%; } } .drawer-body-content-body { overflow: hidden; } #mirrorEditor { width: 100%; height: calc(100% - 30px); // overflow: hidden; .CodeMirror-merge { height: 100%; .CodeMirror-merge-pane { height: 100%; .CodeMirror { height: 100%; } } } } .mirrorDialog { height: 700px; .dialog-body-content { height: 100%; overflow: hidden; } .config-format-radio-group { margin-bottom: 10px; margin-top: 10px; } } </style>编辑数据
07-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值