zoukankan      html  css  js  c++  java
  • VUE实现Studio管理后台(四):状态模式实现窗口停靠,灵动、自由

    昨天做的tabs窗口,非常满意,今天乘胜追击,把它做成了可以根据自身大小改变显示样式,自身宽度过小时,tab页可以浮动停靠其一侧。具体效果:
    左侧

    右侧

    向来喜欢简单明了的东西,所以想实现的简单一点,无奈现实不允许啊,功能实在有一丢丢复杂。硬着头皮搞了整整一下午,终于完成。
    左侧跟右侧窗口,要使用同一个控件,尽量增加代码的可复用性,控件的状态就有些多:正常显示(普通tabs窗口),列表(显示图标跟标题,点击时弹出tab页),迷你列表(只显示图标,点击时弹出tab页)。
    控件在界面左侧时,tab页弹出在其右侧。反之,控件在界面右侧时,tab页弹出在其左侧。
    从正常tabs,缩小到列表显示时,所有tab都是不被激活的。从列表放大到正常tabs,要默认一个标签(tab)是被选中的。
    这么多的状态要求,代码很容易就乱掉。不过还好,设计模式中有一个叫“状态模式”的,可以很好的解决这个问题,缺点就是初期代码量稍大,优点是便于后期管理。
    昨天做了两个tabs控件,一个是WidgetTabs,另外一个是PageTabs,后者现在还能满足我们的需求,只需要修改WidgetTabs这一个就行。
    昨天实现的一些代码删掉,首先重写模板,根据模板写脚本代码,可以让脚本代码更实用些,就像测试驱动的开发里,先写测试再写代码,是一个道理。
    还有,差点忘了。昨天的代码里,把所有的style样式都放在style.css这个文件里了,让后vue全局引入,随着我们写的控件越来越多,这个文件会越来越臃肿,不便于管理。这次把WidgetTabs相关的style代码,拿到vue组件里面。
    先看模板代码:

    <template>
      <div class="widget-tabs" :class="stateClass" ref="widget">
        <ul  class="heads">
          <li v-for="tab in tabs" class="item" :class="{ 'active': tab.isShow }" 
            @click="click(tab)">
            <div v-show="showIcon" class="tab-icon"><i :class="tab.icon"></i></div> 
            <span v-show="showTitle"> {{ tab.name }}</span>
          </li>
        </ul>
        <div v-show="showTabBody" class="tab-body" :class="dockLeft?'dock-left':''">
          <div v-show="showTabTitle" class="tab-title">
            <div>{{selectedTab ? selectedTab.name : ''}}</div>
            <div class="tab-close" @click="close">×</div>
          </div>
          <slot></slot>
        </div>
      </div>
    </template>

    顶层的DIV是我们这个控件的壳子,class对应三个状态的三个css class:
    1、缺省状态,空字符串
    2、列表状态,middle-size
    3、迷你列表状态,mini-size
    css代码里根据这个csss class,用不同的方式显示其子元素,从而实现正常显示,或者弹出显示两种风格。
    ref相当于给这个DIV定了一个唯一ID,我们可以在代码里通过这个ID,获取相应的dom元素,从而判断当前控件大小,根据这个大小,调整控件显示样式。
    ul元素显示的是tabs控件的导航标签部分,根据每个tab页的显示或者隐藏来确定标签是否激活,它还有一个功能就是接受鼠标点击事件,传给控制脚本,模板基本没什么逻辑,主要就是显示和接收事件。
    是否显示图标,根据showIcon计算属性确定。
    是否显示标题,根据showTitle计算属性确定。
    整个选项卡body是否显示,根据showTabBody计算属性确定。因为选项卡body有时停靠在控件左侧,有时停靠在控件右侧,这个停靠方式根据属性dockLeft确定,如果停靠在左边dockLeft为true,反之为false。
    tabTitle是停靠时,显示的标题区域:

    根据计算属性showTabTitle确定是否显示。关闭按钮负责接收点击事件,传递给控制器脚本。不管用什么样的方式实现,控制脚本只要能满足模板的这个要求就可以了。相当于接口定了,根据接口设计实现方式。
    前面已经确定要用状态模式实现,根据状态设计三个状态类:
    NormalState(普通tabs控件),MiddleState(列表状态,带标题带图标),MiniState(迷你列表状态,只显示图标)。后两个类有一些共同的操作,比如弹出隐藏选项卡等,可以继承共同的基类:ListState,三个状态类功能上也有一些交集,他们可以有共同的基类State。类关系图如下(好多年没有用UML工具了,用Excel凑合一下):

     

    不仔细看,不知道这个图其实是Excel画的,还以为是哪个高端UML工具做的呢。

    状态类对应的代码:

    class State{
      constructor(context){
        this.context = context
      } 
    
      widthChange(width){
        if(width <=90){
          this.toState(this.context.miniState)
        }
        else if(width <=160){
          this.toState(this.context.middleState)
        }
        else{
          this.toState(this.context.normalState)
        }
      }
    
      showTabBody(){
        return true
      }
    
      showTabTitle(){
        return false
      }
    
      showIcon(){
        return false
      }
    
      showTitle(){
        return true
      }
    
      close(){}
    
      toState(state){
        if(this.context.state !== state){
          if(this.context.state === this.context.normalState){
            this.context.selectedTab.isShow = false
            console.log('dddd')
          }
          if(state === this.context.normalState){
            this.context.selectedTab.isShow = true
          }
          this.context.state = state
        }
      }
    
      stateClass(){
        return ''
      }
    
    }
    class NormalState extends State{
      constructor(context){
        super(context)
      }
    
      clickTab(clickedTab){
        this.context.tabs.forEach(tab => {
          tab.isShow = (tab.name == clickedTab.name)
            this.context.selectedTab = clickedTab
        });
      }
    }
    
    //需要弹出式显示标签内容
    class ListState extends State{
      constructor(context){
        super(context)
      }
    
      showTabBody(){
        return this.context.selectedTab.isShow
      }
    
      showTabTitle(){
        return true
      }
    
      showIcon(){
        return true
      }
    
      showTitle(){
        return true
      }
    
      close(){
        this.context.selectedTab.isShow = false
      }
    
      clickTab(clickedTab){
        this.context.tabs.forEach(tab => {
          if(tab === clickedTab){
            tab.isShow = !tab.isShow
            this.context.selectedTab = clickedTab
          }
          else{
            tab.isShow = false
          }
        });
      }
    }
    
    //该状态显示图标跟标题
    class MiddleState extends ListState{
      constructor(context){
        super(context)
      }
    
      stateClass(){
        return 'middle-size'
      }
    }
    
    //该状态只显示图标
    class MiniState extends ListState{
      constructor(context){
        super(context)
      }
    
      showTitle(){
        return false
      }
    
      stateClass(){
        return 'mini-size'
      }
    }

    控件脚本代码:

    export default {
      name: 'WidgetTabs',
      data() {
        return {
          tabs: [],
          state: null,
          selectedTab :null,
          dockLeft:false,
        }
      },
      
      created() {
        this.tabs = this.$children;
        this.normalState = new NormalState(this)
        this.middleState = new MiddleState(this)
        this.miniState = new MiniState(this)
        this.state = this.normalState
      },
    
      computed: {
        stateClass(){
          return this.state.stateClass()
        },
    
        showIcon(){
          return this.state.showIcon()
        },
    
        showTitle(){
          return this.state.showTitle()
        },
    
        showTabBody(){
          return this.state.showTabBody()
        },
        showTabTitle(){
          return this.state.showTabTitle()
        },
      },
    
      methods: {
        click(clickTab) {
          this.state.clickTab(clickTab)
        },
    
        mouseMove(){
          if(this.$refs.widget){
            this.dockLeft = this.$refs.widget.offsetLeft < 50
            this.state.widthChange(this.$refs.widget.offsetWidth)
          }
        },
    
        mouseDown(event){
          document.addEventListener('mousemove', this.mouseMove)
        },
    
        mouseUp(event){
          document.removeEventListener('mousemove', this.mouseMove)
        },
    
        close(){
          this.state.close()
        }
      },
    
    
      mounted () {
        document.addEventListener('mousedown', this.mouseDown)
        document.addEventListener('mouseup', this.mouseUp)
        this.tabs.forEach(tab => {
          if(tab.isShow){
            this.selectedTab = tab
          }
        });
      },
    
      beforeDestroyed() {
        document.removeEventListener('mousedown', this.mouseDown)
        document.removeEventListener('mouseup', this.mouseUp)
      },
    }

    组件创建时初始化各种状态。需要注意的一点是,需要在窗口变化时动态获取控件宽度,来确定控件是处在哪个状态。JS中DIV没有resize事件,可以通过鼠标事件来代替。我们的窗口大小是通过鼠标拖动实现的,所以跟踪鼠标拖动事件,动态查询控件大小,然后分发事件。
    这个控件至此就完成了,写这个文章的事件比写代码时间长,天生是个程序员不是writer。
    整个项目在这个历史节点的代码,请到我的Github上查看:https://github.com/vularsoft/studio-ui
    找到该历史节点的方法:

    RXEditor是一个Boostrap代码可视化编辑工具,本系列记录了该软件的开发过程,有问题的朋友请在ithub上给我留言。

  • 相关阅读:
    hibernate配置
    关于memset
    Struts2通配符的使用
    Excel表中的数据导入mysql数据库
    蓝桥杯:错误票据
    蓝桥杯:十六进制转八进制
    Android TabHost中切換、修改需要显示的Activity
    Android图片处理(Matrix,ColorMatrix)
    Android gallery滑动惯性问题
    华为手机在开发Android调试时logcat不显示输出信息的解决办法
  • 原文地址:https://www.cnblogs.com/idlewater/p/12422171.html
Copyright © 2011-2022 走看看