Vue+iview реализует редактируемую форму

внешний интерфейс JavaScript Vue.js iView

Во-первых, позвольте мне кратко объяснить, что методы vue и iview, представленные в этой демонстрации, представлены тегами, а инструменты построения, такие как веб-пакет, не используются... В конце концов, компания до сих пор использует angularjs+jq. Я тоже впервые пишу статью, все просто посмотрите на идеи, было бы лучше, если бы были большие ребята, чтобы дать указатели

Без лишних слов, давайте сделаем рендеринг

Давайте взглянем на очень простую структуру каталогов.

IViewEditTable                ## vue+iview 实现的可编辑表格
└── index.html                ## 首页
└── js
    └── editTable.js          ## 首页JS
└── ivew                      ## iview相关
└── vue
    ├── axios.min.js          ## axios (ajax)
    ├── util.js               ## 与业务无关的纯工具函数包
    └── vue.min.js            ## vue (2.x)

Домашний html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>可编辑表格</title>
  <link href="iview/iview.css" rel="stylesheet" />
</head>
<body style="background-color: #f0f3f4;">
  <div id="editTableCtrl">
    <i-table :loading="loading" border :data="dataList" :columns="columnsList" stripe size="small"></i-table>
  </div>
  <script src="vue/axios.min.js"></script>
  <script src="vue/vue.min.js"></script>
  <script src="iview/iview.min.js"></script>
  <script src="vue/util.js"></script>
  <script src="js/editTable.js"></script>
</body>
</html>

Главная страница мало что говорит, это базовая полка. Вот данные, которые нужно отобразить, и их описание:

{
  "Status": 1,
  "Total": 233,
  "Items": [{
    "ID": 1,
    "PID": 3,
    "PRJCODE": "2018-001",                                          //项目编号  不可编辑
    "PRJNAME": "淡化海水配套泵站",                                  //项目名称  文本输入框
    "PRJTYPE": "基础设施",                                          //项目类型  下拉选项
    "JSUNIT": "投资公司",                                           //建设单位  文本输入框
    "FLOW_TYPE_CODE":"A02",                                         //流程分类  下拉选项,与数据库以code形式交互
    "DATE_START": "2018-12-1",                                      //开工时间  日期选择
    "DATE_END": "2019-12-1",                                        //竣工时间  日期选择
    "CONTENT": "建设淡化海水配套泵站一座,占地面积约8500平方米",    //建设内容  多行输入框
    "INVEST_ALL": "1000"                                            //总投资    数字输入框
  }]
}

Также есть базовая полка editTable.js, $http добавлен в последнюю строку utils для удобства (angularjs много использует и привык использовать $http)

Vue.prototype.utils = utils
window.$http = axios

editTable.js :

var vm = new Vue({
  el: '#editTableCtrl',
  data: function() {
    return {
      loading: true,
      //表格的数据源
      dataList: [],
      // 列
      columnsList: [],
      // 增加编辑状态, 保存状态, 用于操作数据 避免干扰原数据渲染
      cloneDataList: []
    }
  },
  methods: {
    getData: function() {
      var self = this;
      self.loading = true;
      $http.get('json/editTable.txt').then(function(res) {
        self.dataList = res.data.Items;
        self.loading = false;
      });
    },
  },
  created: function() {
    this.getData();
  }
});

Напишем отрендеренный столбец снова по правилам iview:

//...

      /**
       * @name columnsList (浏览器 渲染的列)  
       * @author catkin
       * @see https://www.iviewui.com/components/table
       * @param 
       * { 
       *  titleHtml : 渲染带有html的表头 列: '资金<em class="blue" style="color:red">来源</em>'
       *  editable  : true,可编辑的列 必须有字段 
       *  option    : 渲染的下拉框列表,如果需要与数据库交互的值与显示的值不同,须使用[{value:'value',label:'label'}]的形式,下面有例子
       *  date      : 渲染成data类型 ,可选参数: 
       *              date | daterange: yyyy-MM-dd (默认)
       *              datetime | datetimerange: yyyy-MM-dd HH:mm:ss
       *              year: yyyy
       *              month: yyyy-MM
       *  input     : 渲染input类型 ,可选参数为html5所有类型 (额外增加 textarea 属性), 默认text
       *  handle    : 数组类型, 渲染操作方式,目前只支持 'edit', 'delete'
       * }
       * @version 0.0.1
       */

