一周的时间, 几乎每天都要工作十几个小时, 敲代码 + 写作文, 界面原型算是完成了, 下一步是写内核的 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
若有有问题, 请留言交流.
来源: https://www.cnblogs.com/idlewater/p/12453865.html