1. 程式人生 > >VUE實現Studio管理後臺(完結):標籤式輸入、名值對輸入、對話方塊(modal dialog)

VUE實現Studio管理後臺(完結):標籤式輸入、名值對輸入、對話方塊(modal dialog)

一週的時間,幾乎每天都要工作十幾個小時,敲程式碼+寫作文,介面原型算是完成了,下一步是寫核心的HTML處理引擎,純JS實現。本次實戰展示告一段落,等RXEditor下一個版本完成,再繼續分享吧。
剩下的功能:標籤式輸入、名值對輸入、對話方塊(modal dialog),邊框輸入,全部完成。
css class輸入,樣式跟屬性輸入,效果:

對話方塊(model dialog效果)

前幾期功能效果總覽:

標籤輸入框用來輸入CSS class,名字一如既往的好聽,就叫RxLabelInput吧。
輸入值一個數組,因為有多處要運算元組,增、刪、改、克隆、比較等。比較好的一個方式是把Array類用繼承的方式重寫一下,把這寫方法加到裡面。但是RXEidtor核心用純JS實現,並放在一個iFrame裡面,它跟主介面只能通過windows message傳遞資料,帶有方法的類無法作為訊息被傳遞,暫時先不用這個方法,只把相關功能抽取成獨立函式,放在valueOperate.js裡面。

如果以後陣列操作量更大,再考慮轉成一個通用的陣列類。
前幾期介紹過,使用計算屬性changed來標識資料是否被修改過,changed計算屬性內部,需要比較兩個值是否相等,普通字串不會有問題,要比較陣列用這樣的方式最方便,先排序、轉成字串、比較字串:

aValue.sort().toString() === bValue.sort().toString()

陣列的sort方法會改變原來的陣列值,會引發資料重新整理,從而再次呼叫計算屬性,形成死迴圈,除錯了很長時間,就算空陣列也會死迴圈。所以,需要把資料複製一份出來,再比較:

if(Array.isArray(a) && Array.isArray(b)){
  //複製陣列
  let aValue = a.concat()
  //複製陣列
  let bValue = b.concat()
  //比較陣列
  return aValue.sort().toString() === bValue.sort().toString() 
}

 

元件程式碼:

<template>
  <div class="label-list">
    <div 
      class="label-item"
      v-for = "val in inputValue"
    >
      {{val}} 
      <span 
        class="remove-button"
        @click="remove(val)"
      >×</span>
    </div>
    <div style="width: 100%"></div>
    <div class="add-button"
      @click="addClick"
    >+</div>
    <div style="width: 100%"></div>
    <input 
      v-show="isAdding" 
      v-model="newValue" 
      autofocus="autofocus" 
      :placeholder="$t('widgets.enter-message')"
      @keyup.13 = "finishAdd"
      ref="inputControl"
    />
  </div>
</template>

<script>
import {addToArray, removeFromArray} from './valueOperate'

export default {
  props:{
    value:{ default:[] }, 
  },
  computed:{
    inputValue: {
      get:function() {
        return this.value;
      },
      set:function(val) {
        this.$emit('input', val);
      },
    },
  },
  data () {
    return {
      isAdding : false,
      newValue : '',
    }
  },
  methods: {
    addClick(){
      this.isAdding = true; 
      this.$refs.inputControl.style.display = 'block'
      this.$refs.inputControl.focus()
    },
    finishAdd(){
      if(this.newValue){
        this.newValue.split(' ').forEach((val)=>{
          if(val){
            addToArray(val, this.inputValue)
          }
        })
        this.newValue = ''
      }

      this.isAdding = false
    },
    remove(val){
      removeFromArray(val, this.inputValue)
    }

  },
}
</script>

<style>
 .label-list{
    background: rgba(0,0,0, 0.15);
    display: flex;
    flex-flow: row;
    flex-wrap: wrap;
    padding:10px;
  }

  .label-list .label-item{
    padding:0 3px;
    background: rgba(255,255,255, 0.15);
    margin:1px;
    border-radius: 3px;
    height: 24px;
    display: flex;
    align-items: center;
  }

  .label-list .remove-button{
    cursor: pointer;
    margin-left: 2px;
  }

  .label-list .add-button{
    background: rgba(255,255,255, 0.15);
    width: 24px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 3px;
    margin: 1px;
    margin-top:3px;
    font-size: 16px;
    padding-bottom:3px;
    cursor: pointer;
  }

  .label-list input{
    outline: 0;
    border: 0;
    background: transparent;
    color: #fff;
    margin-top:4px;
  }