columnsList: [{
    width: 80,
    type: 'index',
    title: '序号',
    align: 'center'
}, {
    align: 'center',
    title: '项目编号',
    key: 'PRJCODE'
}, {
    align: 'center',
    title: '项目名称',
    titleHtml: '项目名称 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'PRJNAME',
    editable: true
}, {
    align: 'center',
    title: '项目分类',
    titleHtml: '项目分类 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'PRJTYPE',
    option: ['产业项目', '基础设施', '民生项目', '住宅项目'],
    editable: true
}, {
    align: 'center',
    title: '建设单位',
    titleHtml: '建设单位 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'JSUNIT',
    editable: true
}, {
    align: 'center',
    title: '流程分类',
    titleHtml: '流程分类 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'FLOW_TYPE_CODE',
    option: [{
      value: 'A01',
      label: '建筑-出让'
    }, {
      value: 'A02',
      label: '建筑-划拨'
    }, {
      value: 'B01',
      label: '市政-绿化'
    }, {
      value: 'B02',
      label: '市政-管线'
    }],
    editable: true
}, {
    align: 'center',
    title: '开工时间',
    titleHtml: '开工时间 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'DATE_START',
    //这里在后面处理的时候会分割成['month','yyyy-MM']的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式
    date: 'month_yyyy-MM',   
    editable: true
}, {
    align: 'center',
    title: '竣工时间',
    titleHtml: '竣工时间 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'DATE_END',
    date: 'month_yyyy-MM',
    editable: true
}, {
    align: 'center',
    title: '建设内容',
    titleHtml: '建设内容 <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'CONTENT',
    input: 'textarea',
    editable: true
}, {
    align: 'center',
    title: '总投资(万元)',
    titleHtml: '总投资<br />(万元) <i class="ivu-icon ivu-icon-edit"></i>',
    key: 'INVEST_ALL',
    input: 'number',
    editable: true
}, {
    title: '操作',
    align: 'center',
    width: 150,
    key: 'handle',
    handle: ['edit', 'delete']
}]

//...

К этому моменту страница уже должна отображать таблицу.

Так как я хочу отредактировать данные, и мое требование - отредактировать всю строку, а данные будут обновляться синхронно при редактировании, то возникает проблема

В vue, когда данные обновляются, представление будет соответствующим образом обновлено.Представьте, что я принадлежу значению в поле ввода, которое запускает обновление данных, а затем запускает обновление представления, тогда каждый раз, когда я печатаю, ввод будет быть не в фокусе. Нет взаимодействия с пользователем. Поэтому мы визуализируем редактируемый динамический контент с помощью cloneDataList и статический дисплей с помощью dataList.

//...
self.dataList = res.data.Items;
// 简单的深拷贝,虽然map会返回新数组,但是数组元素也是引用类型,不能直接改,所以先深拷贝一份
self.cloneDataList = JSON.parse(JSON.stringify(self.dataList)).map(function(item) {
  // 给每行添加一个编辑状态 与 保存状态, 默认都是false
  item.editting = false;
  item.saving = false;
  return item;
});
//...

Далее нам нужно сделать оценку цикла в соответствии со списком столбцов и написать различные функции рендеринга в соответствии с соответствующим ключом.

//全局添加
//根据value值找出数组中的对象元素
function findObjectInOption(value) {
  return function(item) {
    return item.value === value;
  }
}
//动态添加编辑按钮
var editButton = function(vm, h, currentRow, index) {
  return h('Button', {
    props: {
      size: 'small',
      type: currentRow.editting ? 'success' : 'primary',
      loading: currentRow.saving
    },
    style: {
      margin: '0 5px'
    },
    on: {
      click: function() {
        // 点击按钮时改变当前行的编辑状态, 当数据被更新时,render函数会再次执行,详情参考https://cn.vuejs.org/v2/api/#render
        // handleBackdata是用来删除当前行的editting属性与saving属性
        var tempData = vm.handleBackdata(currentRow)
        if (!currentRow.editting) {
          currentRow.editting = true;
        } else {
          // 这里也是简单的点击编辑后的数据与原始数据做对比,一致则不做操作,其实更好的应该遍历所有属性并判断
          if (JSON.stringify(tempData) == JSON.stringify(vm.dataList[index])) {
            console.log('未更改');
            return currentRow.editting = false;
          }
          vm.saveData(currentRow, index)
          currentRow.saving = true;
        }
      }
    }
  }, currentRow.editting ? '保存' : '编辑');
};
//动态添加 删除 按钮
var deleteButton = function(vm, h, currentRow, index) {
  return h('Poptip', {
      props: {
        confirm: true,
        title: currentRow.WRAPDATASTATUS != '删除' ? '您确定要删除这条数据吗?' : '您确定要对条数据撤销删除吗?',
        transfer: true,
        placement: 'left'
      },
      on: {
        'on-ok': function() {
          vm.deleteData(currentRow, index)
        }
      }
    },
    [
      h('Button', {
        style: {
          color: '#ed3f14',
          fontSize: '18px',
          padding: '2px 7px 0',
          border: 'none',
          outline: 'none',
          focus: {
            '-webkit-box-shadow': 'none',
            'box-shadow': 'none'
          }
        },
        domProps: {
          title: '删除'
        },
        props: {
          size: 'small',
          type: 'ghost',
          icon: 'android-delete',
          placement: 'left'
        }
      })
    ]);
};


//methods中添加
init: function() {
  console.log('init');
  var self = this;
  self.columnsList.forEach(function(item) {
    // 使用$set 可以触发视图更新
    // 如果含有titleHtml属性 将其值填入表头
    if (item.titleHtml) {
      self.$set(item, 'renderHeader', function(h, params) {
        return h('span', {
          domProps: {
            innerHTML: params.column.titleHtml
          }
        });
      });
    }
    // 如果含有操作属性 添加相应按钮
    if (item.handle) {
      item.render = function(h, param) {
        var currentRow = self.cloneDataList[param.index];
        var children = [];
        item.handle.forEach(function(item) {
          if (item === 'edit') {
            children.push(editButton(self, h, currentRow, param.index));
          } else if (item === 'delete') {
            children.push(deleteButton(self, h, currentRow, param.index));
          }
        });
        return h('div', children);
      };
    }
    //如果含有editable属性并且为true
    if (item.editable) {
      item.render = function(h, params) {
        var currentRow = self.cloneDataList[params.index];
        // 非编辑状态
        if (!currentRow.editting) {
          // 日期类型单独 渲染(利用工具暴力的formatDate格式化日期)
          if (item.date) {
            return h('span', self.utils.formatDate(currentRow[item.key], item.date.split('_')[1]))
          }
          // 下拉类型中value与label不一致时单独渲染
          if (item.option && self.utils.isArray(item.option)) {
            // 我这里为了简单的判断了第一个元素为object的情况,其实最好用every来判断所有元素
            if (typeof item.option[0] === 'object') {
              return h('span', item.option.find(findObjectInOption(currentRow[item.key])).label);
            }
          }
          return h('span', currentRow[item.key]);
        } else {
        // 编辑状态
          //如果含有option属性
          if (item.option && self.utils.isArray(item.option)) {
            return h('Select', {
              props: {
                // ***重点***:  这里要写currentRow[params.column.key],绑定的是cloneDataList里的数据
                value: currentRow[params.column.key]
              },
              on: {
                'on-change': function(value) {
                  self.$set(currentRow, params.column.key, value)
                }
              }
            }, item.option.map(function(item) {
              return h('Option', {
                props: {
                  value: item.value || item,
                  label: item.label || item
                }
              }, item.label || item);
            }));
          } else if (item.date) {
            //如果含有date属性
            return h('DatePicker', {
              props: {
                type: item.date.split('_')[0] || 'date',
                clearable: false,
                value: currentRow[params.column.key]
              },
              on: {
                'on-change': function(value) {
                  self.$set(currentRow, params.column.key, value)
                }
              }
            });
          } else {
            // 默认input
            return h('Input', {
              props: {
                // type类型也是自定的属性
                type: item.input || 'text',
                // rows只有在input 为textarea时才会起作用
                rows: 3,
                value: currentRow[params.column.key]
              },
              on: {
                'on-change'(event) {
                  self.$set(currentRow, params.column.key, event.target.value)
                }
              }
            });
          }
        }
      };
    }
  });
},
// 还原数据,用来与原始数据作对比的
handleBackdata: function(object) {
  var clonedData = JSON.parse(JSON.stringify(object));
  delete clonedData.editting;
  delete clonedData.saving;
  return clonedData;
}