</style>

用於輸入html屬性(attributes)和樣式(style)的名值對輸入控制元件,也有一個拉風的名字:RxNameValueInput。

這個控制元件的傳入值v-model是一個物件,作為一個物件,動態增刪屬性再加排序,會稍微有些不便,所以元件內部處理時,把這個物件轉換成一個二維陣列:

mounted () {
  for(var name in this.inputValue){
    this.valueArray.push([name, this.inputValue[name]])
  }
},

然後watch這個陣列,當它有變化時,逆向轉化成物件,相當於完成一個雙向繫結,逆向轉化程式碼:

watch: {
  valueArray() {
    this.inputValue = {}
    for(var i = 0; i < this.valueArray.length; i++){
      let name = this.valueArray[i][0]
      let value = this.valueArray[i][1]
      this.inputValue[name] = value
    }
  }
}

 

整個元件的程式碼:

<template>
  <div class="name-value-box">
    <div class="name-value-row"
      v-for="(item, i) in valueArray"
    >
      <div class="name-input">
        <input v-model="item[0]"
          @blur = "nameBlur(i)"
        >
      </div>
      <div class="separator">:</div>
      <div class="value-input">
        <input v-model="item[1]">
      </div>
      <div class="clear-button"
        @click="remove(i)"
      >×</div>
    </div>
    <div class="name-value-row">
      <div class="name-input">
        <input 
          v-model="newName"
          @keyup.13 = "addNew"
          @blur = "newBlur"
          ref="newName"
        >
      </div>
      <div class="separator">:</div>
      <div class="value-input">
        <input 
          v-model="newValue"
          @keyup.13 = "addNew"
          @blur = "newBlur"
        >
      </div>
      <div class="button-placeholder"
      ></div>
    </div>
  </div>
</template>

<script>
export default {
  props:{
    value:{ default:{} }, 
  },
  computed:{
    inputValue: {
      get:function() {
        return this.value;
      },
      set:function(val) {
        this.$emit('input', val);
      },
    },
  },
  data () {
    return {
      valueArray : [],
      newName : '',
      newValue : '',
    }
  },
  mounted () {
    for(var name in this.inputValue){
      this.valueArray.push([name, this.inputValue[name]])
    }
  },
  methods: {
    addClick(){
    },

    nameBlur(i){
      this.valueArray[i][0] = this.valueArray[i][0].trim()
      if(!this.valueArray[i][0]){
        this.remove(i)
      }
    },

    remove(i){
      this.valueArray.splice(i, 1)
    },

    addNew(){
      this.newName = this.newName.trim()
      if(this.newName && !this.exist(this.newName)){
        this.valueArray.push([this.newName, this.newValue])
        this.newName = ''
        this.newValue = ''
        this.$refs.newName.focus()
      } 
    },

    newBlur(){
      this.newName = this.newName.trim()
      this.newValue = this.newValue.trim()
      if(this.newName && this.newValue){
        this.addNew()
      }
    },

    exist(name){
      for(var i = 0; i < this.valueArray.length; i++){
        if(this.valueArray[i][0] === name){
          return true
        }
      }
      return false
    }
  },
  watch: {
    valueArray() {
      this.inputValue = {}
      for(var i = 0; i < this.valueArray.length; i++){
        let name = this.valueArray[i][0]
        let value = this.valueArray[i][1]
        this.inputValue[name] = value
      }
    }
  }

}
</script>

<style>
 .name-value-box{
    background: rgba(0,0,0, 0.15);
    display: flex;
    flex-flow: column;
    padding:10px;
  }

  .name-value-box .add-button{
    background: rgba(255,255,255, 0.15);
    width: 24px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 3px;
    margin: 1px;
    margin-top:3px;
    font-size: 16px;
    padding-bottom:3px;
    cursor: pointer;
  }

  .name-value-row{
    width: 100%;
    display: flex;
    flex-flow: row;
    height: 24px;
    align-items: center;
    font-size: 11px;
  }

  .name-value-row .name-input input, .name-value-row .value-input input{
    width: 100%;
    background: transparent;
    color:#bababa;
    outline: 0;
    border: 0;
  }

  .name-value-row .separator{
    width: 5px;
    display: flex;
    justify-content: center;
    flex-shrink: 0;
    color: #bababa;
  }

  .name-value-row .name-input{
    flex: 1;
  }

  .name-value-row .value-input{
    flex: 1.5;
    padding-left:3px;
  }

  .name-value-row .clear-button{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 17px;
    background: rgba(255,255,255,0.1);
    border-radius: 3px;
    margin:1px;
    font-size: 12px;
    padding-bottom: 3px;
    cursor: pointer;
  }

  .name-value-row .button-placeholder{
    width: 20px;
    height: 20px;
    background: transparent;
  }