Здесь почти готово, добавляем функции сохранения данных и удаления данных

// 保存数据
saveData: function(currentRow, index) {
  var self = this;
  // 修改当前的原始数据, 就不需要再从服务端获取了
  this.$set(this.dataList, index, this.handleBackdata(currentRow))
  // 需要保存的数据
  // 模拟ajax
  setTimeout(function() {
    充值编辑与保存状态
    currentRow.saving = false;
    currentRow.editting = false;
    self.$Message.success('保存完成');
    console.log(self.dataList);
  }, 1000)
},
// 删除数据
deleteData: function(currentRow, index) {
  var self = this;
  console.log(currentRow.ID);
  setTimeout(function() {
    self.$delete(self.dataList, index)
    self.$delete(self.cloneDataList, index)
    vm.$Message.success('删除成功');
  }, 1000)
},

Полный код editTable.js

// 根据数据中下拉的值找到对应的对象
function findObjectInOption(name) {
  return function(item) {
    return item.value === name;
  }
}
var editButton = function(vm, h, currentRow, index) {
  return h('Button', {
    props: {
      size: 'small',
      type: currentRow.editting ? 'success' : 'primary',
      loading: currentRow.saving
    },
    style: {
      margin: '0 5px'
    },
    on: {
        click: function() {
          // 点击按钮时改变当前行的编辑状态,当数据被更新时,render函数会再次执行,详情参考https://cn.vuejs.org/v2/api/#render
          // handleBackdata是用来删除当前行的editting属性与saving属性
          var tempData = vm.handleBackdata(currentRow)
          if (!currentRow.editting) {
            currentRow.editting = true;
          } else {
            // 这里也是简单的点击编辑后的数据与原始数据做对比,一致则不做操作,其实更好的应该遍历所有属性并判断
            if (JSON.stringify(tempData) == JSON.stringify(vm.dataList[index])) {
              console.log('未更改');
              return currentRow.editting = false;
            }
            vm.saveData(currentRow, index)
            currentRow.saving = true;
          }
        }
    }
  }, currentRow.editting ? '保存' : '编辑');
};
//动态添加 删除 按钮
var deleteButton = function(vm, h, currentRow, index) {
  return h('Poptip', {
      props: {
        confirm: true,
        title: currentRow.WRAPDATASTATUS != '删除' ? '您确定要删除这条数据吗?' : '您确定要对条数据撤销删除吗?',
        transfer: true,
        placement: 'left'
      },
      on: {
        'on-ok': function() {
          vm.deleteData(currentRow, index)
        }
      }
    },
    [
      h('Button', {
        style: {
          color: '#ed3f14',
          fontSize: '18px',
          padding: '2px 7px 0',
          border: 'none',
          outline: 'none',
          focus: {
            '-webkit-box-shadow': 'none',
            'box-shadow': 'none'
          }
        },
        domProps: {
          title: '删除'
        },
        props: {
          size: 'small',
          type: 'ghost',
          icon: 'android-delete',
          placement: 'left'
        }
      })
    ]);
};
var vm = new Vue({
  el: '#editTableCtrl',
  data: function() {
    return {
      loading: true,
      //表格的数据源
      dataList: [],
      /**
       * @name columnsList (浏览器 渲染的列)  
       * @author ch
       * @see https://www.iviewui.com/components/table
       * @param 
       * { 
       *  titleHtml : 渲染带有html的表头 列: '资金<em class="blue" style="color:red">来源</em>'
       *  editable  : true,可编辑的列 必须有字段 
       *  option    : 渲染的下拉框列表
       *  date      : 渲染成data类型 ,可选参数: 
       *              date | daterange: yyyy-MM-dd (默认)
       *              datetime | datetimerange: yyyy-MM-dd HH:mm:ss
       *              year: yyyy
       *              month: yyyy-MM
       *  input     : 渲染input类型 ,可选参数为html5所有类型 (额外增加 textarea 属性), 默认text
       *  handle    : 数组类型, 渲染操作方式,目前只支持 'edit', 'delete'
       * }
       * @version 0.0.1
       */
      columnsList: [{
        width: 80,
        type: 'index',
        title: '序号',
        align: 'center'
      }, {
        align: 'center',
        title: '项目编号',
        key: 'PRJCODE'
      }, {
        align: 'center',
        title: '项目名称',
        titleHtml: '项目名称 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'PRJNAME',
        editable: true
      }, {
        align: 'center',
        title: '项目分类',
        titleHtml: '项目分类 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'PRJTYPE',
        option: ['产业项目', '基础设施', '民生项目', '住宅项目'],
        editable: true
      }, {
        align: 'center',
        title: '建设单位',
        titleHtml: '建设单位 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'JSUNIT',
        editable: true
      }, {
        align: 'center',
        title: '流程分类',
        titleHtml: '流程分类 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'FLOW_TYPE_CODE',
        option: [{
          value: 'A01',
          label: '建筑-出让'
        }, {
          value: 'A02',
          label: '建筑-划拨'
        }, {
          value: 'B01',
          label: '市政-绿化'
        }, {
          value: 'B02',
          label: '市政-管线'
        }],
        editable: true
      }, {
        align: 'center',
        title: '开工时间',
        titleHtml: '开工时间 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'DATE_START',
        //这里在后面处理的时候会分割成['month','yyyy-MM']的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式
        date: 'month_yyyy-MM',
        editable: true
      }, {
        align: 'center',
        title: '竣工时间',
        titleHtml: '竣工时间 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'DATE_END',
        date: 'month_yyyy-MM',
        editable: true
      }, {
        align: 'center',
        title: '建设内容',
        titleHtml: '建设内容 <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'CONTENT',
        input: 'textarea',
        editable: true
      }, {
        align: 'center',
        title: '总投资(万元)',
        titleHtml: '总投资<br />(万元) <i class="ivu-icon ivu-icon-edit"></i>',
        key: 'INVEST_ALL',
        input: 'number',
        editable: true
      }, {
        title: '操作',
        align: 'center',
        width: 150,
        key: 'handle',
        handle: ['edit', 'delete']
      }],
      // 增加编辑状态, 保存状态, 用于操作数据 避免干扰原数据渲染
      cloneDataList: []
    }
  },
  methods: {
    getData: function() {
      var self = this;
      self.loading = true;
      $http.get('json/editTable.txt').then(function(res) {
        // 给每行添加一个编辑状态 与 保存状态
        self.dataList = res.data.Items;
        self.cloneDataList = JSON.parse(JSON.stringify(self.dataList)).map(function(item) {
          item.editting = false;
          item.saving = false;
          return item;
        });
        self.loading = false;
      });
    },
    //初始化数据
    //methods中添加
    init: function() {
      console.log('init');
      var self = this;
      self.columnsList.forEach(function(item) {
        // 使用$set 可以触发视图更新
        // 如果含有titleHtml属性 将其值填入表头
        if (item.titleHtml) {
          self.$set(item, 'renderHeader', function(h, params) {
            return h('span', {
              domProps: {
                innerHTML: params.column.titleHtml
              }
            });
          });
        }
        // 如果含有操作属性 添加相应按钮
        if (item.handle) {
          item.render = function(h, param) {
            var currentRow = self.cloneDataList[param.index];
            var children = [];
            item.handle.forEach(function(item) {
              if (item === 'edit') {
                children.push(editButton(self, h, currentRow, param.index));
              } else if (item === 'delete') {
                children.push(deleteButton(self, h, currentRow, param.index));
              }
            });
            return h('div', children);
          };
        }
        //如果含有editable属性并且为true
        if (item.editable) {
          item.render = function(h, params) {
            var currentRow = self.cloneDataList[params.index];
            // 非编辑状态
            if (!currentRow.editting) {
              // 日期类型单独 渲染(利用工具暴力的formatDate格式化日期)
              if (item.date) {
                return h('span', self.utils.formatDate(currentRow[item.key], item.date.split('_')[1]))
              }
              // 下拉类型中value与label不一致时单独渲染
              if (item.option && self.utils.isArray(item.option)) {
                // 我这里为了简单的判断了第一个元素为object的情况,其实最好用every来判断所有元素
                if (typeof item.option[0] === 'object') {
                  return h('span', item.option.find(findObjectInOption(currentRow[item.key])).label);
                }
              }
              return h('span', currentRow[item.key]);
            } else {
              // 编辑状态
              //如果含有option属性
              if (item.option && self.utils.isArray(item.option)) {
                return h('Select', {
                  props: {
                    // ***重点***:  这里要写currentRow[params.column.key],绑定的是cloneDataList里的数据
                    value: currentRow[params.column.key]
                  },
                  on: {
                    'on-change': function(value) {
                      self.$set(currentRow, params.column.key, value)
                    }
                  }
                }, item.option.map(function(item) {
                  return h('Option', {
                    props: {
                      value: item.value || item,
                      label: item.label || item
                    }
                  }, item.label || item);
                }));
              } else if (item.date) {
                //如果含有date属性
                return h('DatePicker', {
                  props: {
                    type: item.date.split('_')[0] || 'date',
                    clearable: false,
                    value: currentRow[params.column.key]
                  },
                  on: {
                    'on-change': function(value) {
                      self.$set(currentRow, params.column.key, value)
                    }
                  }
                });
              } else {
                // 默认input
                return h('Input', {
                  props: {
                    // type类型也是自定的属性
                    type: item.input || 'text',
                    // rows只有在input 为textarea时才会起作用
                    rows: 3,
                    value: currentRow[params.column.key]
                  },
                  on: {
                    'on-change'(event) {
                      self.$set(currentRow, params.column.key, event.target.value)
                    }
                  }
                });
              }
            }
          };
        }
      });
    },
    saveData: function(currentRow, index) {
      var self = this;
      // 修改当前的原始数据, 就不需要再从服务端获取了
      this.$set(this.dataList, index, this.handleBackdata(currentRow))
      // 需要保存的数据
      // 模拟ajax
      setTimeout(function() {
        // 重置编辑与保存状态
        currentRow.saving = false;
        currentRow.editting = false;
        self.$Message.success('保存完成');
        console.log(self.dataList);
      }, 1000)
    },
    // 删除数据
    deleteData: function(currentRow, index) {
      var self = this;
      console.log(currentRow.ID);
      setTimeout(function() {
        self.$delete(self.dataList, index)
        self.$delete(self.cloneDataList, index)
        vm.$Message.success('删除成功');
      }, 1000)
    },
    // 还原数据,用来与原始数据作对比的
    handleBackdata: function(object) {
      var clonedData = JSON.parse(JSON.stringify(object));
      delete clonedData.editting;
      delete clonedData.saving;
      return clonedData;
    }
  },
  created: function() {
    this.getData();
    this.init();
  }
});

Суммировать

На это ушло два-три дня, и поначалу все было запутанно.В этот период я ​​тоже пытался использовать слот для достижения этого, но все равно не так понятно, как этот метод (командники его понимают) , в общем-то она есть в columnsList Настроить какие-то атрибуты, а потом возвращать разные значения в функции рендеринга на основе этих атрибутов.Идея еще очень проста.

Это первый раз, когда я пишу статью, и я также новичок в vue, поэтому давайте обойдемся ^_^, добро пожаловать, чтобы оставить сообщение для исправления

Эта демонстрация размещена на моем GitHub вместе с демонстрацией, которую я написал, когда изучал Vue. Добро пожаловать в звезду...  

github: GitHub.com/catkinwood/vu…

Ссылаться на