</style>

 

還實現了一個邊框輸入控制元件,這個控制元件沒有成長為通用控制元件的潛力,就不介紹了,感興趣的直接看原始碼,名字叫:RxBorderInput。

最後實現的一個控制元件時對話方塊 ,Modal Dialog,目前有兩處地方用到它,一處時主題選擇對話方塊,一處時關於(about)對話方塊。

這兩處共用了通用對話方塊Modal,通過v-model傳入控制對話方塊是否顯示的值,通過卡槽Slot傳入對話方塊內容,Modal程式碼:

<template>
  <div v-if="inputValue" class="modal-mask" @click="inputValue = false">
    <div 
      class="modal"
      :style="{
        top : top,
        left : left,
        width :width,
        height : height,
      }" 
      @click="modalClick"
    >
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Modal',
  props:{
    value:{ default:'' }, 
    width:{ default: '800px'},
    height:{ default: 'calc(100vh - 80px)'},
    top:{default: '40px'},
    left:{default: 'calc(50% - 400px)'},
  },
  computed:{
    inputValue: {
      get:function() {
        return this.value;
      },
      set:function(val) {
        this.$emit('input', val);
      },
    },

  },
  data () {
    return {
    }
  },

  methods: {
    modalClick(event){
      event.stopPropagation()
    },
  },
}
</script>

<style>
.modal-mask{
  position: fixed;
  z-index: 9999;
  top:0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(20, 20, 20, 0.9);
}
.modal-mask .modal{
  position: fixed;
  top:50%;
  left:50%;
  background: #fff;
  box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 0.1); 
  transform: all 0.3s;
  display: flex;
  flex-flow: column;
  color: #474747;
}

</style>

 

還可以通過屬性傳入對話方塊寬、高、位置等資訊。呼叫樣例,也是about對話方塊的程式碼:

<template>
  <Modal v-model="inputValue" 
    width='600px'
    height='400px'
    top ="calc(50% - 200px)"
    left ="calc(50% - 300px)"
  >
    <div class="dialog-head">
      <div><i class="fas fa-question-circle"></i> {{$t('about.about-title')}} </div>
      <span 
        class="close-button"
        @click="inputValue = false"
      >×</span>
    </div>
    <div class="dialog-body about-content">
      本程式是RXEditor第二版的介面原型。<br/>
      基於VUE實現,程式碼已轉入RXeditor專案。<br />
      本原型不再維護,僅供學習參考。<br />
      RXEditor是一個開源的,視覺化的,HTML編輯工具,基於Bootstrap實現。<br />
      RXEditor 程式碼地址:<a href="https://github.com/vularsoft/rxeditor" target="_blank">https://github.com/vularsoft/rxeditor</a>
      演示地址:<a href="https://vular.cn/rxeditor/" target="_blank" >https://vular.cn/rxeditor</a>
    </div>
    <div class="dialog-footer">
      <div class="dialog-button confirm-btn"
        @click="inputValue = false"
      >{{$t('about.close')}}</div>
    </div>
  </Modal>
</template>

<script>
import Modal from './Modal.vue'
export default {
  name: 'AboutDialog',
  components:{
    Modal,
  },
  props:{
    value:{ default:'' }, 
  },
  computed:{
    inputValue: {
      get:function() {
        return this.value;
      },
      set:function(val) {
        this.$emit('input', val);
      },
    },
  },
}
</script>

<style>
.about-content{
  display: flex;
  justify-content: center;
  align-items:flex-start;
  font-size:14px;
  line-height: 32px;
  padding-left: 40px;
}

.about-content a{
  color: #75b325;
}

.about-content a:hover{
  color: #60921e;
  text-decoration: underline;
}
</style>

 

到此為止,本是實戰專案全部完成,感謝大家的閱讀、關注。接下來會把這些程式碼應用在RxEditor中,具體是否要分享RxEditor核心,要看以後個人精力與時間。

本展示專案全部程式碼,請參考Github:https://github.com/vularsoft/studio-ui
若有有問題,請留言交