vue 案例

lishihuan大约 38 分钟

vue 案例

1. 案例1:H5 下拉加载更多(模拟微信聊天记录)

参考:https://blog.csdn.net/sensation_cyq/article/details/112661714open in new window

  • 原版写法
<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>H5 下拉加载更多(模拟微信聊天记录)</title>
    <style>
        .container {
            width: 300px;
            height: 300px;
            overflow: auto;
            border: 1px solid;
            margin: 10px auto;
        }
 
        .item {
            height: 29px;
            line-height: 30px;
            text-align: center;
            border-bottom: 1px solid #aaa;
        }
    </style>
</head>
<body>
<div id="app">
    <div class="container" ref="container">
        <div class="item">{{loadText+"第"+pageNum+"页"}}</div>
        <div v-for="(item, index) in list" :key="index" class="item">{{item}}</div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    new Vue({
        el: '#app',
        data: {
            scrollHeight: 0,
            list: [],
            loadText:"加载中...",
            pageSize:20,
            pageNum:1,
        },
        mounted() {
            this.initData();
            const container = this.$refs.container;
            //这里的定时是为了列表首次渲染后获取scrollHeight并滑动到底部。
            setTimeout(() => {
                this.scrollHeight = container.scrollHeight;
                container.scrollTo(0, this.scrollHeight);
            }, 10);
            container.addEventListener('scroll', (e) => {
                //这里的2秒钟定时是为了避免滑动频繁,节流
                setTimeout(() => {
                    if(this.list.length>=90){
                        this.loadText = "加载完成";
                        return;
                    }
                    //滑到顶部时触发下次数据加载
                    if (e.target.scrollTop == 0) {
                        //将scrollTop置为10以便下次滑到顶部
                        e.target.scrollTop = 10;
                        //加载数据
                        this.initData();
                        //这里的定时是为了在列表渲染之后才使用scrollTo。
                        setTimeout(() => {
                            e.target.scrollTo(0, this.scrollHeight - 30);//-30是为了露出最新加载的一行数据
                        }, 100);
                    }
                }, 2000);
            });
        },
        methods:{
            //初始数据
            initData() {
                for (var i = 20; i > 0; i--) {
                    this.list.unshift(i)
                }
                this.pageNum++;
            }
        }
    })
</script>
</body>
</html>
  • 改进版
<template>
    <!--缺陷通知页面-->
    <div class="page">
        <!--页面头部-->
        <PageHeader :type="pageType" :title="title" position="center">
            <!--<template #header-right>
                <van-badge :content="taskNum">
                    <div class="page-right">已读</div>
                </van-badge>
            </template>-->
        </PageHeader>
        <!--页面主体-->
        <div class="page-body" :class="pageType === 'tab' ? 'page-tab-body' : ''">
            <div style="overflow-y: scroll; height: 100%;" ref="container">
                <MyLoading :loading="loading" :finished="finished"></MyLoading>
                <!--缺陷通知每一项-->
                <InformListItem type="hidden" v-for="(item, index) in list" :ref="'informlistitem'+index"
                                :detail="item"></InformListItem>
            </div>
        </div>
    </div>
</template>
<script>
    import PageHeader from '@/components/page-header/PageHeader';// 
    import MyLoading from '@/components/MyLoading.vue';// loading 组件-自定义
    import InformListItem from '../components/InformListItem.vue';// 内容主体
    import { Toast } from 'vant';
    import { commonRequestJSON } from '../../../net/commonRequest'

    export default {
        name: 'HiddenInform',
        components: { PageHeader, InformListItem, MyLoading },
        data () {
            return {
                title: '',
                loading: false,
                finished: false,
                refreshing: false,
                queryParams: {
                    pageNum: 0,
                    pageSize: 10,
                    receiveId: this.$store.state.userInfo.userId
                },
                list: [],
                loadText: '',
                dataSumNum: 0,// 列表总数
                scrollToBottomFlag: false,// 已经定位到底部
                informlistitem_height: 0,// 单个子组件高度
                scrollHeight: 0,
            }
        },
        // 初始化页面完成后
        mounted () {
            var this_ = this

            function fistCallback () {
                this_.scrollToBottom()
                //this_.getListItemHeight()
            }

            this.onLoad(fistCallback)
            const container = this.$refs.container
            container.addEventListener('scroll', (e) => {
                //这里的2秒钟定时是为了避免滑动频繁,节流
                setTimeout(() => {
                    if (this.finished) {
                        return
                    }
                    //滑到顶部时触发下次数据加载
                    if (e.target.scrollTop == 0) {
                        //将scrollTop置为10以便下次滑到顶部
                        e.target.scrollTop = 10// 防止一直在定部,导致一直在加载下一页
                        //加载数据
                        var this_ = this
                        function callback () {
                            /**
                             * 如果想加载下一页时,显示出 最新加载的一行,则 使用 this_.scrollHeight - this_.informlistitem_height 注:初始时需要调用 getListItemHeight 获取单行高度
                             setTimeout(() => {
                                    e.target.scrollTo(0, this_.scrollHeight );//-30是为了露出最新加载的一行数据
                                }, 100);
                             */
                            //这里的定时是为了在列表渲染之后才使用scrollTo。
                            this_.$nextTick(() => {
                                e.target.scrollTo(0, this_.scrollHeight );//-30是为了露出最新加载的一行数据
                            })
                        }
                        this.onLoad(callback)
                    }
                }, 1000)
            })
        },
        created () {
            this.title = this.$route.query.pname
            this.queryParams.typeId = this.$route.query.pid
            this.loading = true
        },
        methods: {
            /*数据加载,*/
            onLoad (callback) {
                this.loading = true;
                this.queryParams.pageNum++
                var this_ = this

                function dataCallback (res) {
                    this_.loading = false
                    // 加载状态结束
                    if (res.code == 200) {
                        if (res.rows == undefined || res.rows.length == 0) {
                            this_.finished = true// 数据加载完成
                            this_.loading = false
                            return
                        }
                        res.rows.forEach((item, index) => {
                            if (!item.extraInfo) {
                                item.extraInfo = {}
                            } else {
                                item.extraInfo = JSON.parse(item.extraInfo)
                            }
                            this_.list.unshift(item)
                        })
                        if (this_.list.length == res.total) {
                            this_.finished = true// 数据加载完成
                        }
                        if (typeof callback === 'function') {
                            callback(true)
                        }
                    } else {
                        Toast.fail('数据加载异常!')
                        this_.finished = true
                    }
                }

                function dataErrCallback (data) {
                    // 异常 没有数据
                    Toast.fail('数据加载异常!')
                    this_.loading = false
                    this_.finished = true
                }

                commonRequestJSON('get', 'system/pushContent/findSefMessage', this.queryParams, dataCallback, dataErrCallback)
            },
            // 获取子组件的高度
            getListItemHeight () {
                var this_ = this
                this.$nextTick(() => {
                    this_.informlistitem_height = this_.$refs['informlistitem0'][0].$refs.inform.offsetHeight
                })
            },
            onRefresh () {
                // 清空列表数据
                this.list = []
                this.finished = false
                // 重新加载数据
                // 将 loading 设置为 true,表示处于加载状态
                this.queryParams.pageNum = 0
                this.refreshing = false
                this.onLoad()
            },
            // 初始加载,定位到最低部
            scrollToBottom () {
                var this_ = this
                this.$nextTick(() => {
                    // var div = this_.$refs.container// document.getElementById('data-list-content');div.scrollTop = div.scrollHeight
                    const container = this_.$refs.container
                    //这里的定时是为了列表首次渲染后获取scrollHeight并滑动到底部。
                    this_.scrollHeight = container.scrollHeight
                    container.scrollTo(0, this.scrollHeight)
                })
            }
        },
    }
</script>
<style lang="less" scoped>
    .page {
        &-body {
            overflow: auto;
            background: #F3F6FA;
            padding-bottom: 20px;
        }

        &-right {
            font-size: 36px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .inform[data-v-000b813e] {
            background: #F3F6FA;
        }
    }
</style>

子组件:MyLoading

<template>
    <div>
        <van-loading size="24px" vertical v-if="loading">加载中...</van-loading>
        <div class="van-loading--vertical" style="font-size: 24px" v-if="finished">
            <span class="van-loading__text">加载完成</span>
        </div>
    </div>
</template>

<script>
    /*自定义 loading 组件,结合 vant 的 list 组件,*/
    export default {
        name: 'MyLoading',
        components: {},
        props: {
            loading: {// 加载中
                type: Boolean,
                default: false,
            },
            finished: {// 结束
                type: Boolean,
                default: false,
            },
        },
    }
</script>

子组件 :InformListItem

<template>
    <!-- 缺陷/隐患通知 -->
    <div class="inform" ref="inform">
        xxxx
    </div>
</template>

2. 类似外卖点餐界面的左右侧菜单联动:点击左侧使右侧滚动到对应位置,右侧滚动时选中左侧对应选项

demo\外卖点餐界面的左右侧菜单联动.html

参考:https://blog.csdn.net/liangziqi233/article/details/120352529open in new window

.foods-wrapper { flex: 1;} 右侧区域,通过设置flex: 1 来平分 身下的空间(左侧宽度固定,通过设置flex:1 平分剩余空间),这样处理是 宽度合再一起100%,

如果需求是,左侧菜单能够隐藏,右侧通过平移的方法展示,同时右侧的区域宽度始终是100%,这时可以设置

.foods-wrapper { width: 100%; flex-shrink:0} ,其中 flex-shrink 表示伸缩不变形

2.1效果图

2.2添加依赖:

npm install better-scroll

npm install stylus stylus-loader@3.0.1 --save-dev

2.3vue源码

<template>
  <div>
    <div class="goods">
      <div class="menu-wrapper">
        <ul>
          <li
            class="menu-item"
            v-for="(good, index) in goods"
            :key="index"
            :class="{ current: index === currentIndex }"
            @click="clickMenuItem(index)"
          >
            <span class="text">
              <img class="icon" :src="good.icon" v-if="good.icon" />
              {{ good.name }}
            </span>
          </li>
        </ul>
      </div>
      <div class="foods-wrapper">
        <ul ref="foodsUl">
          <li
            class="food-list-hook"
            v-for="(good, index) in goods"
            :key="index"
          >
            <h1 class="title">{{ good.name }}</h1>
            <ul>
              <li
                class="food-item"
                v-for="(food, index) in good.foods"
                :key="index"
              >
                <div class="icon">
                  <img width="57" height="57" :src="food.icon" />
                </div>
                <div class="content">
                  <h2 class="name">{{ food.name }}</h2>
                  <p class="desc">{{ food.description }}</p>
                  <div class="extra">
                    <span class="count">月售{{ food.sellCount }}份</span>
                    <span>好评率{{ food.rating }}%</span>
                  </div>
                  <div class="price">
                    <span class="now">¥{{ food.price }}</span>
                    <span class="old" v-if="food.oldPrice"
                      >¥{{ food.oldPrice }}</span
                    >
                  </div>
                </div>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>
<script>
import BScroll from "better-scroll";
export default {
  data() {
    return {
      scrollY: 0, // 右侧滑动的Y轴坐标 (滑动过程时实时变化)
      tops: [], // 所有右侧分类li的top组成的数组  (列表第一次显示后就不再变化)
      goods: [
        {
          icon: "http://liangziqi.top/meme-img/126-430.jpg",
          name: "优惠",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "瑶",
              description: "公主",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "安琪拉",
              description: "双马尾",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          icon: "http://liangziqi.top/meme-img/126-430.jpg",
          name: "折扣",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "小乔",
              description: "扇子",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "王昭君",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          name: "法师",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "小乔",
              description: "扇子",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "王昭君",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "王昭君",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "王昭君",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          name: "辅助",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "瑶",
              description: "公主",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "蔡文姬",
              description: "kkk",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "东皇",
              description: "hhh",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "孙膑",
              description: "666",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          name: "射手",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "伽罗",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "后裔",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "狄仁杰",
              description: "hhh",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "公孙离",
              description: "666",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          name: "打野",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "李白",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "百里玄策",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "韩信",
              description: "hhh",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "孙悟空",
              description: "666",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        },
        {
          name: "坦克",
          foods: [
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "亚瑟",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "项羽",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "凯",
              description: "hhh",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "夏侯惇",
              description: "666",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "亚瑟",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "项羽",
              description: "",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "凯",
              description: "hhh",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            },
            {
              icon: "http://liangziqi.top/meme-img/126-430.jpg",
              name: "夏侯惇",
              description: "666",
              sellCount: 1,
              rating: 100,
              price: 2,
              oldPrice: "6"
            }
          ]
        }
      ]
    };
  },
  mounted() {
    this.$nextTick(() => {
      this._initScroll();
      this._initTops();
    });
  },
  computed: {
    // 计算得到当前分类的下标
    currentIndex() {
      // 初始化和相关数据发生了变化时执行
      // 得到条件数据
      const { scrollY, tops } = this;
      // 根据条件计算产生一个结果
      const index = tops.findIndex((top, index) => {
        // scrollY>=当前top && scrollY<下一个top
        return scrollY >= top && scrollY < tops[index + 1];
      });
      // 返回结果
      return index;
    }
  },
  methods: {
    // 初始化滚动
    _initScroll() {
      // 列表显示之后创建
      new BScroll(".menu-wrapper", {
        click: true
      });
      this.foodsScroll = new BScroll(".foods-wrapper", {
        probeType: 2, // 因为惯性滑动不会触发
        click: true
      });

      // 给右侧列表绑定scroll监听
      this.foodsScroll.on("scroll", ({ x, y }) => {
        console.log("scroll", x, y);
        this.scrollY = Math.abs(y);
      });
      // 给右侧列表绑定scroll结束的监听
      this.foodsScroll.on("scrollEnd", ({ x, y }) => {
        console.log("scrollEnd", x, y);
        this.scrollY = Math.abs(y);
      });
    },
    // 初始化tops
    _initTops() {
      // 1. 初始化tops
      const tops = [];
      let top = 0;
      tops.push(top);
      // 2. 收集
      // 找到所有分类的li
      const lis = this.$refs.foodsUl.getElementsByClassName("food-list-hook");
      Array.prototype.slice.call(lis).forEach(li => {
        top += li.clientHeight;
        tops.push(top);
      });

      // 3. 更新数据
      this.tops = tops;
      console.log(tops);
    },

    clickMenuItem(index) {
      console.log(index);
      // 使用右侧列表滑动到对应的位置

      // 得到目标位置的scrollY
      const scrollY = this.tops[index];
      // 立即更新scrollY(让点击的分类项成为当前分类)
      this.scrollY = scrollY;
      // 平滑滑动右侧列表
      this.foodsScroll.scrollTo(0, -scrollY, 300);
    }
  }
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
bottom-border-1px($color) {
  position: relative;
  border: none;

  &:after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background-color: $color;
    transform: scaleY(0.5);
  }
}

.goods {
  display: flex;
  position: absolute;
  top: 15px;
  bottom: 46px;
  width: 100%;
  background: #fff;
  overflow: hidden;

  .menu-wrapper {
    flex: 0 0 80px;
    width: 80px;
    background: #f3f5f7;

    .menu-item {
      display: table;
      height: 54px;
      width: 56px;
      padding: 0 12px;
      line-height: 14px;

      &.current {
        position: relative;
        z-index: 10;
        margin-top: -1px;
        background: #fff;
        color: $green;
        font-weight: 700;
      }

      .icon {
        display: inline-block;
        vertical-align: top;
        width: 12px;
        height: 12px;
        margin-right: 2px;
        background-size: 12px 12px;
        background-repeat: no-repeat;
      }

      .text {
        display: table-cell;
        width: 56px;
        vertical-align: middle;
        bottom-border-1px(rgba(7, 17, 27, 0.1));
        font-size: 12px;
      }
    }
  }

  .foods-wrapper {
    flex: 1;

    .title {
      padding-left: 14px;
      height: 26px;
      line-height: 26px;
      border-left: 2px solid #d9dde1;
      font-size: 12px;
      color: rgb(147, 153, 159);
      background: #f3f5f7;
      text-align: left;
      margin: 0;
    }

    .food-item {
      display: flex;
      margin: 18px;
      padding-bottom: 18px;
      bottom-border-1px(rgba(7, 17, 27, 0.1));

      &:last-child {
        margin-bottom: 0;
      }

      .icon {
        flex: 0 0 57px;
        margin-right: 10px;
      }

      .content {
        flex: 1;
        text-align: left;

        .name {
          margin: 2px 0 8px 0;
          height: 14px;
          line-height: 14px;
          font-size: 14px;
          color: rgb(7, 17, 27);
        }

        .desc, .extra {
          line-height: 10px;
          font-size: 10px;
          color: rgb(147, 153, 159);
        }

        .desc {
          line-height: 12px;
          margin-bottom: 8px;
        }

        .extra {
          .count {
            margin-right: 12px;
          }
        }

        .price {
          font-weight: 700;
          line-height: 24px;

          .now {
            margin-right: 8px;
            font-size: 14px;
            color: rgb(240, 20, 20);
          }

          .old {
            text-decoration: line-through;
            font-size: 10px;
            color: rgb(147, 153, 159);
          }
        }
      }
    }
  }
}

li {
  list-style: none;
}

ul {
  padding: 0;
  margin: 0;
}
</style>

3. 页面滚动到顶部后,往下拖拽执行关闭vue

123

用来模拟手势,向下,关闭弹窗 1.当前容器记录容器顶部 0 (scrollTop) 2.手势向下 3.位移量 >100

<template>
  <div id="member" ref="scrollContainer">
      <!-- 模块1-->
    <member-xinxi />
       <!-- 模块2-->
    <member-xunshi />
    <member-yinhuan />
    <member-jiance />
    <member-xstongji />
  </div>
</template>

<script>
import MemberXinxi from "./userComponents/userxinxi.vue";
import MemberXunshi from "./userComponents/user-xunshi.vue";
import MemberYinhuan from "./userComponents/user-yinhuan.vue";
import MemberJiance from "./userComponents/user-jiance.vue";
import MemberXstongji from "./userComponents/user-xstongji.vue";

export default {
  name: "UserDetail",
  components: {
    MemberXinxi,
    MemberXunshi,
    MemberYinhuan,
    MemberJiance,
    MemberXstongji
  },
  data() {
    return {
        scrollContainer:undefined,// 滚动元素
        scrollTop:0,// 移动量(被滚动元素距离头部的高度)
    };
  },
    mounted() {
        // 添加滚动监听事件
        this.scrollContainer = this.$refs.scrollContainer
        this.scrollContainer.addEventListener('scroll', this.handleScrollEvent);
        this.bindEvent();// 绑定手势
    },
    beforeDestroy() {
        this.scrollContainer.removeEventListener('scroll', this.handleScrollEvent)
    },
    methods: {
        // 获取页面滚动距离
        handleScrollEvent (e) {
            this.scrollTop = this.scrollContainer.scrollTop;// 弹窗 滚动位移量
            //let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;// 页面
            //console.log(this.scrollTop)
        },
        // 注册 手势
        bindEvent () {
            this.scrollContainer.addEventListener('touchstart', (e) => {
                this.touchstart(e)
            })
            this.scrollContainer.addEventListener('touchmove', (e) => {
                this.touchmove(e)
            })
        },
        // 获取初始位置,用于决定 位移量和 位移方向
        touchstart (e) {
            this.lastClientY = e.touches[0].clientY
        },
        /**
         * 用来模拟手势,向下,关闭弹窗
         *  1.当前容器记录容器顶部 0 (scrollTop)
         *  2.手势向下
         *  3.位移量 >100
         * @param e
         */
        touchmove(e){
            var clientY = e.touches[0].clientY
            this.drag_direction = clientY - this.lastClientY > 0 ? 'toBottom' : 'toTop'//定义向上向下
            this.move_distance = Math.abs(clientY - this.lastClientY)// 位移距离
            // 元素一级滚动到顶部了,现在还往下拉,认为用户是想关闭弹窗
            console.log('元素现在高度,位移量'+this.scrollTop+'-------'+this.move_distance+'/-----'+this.drag_direction);
            if(this.scrollTop==0 && this.drag_direction=='toBottom' && this.move_distance>100){
                var data={
                    type:'closeDetail'
                }
                this.$emit('sonHandleCallback', data)
            }
        },
    },
};
</script>

4.tab&锚点

image-20220929140126008
image-20220929140126008
<template>
    <div id="xianlu" ref="scrollContainer">
        <!-- 滑动选项 -->
        <van-tabs class="tab_menu" :active="activeIndex" swipeable @click="onClickTab">
            <van-tab v-for="item in tabMenuList" :key="item" :title="item.title">
                <template #title>
                    <div class="line_horizontal_layout">
                        <div class="tab_menu_title">{{ item.title }}</div>
                    </div>
                </template>
            </van-tab>
        </van-tabs>

        <!-- 引用的组件 -->
        <jichu-xinxi ref="xinxi"/>
        <tai-zhang ref="taizhang"/>
        <xianlu-tongdao ref="xianlu"/>
        <xunshi-tongji ref="xunshi"/>
        <dunshou-tongji ref="dunshou"/>
    </div>
</template>

<script>
    import JichuXinxi from './jichuxinxi.vue'
    import TaiZhang from './taizhang.vue'
    import XianluTongdao from './xianlutongdao.vue'
    import XunshiTongji from './xunshitongji.vue'
    import DunshouTongji from './dunshoutongji.vue'


    export default {
        name: 'Xianludetail',
        components: { JichuXinxi,  TaiZhang, XianluTongdao, XunshiTongji, DunshouTongji },
        data () {
            return {
                scrollContainer:undefined,// 滚动元素
                scrollTop:0,// 移动量(被滚动元素距离头部的高度)
                activeIndex: 0,
                tabMenuList: [
                    {id: 1, title: '基础信息', el: 'xinxi' },
                    { id: 2, title: '台账统计', el: 'taizhang' },
                    { id: 3, title: '线路通道', el: 'xianlu' },
                    { id: 4, title: '巡视统计', el: 'xunshi' },
                 ]
            }
        },
        mounted () {
            // 添加滚动监听事件
            this.scrollContainer = this.$refs.scrollContainer
            this.scrollContainer.addEventListener('scroll', this.handleScrollEvent)
            this.bindEvent();// 绑定手势
        },
        beforeDestroy () {
            this.scrollContainer.removeEventListener('scroll', this.handleScrollEvent)
        },
        methods: {
            onClickTab (item) {
                // 拿到的是下标
                this.activeIndex = item
                const refName = this.tabMenuList[item].el
                if (!refName) return
                const el = this.$refs[refName]
                if (el) {
                    let offsetTop = el.$el ? el.$el.offsetTop : el.offsetTop
                    offsetTop -= 35 + 25 // 减去menu高度,向上预留25
                    if (offsetTop < 0) offsetTop = 0
                    console.log(offsetTop)
                    this.scrollContainer.scrollTo({
                        top: offsetTop,
                        behavior: 'smooth'
                    })
                }
            },
            // 滚动事件
            handleScrollEvent () {
                const arr = []
                this.tabMenuList.forEach((item, index) => {
                    const refName = item.el
                    const el = this.$refs[refName]
                    if (el) {
                        let offsetTop = el.$el ? el.$el.offsetTop : el.offsetTop
                        offsetTop -= 35 + 25 // 减去menu高度,向上预留25
                        if (offsetTop < 0) offsetTop = 0
                        arr.push({
                            offsetTop,
                            index
                        })
                    }
                })
                this.scrollTop = this.scrollContainer.scrollTop
                // 对滚动的值进行判断
                if (this.scrollTop <= arr[0].offsetTop) {
                    this.activeIndex = arr[0].index
                } else if (this.scrollTop <= arr[1].offsetTop) {
                    this.activeIndex = arr[1].index
                } else if (this.scrollTop <= arr[2].offsetTop) {
                    this.activeIndex = arr[2].index
                } else if (this.scrollTop <= arr[3].offsetTop) {
                    this.activeIndex = arr[3].index
                } else if (this.scrollTop <= arr[4].offsetTop) {
                    this.activeIndex = arr[4].index
                } else if (this.scrollTop <= arr[5].offsetTop) {
                    this.activeIndex = arr[5].index
                }
            },
            // 注册 手势
            bindEvent () {
                this.scrollContainer.addEventListener('touchstart', (e) => {
                    this.touchstart(e)
                })
                this.scrollContainer.addEventListener('touchmove', (e) => {
                    this.touchmove(e)
                })
            },
            // 获取初始位置,用于决定 位移量和 位移方向
            touchstart (e) {
                this.lastClientY = e.touches[0].clientY
            },
            /**
             * 用来模拟手势,向下,关闭弹窗
             *  1.当前容器记录容器顶部 0 (scrollTop)
             *  2.手势向下
             *  3.位移量 >100
             * @param e
             */
            touchmove(e){
                var clientY = e.touches[0].clientY
                this.drag_direction = clientY - this.lastClientY > 0 ? 'toBottom' : 'toTop'//定义向上向下
                this.move_distance = Math.abs(clientY - this.lastClientY)// 位移距离
                // 元素一级滚动到顶部了,现在还往下拉,认为用户是想关闭弹窗
                console.log('元素现在高度,位移量'+this.scrollTop+'-------'+this.move_distance+'/-----'+this.drag_direction);
                if(this.scrollTop==0 && this.drag_direction=='toBottom' && this.move_distance>100){
                    var data={
                        type:'closeDetail'
                    }
                    this.$emit('sonHandleCallback', data)
                }
            },
        }
    }
</script>

<style lang="scss" scoped>
    @import "~@/assets/style/mapdetail/style.css";
</style>

时间轴

1233
1233
<!--
 * @Descripttion:
 * @Author: cuixu
 * @Date: 2022-11-01 14:56:00
-->
<template>
    <!--线路区段组件-->

    <div class="mapLine" :class="{check:itemObj.type != 'line'}">
        <div class="mapLine-top">
            <img src="@/assets/mapIcons/top_11.png" alt=""/>
            <span>线路区段</span>
            <img class="mapLine-top-dian" src="@/assets/mapIcons/dian.png" alt=""/>
        </div>
        <!-- 内容 -->
        <div class="line-steps pr" ref="time_warp" >
            <el-steps :space="100" align-center id="steps">
                <el-step v-for="(item, index) in list" :key="index" :title="item.name">
                    <i class="build stepIcon" slot="icon" :title="item.name">{{substrfilter(item.name,1)}}</i>
                </el-step>
            </el-steps>
            <!-- 左右 方向箭
                左侧箭头-->
            <i class="iconfont icon-zhankai2 pa key-left key-direction" v-if="showStart" @click="move('subtract')"></i>
            <i class="iconfont icon-zhankai1 pa key-right key-direction" v-if="showEnd" @click="move('add')"></i>
        </div>
    </div>
</template>

<script>
    import { wayPointForTeam } from '@/api/ydxj/base/basLine'
    import { substrfilter } from '@/utils/common.js'

    export default {
        props: {
            itemObj: {
                type: Object,
                default: () => ({})
            }
        },
        data () {
            return {
                list: [],
                time_width:105,// Steps 步骤条 单个宽带
                showStart: false,
                showEnd: false,
                timeSelector:undefined,// steps 进步器 对象,因为无法通过 ref 所以只能  通过 this.$el.querySelector('#steps')
                maxOffset:0,// 最大偏移量,用于 判断是否 拖拽到最左侧
            }
        },
        created () {
            this.loadData()
        },
        // activated () { this.loadData() },
        methods: {
            loadData () {
                this.list = [];
                this.showStart=false;// 重现隐藏 左右 方向箭头
                this.showEnd=false;
                if (!this.itemObj.lineId) {
                    return
                }
                if (this.itemObj.type == 'line') {
                    // 加载线路
                    wayPointForTeam({ lineId: this.itemObj.lineId,towerId:this.itemObj.towerId }).then(response => {
                        this.list = response.data;
                        this.initTimes();
                    })
                }
            },
            substrfilter (value, length) {
                return substrfilter(value, length)
            },
            initTimes(){
                this.$nextTick(()=>{
                    let warp_width = this.$refs.time_warp.clientWidth-70;// 父容器 宽度
                    let item_width = this.time_width*this.list.length;// 子容器宽度
                    this.maxOffset = item_width - warp_width;
                    if(warp_width<=item_width){// 说明超出容器,需要放开 方向箭头,用于指示方向
                        this.showEnd=true;// 数据加载完成后,并且 超长容器宽度,此时需要显示 右侧 方向箭头
                    }
                    this.timeSelector = this.$el.querySelector('#steps');
                })
            },
            move(type){
                let num=this.time_width;
                this.showStart=true;
                this.showEnd=true;
                if(type=='subtract'){
                    num=-this.time_width;
                }
                let nowLeft = this.timeSelector.scrollLeft+num;//
                this.timeSelector.scrollTo({
                    left: nowLeft,
                    behavior: 'smooth'
                })
                // 因为scrollTo 当前添加动画,导致 修改属性是异步的不会立刻生效,所以 下面直接使用this.timeSelector.scrollLeft进行判断 具有滞后性
                if(nowLeft<=10){// 移动到开始
                    this.showStart=false;
                }
                if(this.maxOffset<=nowLeft){// 移动到 结束了
                    this.showEnd=false;
                }
            },
        }
    }
</script>

<style lang="scss" scoped>
    .mapLine{
        overflow: hidden;
        transition: all 0.6s;
        height: 13.5vh;
        &.check{
            height: 0;
        }
    }
    /deep/ .el-step__title {
        font-size: 12px;
        margin-top: 3px;
        color: #fff;
    }

    /deep/ .el-step__line {
        background: #012e43;
        top: 17px !important
    }

    .line-steps {
        padding: 10px 35px 0 35px;
        height: 10vh;
        display: flex;
        align-items: center;
        justify-content: center;

        .el-steps {
            display: block;
            height: 100%;
            overflow: hidden;
            display: flex;

            .el-step {
                display: inline-block;
                width: 105px;
                padding-top: 6px;
                max-width: 50% !important;
                min-width: 105px;

                .el-step__main {
                    .el-step__title {
                        width: 100%;
                        overflow: hidden;
                        white-space: nowrap;
                        text-overflow: ellipsis;
                    }
                }
            }
        }

        /deep/ .el-step__icon.is-text {
            border-radius: 50%;
            border: 2px solid #00fffd !important;
            border-color: inherit;
            width: 36px;
            height: 36px;
            background: #012e43;
            color: #fefefe;
            cursor:default;
        }

        /*  .stepIcon {
            width: 36px;
            height: 36px;
            background-size: 100% 100%;
            position: absolute;
          }*/

        .build {
            font-style:normal;
            /*background-image: url("~@/assets/mapIcons/yun.png");*/
        }
    }

    /*左右方向箭头*/
    .key-direction{
        top: 23%;
        font-size: 20px;
        color: #54f3f3;
    }
    .key-left{
        left: 14px;
    }
    .key-right{
        right: 14px;
    }

    .key-right {
        -webkit-animation: arrow .8s .5s ease-in-out infinite alternate;
        animation: timebs_animation .8s .5s ease-in-out infinite alternate;
    }

    @-webkit-keyframes timebs_animation {
        100% {
            -webkit-transform: translateX(-10%);
            opacity: 0.5;
        }
        0% {
            -webkit-transform: translateX(30%);
            opacity: 0;
        }
    }

    @keyframes timebs_animation {
        100% {
            transform: translateX(-10%);
            opacity: 0.5;
        }
        0% {
            transform: translateX(30%);
            opacity: 0;
        }
    }


    .key-left {
        -webkit-animation: arrow .8s .5s ease-in-out infinite alternate;
        animation: timebs_animation .8s .5s ease-in-out infinite alternate;
    }

</style>

element中 仿$confirm 确认框

https://blog.csdn.net/weixin_36617251/article/details/112585426open in new window

1. 创建 Comfirm.js 文件

import Vue from 'vue';
import confirm from '../Comfirm.vue';
let confirmConstructor = Vue.extend(confirm);
let theConfirm = function (content) {
    return new Promise((res, rej) => {
        //promise封装,ok执行resolve,no执行rejectlet
        let confirmDom = new confirmConstructor({
            el: document.createElement('div')
        })
        document.body.appendChild(confirmDom.$el); //new一个对象,然后插入body里面
        confirmDom.content = content; //为了使confirm的扩展性更强,这个采用对象的方式传入,所有的字段都可以根据需求自定义
        confirmDom.ok = function () {
            res()
            confirmDom.isShow = false
        }
        confirmDom.close = function () {
            rej()
            confirmDom.isShow = false
        }
 
    })
}
export default theConfirm;

2. Confirm.vue

<template>
    <!-- 自定义确认弹窗样式 -->
    <el-dialog width="600px" :title="content.title" :visible.sync="content.show" v-if="isShow">
        <span>{{ content.message }}</span>
        <div slot="footer" class="dialog-footer">
            <el-button @click="close">
                取 消
            </el-button>
            <el-button type="primary" @click="ok">
                确 定
            </el-button>
        </div>
    </el-dialog>
 
</template>
 
<script>
    export default {
        data() {
            return {
                // 弹窗内容
                isShow: true,
                content: {
                    title: "",
                    message: "",
                    data: "",
                    show: false
                }
            };
        },
        methods: {
            close() {
              
            },
            ok() {
   
            }
        }
    };
</script>
 
<style>
</style>

3. 在main.js中引入

import confirm from '@/confirm.js' 
Vue.prototype.$confirm = confirm;

4. 调用

 this.$confirm({ title: "删除", message: "确认删除该文件吗?", show: true })
     .then(() => {
     //用户点击确认后执行
 }) .catch(() => {
     // 取消或关闭
 });

vue 记录用户修改的字段

思路: 获取初始值是复制一份(用于比对用户修改),记录需要监听的字段,通过 mixins 混入,然后将具体的监听方法提取出来,作为公共方法

新建 watchFromMixins.js

/**
 * 班组管理中,需要记录 用于修改的字段,通过 mixins 注入需要的的 组件中
 */
import {isEmptyObj} from '@/utils/common'
export default {
    data() {
        return {
            form:{},
            origForm: {}, // form 对象备份-用于比对属性是否变更
            watchField: [], // 需要监听的字段
            needChangeField:'changeDesc' // 默认 赋值的字段,防止 页面不是这个字段,如果不一致,可在需要的组件中指定
        }
    },
    created() { },
    destroyed() { },
    methods: {
        /**
         *  格式化 显示的数值
         * @param value:当前对应的值
         * @param options: 针对下拉选和字典 (对于下拉选和字典 存储数据的字段都不一致,该方法默认是从字典中获取数据,如果不是,则指定对应的属性名)
         *  options{
         *    dataName:"statusList", 下拉选\字典 存储的字典名称
         *    valueName:"dictvalue",:默认是 字典 dictValue
         *    labelName: "dictLabel":默认是字典的 dictLabel
         *  }
         * @returns {string}
         */
        getFieldName (value,options) {
            if(!value && value != 0){
                value = '空';
            }else{
                // 考虑字典,下拉选,自动 由vale 值获取label
                if(options && this[options.dataName] && this[options.dataName].length > 0){
                    value = this.getSelectLabel(options, value)
                }
            }
            return value
        },
        // 针对下拉选,和字典,由vale 值获取label
        getSelectLabel (options, value) {
            const valueField = options.valueName || 'dictValue'; //
            const labelField = options.valueName || 'dictLabel';
            this[options.dataName].some((item, index, self) => {
                if (item[valueField] === value) {
                    value = item[labelField];
                    return true;// 查询到终止循环
                }
            })
            return value
        },
        // 重新组装 changeDesc
        _updateFormData() {
            const tempForm = JSON.parse(JSON.stringify(this.form))
            if(tempForm.changeDesc2){
                tempForm.changeDesc += '\n'+this.form.changeDesc2
                tempForm.changeDesc2 = '';
            }
            return tempForm;
        }
    },
    watch: {
        form: {
            handler(newVal, oldVal) {
                if(newVal && !isEmptyObj(newVal) && this.origForm.id) {
                    const messageArr = []; //组装的提示信息
                    if (!this.watchField || this.watchField.length ==0) {
                        return;
                    }
                    this.watchField.forEach( ({field,fieldName,options}) => {
                        if(this.form[field] != this.origForm[field]){
                            if (!this.origForm[field] && this.form[field] == '') { // 针对的是 初始值为 undefined,修改后又清除的场景
                                return;
                            }
                            const newFieldName = this.getFieldName(this.origForm[field],options); // 新字段值
                            const oldFieldName = this.getFieldName(this.form[field],options);
                            const mes = `${fieldName}: 由 "${newFieldName}" 变更为 "${oldFieldName}"`
                            messageArr.push(mes);
                        }
                    })
                    this.$set(this.form, this.needChangeField, messageArr.join(";"))
                }
            },
            deep:true // 深度监听
        }
    }
}

<script>
import mixins from '@/mixin/watchFromMixins'
export default {
	mixins: [mixins], // 通过 mixins(混入)将 监听字段变更 提出到功能js类中
    data () {
        return {
            form: {},
            needChangeField:'changeDesc'
            watchField: [
                { field: 'sbStatusName', fieldName: '设备状态' },{ field: 'bqArea', fieldName: '便桥行政区划' },
                { field: 'bqAddr', fieldName: '便桥位置' },{ field: 'bqClass', fieldName: '便桥种类' }
            ], // 需要监听的字段
        }
    }
    methods: {
                loadData (item) {
                    item = item || {}
                    this.getMainData(item)
                },
                getMainData (item) {
                    // 后台查询数据
                    getDataInfo(item).then(response => {
                        this.form = response.data
                        this.initOtherData() // 数据获取后的一些默认赋值 操作
                    })
                },
                // 数据获取后的一些默认赋值 操作
                initOtherData () {
                   if (this.form.sbStatus == 1) {
                        this.form.sbStatusName = '在用'
                    } else {
                        this.form.sbStatusName = '停用'
                    }
                    // 这部很重要,如果不默认赋值,可能会导致该字段无法修改
                    this.form.changeDesc = ''; // 初始化,否则data不会监听数值变化,同样,会导致 mixins 混入赋值后,用户无法修改该字段
                    this.origForm = JSON.parse(JSON.stringify(this.form))
                },
    }
}
</script>

vue 水平tab ,根据内容自动定位

初始化时根据自定义的index,默认选择指定的tab,同时随着内容滚动,默认选中指定tab,并且

1138411gg0u4788p7yzw5p.gif.optim
1138411gg0u4788p7yzw5p.gif.optim

运行管理towerParameter.vue -父页面

<!--
 * @Descripttion:
 * @Author: cuixu
 * @Date: 2022-10-19 08:51:10
-->
<template>
    <div class="page">
        <!--页面头部-->
        <PageHeader  :title="title" position="center"> </PageHeader>
        <div class="towerContainer">
            <!-- 头部滑动导航 -->
            <towerTopScroll ref="towerTopScrollRef" :topTabType="topTabType" :bottomTabIndex="bottomTabIndex" @towerTopTabclick="towerTopclick" />
            <div class="tower-scroll" ref="cont" id="tower">
                <!-- 基本信息 -->
                <baseInfo id="tabs0" :name="comName" :item-obj="itemObj" ref="teamBase" @updateTowerInfo="updateTowerInfo" />
                <!-- 隐患信息 -->
                <hiddenDangerInfo id="tabs1" :name="comName" :item-obj="itemObj" ref="hiddenDanger" />
                <!-- 缺陷信息 -->
                <defectsInfo id="tabs2" :name="comName" :item-obj="itemObj" ref="defects" />
                <!-- 通道环境 -->
                <channelEnvironment id="tabs3" :name="comName" :item-obj="itemObj" ref="basTd" />
                <!-- 附属设施 -->
                <ancillaryFacilities id="tabs4" :name="comName" :item-obj="itemObj" ref="basFs" />
                <!-- 六防信息 -->
                <sixPrevention id="tabs5" :name="comName" :item-obj="itemObj" ref="basLf" />
                <!-- 检测信息 -->
                <detection id="tabs6" :name="comName" :item-obj="itemObj" ref="basJc" />
                <!-- 三跨/交跨 -->
                <threeSpans id="tabs7" :name="comName" :item-obj="itemObj" ref="threeSpans" />
            </div>
        </div>
    </div>
</template>
<script>
import PageHeader from "@/components/page-header/PageHeader.vue";
import common from "../../../public";
import towerContent from "./components/towerContent";
import towerTopScroll from "./components/towerTopScroll";
import baseInfo from "./components/baseInfo";
import hiddenDangerInfo from "./components/hiddenDangerInfo";
import defectsInfo from "./components/defectsInfo";
import channelEnvironment from "./components/channelEnvironment";
import ancillaryFacilities from "./components/ancillaryFacilities";
import sixPrevention from "./components/sixPrevention";
import detection from "./components/detection";
import threeSpans from "./components/threeSpans";
import { mapState, mapMutations } from 'vuex'
// import selectorTowerPopup from '@/components/bac-selector/selectorTowerPopup'
import bottomBtton from "@/components/bottomButton/index.vue";
import { Toast, Dialog } from 'vant';
import { getDicts } from '../../../net/commonRequest'

export default {
    name: "towerParameter",
    components: {
        PageHeader,
        towerContent,
        towerTopScroll,
        baseInfo,
        hiddenDangerInfo,
        defectsInfo,
        channelEnvironment,
        ancillaryFacilities,
        sixPrevention,
        detection,
        threeSpans,
        bottomBtton,
        // selectorTowerPopup
    },
    data() {
        return {
            // 页面名称
            title: "GT台账",
            //当前导航名称
            comName: "",
            // 头部导航类型
            topTabType: "tipTab1",
            // 头部导航当前下标
            bottomTabIndex: 0,
            showUserSelect: false,
            towerUpdateInfo: {},
            userList: [],
            // towerList: [],
            jyzInfoList: [],
            //已删除的绝缘子信息
            jyzInfoList_DEL: [],
            selectDataList: [],
            //绝缘子类型
            typeOptions: [],
            //绝缘子组装形式
            zzTypeOptions: [],
            //绝缘子材料
            jyzClOptions: [],
            userCascaderValue:'',
            guarderInfoId: "",
            jyzIndex: "",
            currentUser: this.$store.state.userInfo.userId,
            originQzUserId: undefined,
            dictPopupShow: false,
            canUpdateQzhxy: true,//目前取消不能修改群众护线员信息的限制
            dictFieldName: {
                text: 'dictLabel'
            },
            // canSelectTower: false,
            itemObj: {}, // 业务参数-父页面传递过来的 GT信息
            fieldNames: {
                text: "label",
                value: "id",
                children: "children",
            },
            list: [ ],
            //滚动公共class
            arrDom: null,
            scrollTop: 0,
            towerInfoUpdatePop: false,
            needActivated: false, // 是否需要 走 activated ,目前 activated-是为了解决 局部刷新和 由详情界面返回滚动到原来的位置
            needHandleScroll: false // 是否需要 执行滚动(主要是因为初始化,模拟点击头部tab,但是会触发 handleScroll 事件,导致)
        };
    },
    computed: {
        ...mapState(['cachePageList']),// 组件访问 State 中数据的第二种方式  也可以直接用this.$store.state.count
    },
    mounted() {
        this.needActivated = false;
        let data ={};
        let index = 0; // 用来 默认 选中 指定tab
        // 从GT数据点击跳转过来
        if(this.$route.query.index){ // 说明需要跳转到 当前指定的模块
            data={
                name:this.$route.query.name
            }
            index = Number(this.$route.query.index)
        }else{
            data={ name:'baseInfo'}; // 初始化时,默认 选择的是 GT基本信息
            this.resetScrollTop(0); // 重置锚点定位,否在位置不对
        }
        this.towerTopclick(data,index)
        // 监听页面滚动
        var tower = document.getElementById("tower");
        tower.addEventListener("scroll", this.handleScroll);
        // 每个滚动模块
        this.arrDom = document.getElementsByClassName("towerBaseBox");
        setTimeout(() => {
            this.needActivated = true;
        }, 500);
    },
    created() {
        this.itemObj = this.$route.query;
        // 权限
        let dataScope = common.getUserMaxDataScope();
        if (dataScope > 1) {
            this.canUpdateQzhxy = true;
        }
    },
    activated() {
        if(!this.needActivated){
            return;
        }
        // 局部刷新,为了解决 修改后,GT台账里面数据更新
        this.partialRefresh();
        // 锚点定位,解决,从详情界面返回还能定位到之前的位置
        this.resetScrollTop(this.scrollTop);
    },
    // 监听离开,如果返回父页面则清除当前组件的缓存
    beforeRouteLeave: function (to, from, next) {
        this.needActivated = true;
        if (to.name == "lineRunning") { // 返回上一级页面需要 清除当前页面的缓存
            // 因为目前当前页面 路由中配置的 name 和组件name 没对应上,否则可有直接用  from.name
            this.setCachePageListRemoveMutation("towerParameter");// 移除 当前组件的缓存
            this.setNeedUpdateModulesRemoveAllMutation();// 清除一下局部缓存
        }
        next();
    },
    deactivated() {
        console.log('组件被销毁');
    },
    methods: {
        ...mapMutations(['setCachePageListRemoveMutation', 'setNeedUpdateModulesRemoveAllMutation']),//  将指定的 mutations 函数,映射为当前组件的 methods 函数
        // 页面初始化参数
        loadData() { },
        /**
         * 滚动监听事件
         */
        handleScroll() {
            if(!this.needHandleScroll){ // 标识当前真正进行 模拟点击tab 定位,不执行,由高度确定 选择tab 的index
                return;
            }
            const current_offset_top = this.$refs.cont.scrollTop + this.arrDom[0].offsetTop; // 需要去掉 头部和tab区域高度
            this.scrollTop = this.$refs.cont.scrollTop;
            for (let i = 0; i < this.arrDom.length; i++) {
                if (i < 7) {
                    if (this.arrDom[i].offsetTop <= current_offset_top && current_offset_top < this.arrDom[i + 1].offsetTop) {
                        // 根据滚动距离判断应该滚动到第几个导航的位置
                        this.bottomTabIndex = i;
                        break;
                    }
                } else {
                    this.bottomTabIndex = 7;
                }
            }
            this.$refs.towerTopScrollRef.locationTab();
        },

        // 点击头部导航按钮
        towerTopclick(item, data) {
            this.needHandleScroll = false;
            console.log('towerTopclick')
            this.bottomTabIndex = data;
            this.comName = item.name;
            let id = `#tabs${data}`;
            this.$nextTick(() => {
                document.querySelector(id).scrollIntoView({
                    behavior: "instant", // 定义过渡动画 instant立刻跳过去 smooth平滑过渡过去
                    block: "start", // 定义垂直滚动方向的对齐 start顶部(尽可能)  center中间(尽可能)  end(底部)
                    inline: "start", // 定义水平滚动方向的对齐
                });
                setTimeout(() => {
                    this.needHandleScroll = true; // 模拟点击tab滚动完成,开始监听 滚动和tab
                }, 500);
            });
        },
        resetScrollTop (scrollTop) {
            console.log('定位-'+scrollTop)
            this.$nextTick(() => {
                this.$refs.cont.scrollTop = scrollTop;
            });
        },
        // 局部 刷新 页面
        partialRefresh() {
            let needUpdateModules = this.$store.state.needUpdateModules;
            if (needUpdateModules && needUpdateModules.length > 0) {
                needUpdateModules.forEach(modules => {
                    try {
                        this.$refs[modules].onLoad()
                    } catch (e) {
                        console.log(e);
                        console.log("局部刷新异常")
                    }
                })
                this.setNeedUpdateModulesRemoveAllMutation();
            }
        },
        updateTowerInfo(item) {
            this.towerUpdateInfo = { userId: item.ownerQzId, userName: item.ownerQzName, guarderType: "2", lineId: item.lineId, eqTowerId: item.eqTowerId,towerId: item.towerId };
            this.originQzUserId = item.ownerQzId;
            this.getJyzData(item);
        },
        //获取GT绝缘子信息
        getJyzData(item) {
            let that = this;
            let options = {
                method: "post",
                url: "/teams/basInsulator/getJueyuanziByEqTowerId",
                headers: { 'Content-Type': 'application/json;charset=utf-8' },
                params: { eqTowerId: item.eqTowerId, sbStatus: '1' },
                appservercode: "teams"
            };
            this.$commonRequest(options, function (res) {
                if (res && res.code == 200) {
                    if (res.data && res.data.length > 0) {
                        that.jyzInfoList = res.data;
                        that.initDictData();
                    }
                } else {
                    Toast.fail({
                        message: "查询绝缘子信息出错了,错误信息:" + res.msg,
                        duration: 5000,
                    });
                }
                // that.getHuxianyuanData(item);
                that.userTreeselect();
                that.towerInfoUpdatePop = true;
            });
        },
    },

};
</script>
<style scoped lang="scss">
.content {
    padding: 16px 16px 160px;
}

.needUpdate {
    color: #1c71fb !important;
    font-weight: 600;
}

:deep {
    .towerJyzContent .van-field__label {
        width: unset !important;
    }

    .bottomBtton .bottomBtton-box .bottomBtton-right {
        margin-left: unset !important;
    }
}
</style>

tab区域

<!--
 * @Descripttion:
 * @Author: cuixu
 * @Date: 2022-11-03 16:58:46
-->
<template>
    <div ref="tabUl" class="type-ul">
        <div :id="'tabli'+index" class="type-ul-li" :class="{ typeActive: index == bottomTabIndex }"
             v-for="(item, index) in tabbars" :key="index" @click="typeClick(item,index)">
            <img :src="index == bottomTabIndex ? item.active : item.normal" alt=""/>
            <div>{{ item.title }}</div>
        </div>
    </div>
</template>
<script>
    export default {
        props: {
            topTabType: {
                type: String,
            },
            bottomTabIndex: {
                type: Number,
            },
        },

        data () {
            return {
                // GT台账
                tabbars: [
                    {
                        name: "baseInfo",
                        title: "基本信息",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab1.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab1_active.png"),
                    },

                    {
                        name: "hiddenDangerInfo",
                        title: "隐患信息",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab3.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab3_active.png"),
                    },

                    {
                        name: "defectsInfo",
                        title: "缺陷信息",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab2.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab2_active.png"),
                    },
                    {
                        name: "channelEnvironment",
                        title: "通道环境",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab4.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab4_active.png"),
                    },

                    {
                        name: "ancillaryFacilities",
                        title: "附属设施",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab5.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab5_active.png"),
                    },
                    {
                        name: "sixPrevention",
                        title: "六防信息",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab6.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab6_active.png"),
                    },
                    {
                        name: "detection",
                        title: "检测",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab7.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab7_active.png"),
                    },
                    {
                        name: "threeSpans",
                        title: "三跨/交跨",
                        normal: require("../../../../assets/teamImgs/towerTopTab/tab8.png"),
                        active: require("../../../../assets/teamImgs/towerTopTab/tab8_active.png"),
                    },
                ]
                
            };
        },

        created () {},
        activated () {
            /**
             *  将当前选中的tab卡片 滚动到可视区域 (2个场景下会触发)
             *      1.需要跳转到指定的tab
             *      2.详情界面返回(页面已经实现缓存,但是滚动位置需要自己手动设置)
             */
            this.$nextTick(() => {
                this.scrollCheckDom()
            });
        },
        methods: {
            // 点击选中
            typeClick (item, index) {
                this.currentIndex = index;
                this.$emit("towerTopTabclick", item, index);
            },
            /**
             * 将当前选中的tab卡片 滚动到可视区域(由父页面调用)
             * 父页面 滚动内容区域,默认选中当前对于的 tab选项卡
             *    计算是否在 可视区域内,否则触发 scrollCheckDom 方法
             */
            locationTab () {
                setTimeout(() => {
                    const checkDm = this.$el.querySelector(`#tabli${this.bottomTabIndex}`).getBoundingClientRect();//this.$refs.wdzb_detail_item_wrap;
                    const presentWidth = checkDm.left + checkDm.width;  // 当前选中的 tab,最大宽度,如果这个值大于 父容器,则表示 当前 需要滚动否则元素在最右侧 看不到
                    // checkDm.left < 0 表示当前 选中的tab,不在可视区域内了
                    if (this.$refs.tabUl.getBoundingClientRect().width >= presentWidth && checkDm.left > 0){
                        return;
                    }
                    this.scrollCheckDom();
                }, 300);
            },
            // 将当前选中的tab卡片 滚动到可视区域
            scrollCheckDom () {
                document.querySelector(`#tabli${this.bottomTabIndex}`).scrollIntoView({
                    behavior: "instant", // 定义过渡动画 instant立刻跳过去 smooth平滑过渡过去
                    block: "start", // 定义垂直滚动方向的对齐 start顶部(尽可能)  center中间(尽可能)  end(底部)
                    inline: "start", // 定义水平滚动方向的对齐
                });
            }
        }
    };
</script>
<style scoped lang="scss">
    .type-ul {
        position: fixed;
        // bottom: 0;
        background: #f5f5f5;
        // height: 140px;
        // display: flex;
        // align-items: center;
        padding: 20px 0 0 0;
        overflow: hidden;
        overflow-x: auto;
        white-space: nowrap;
        width: 100%;
        // box-shadow: 0px -2px 0px 0px #efefef;
        z-index: 1000;

        &-li {
            display: inline-block;
            // padding: 0 20px;
            width: 135px;
            color: #999999;
            font-size: 22px;

            img {
                width: 64px;
                height: 64px;
            }
        }

        .typeActive {
            color: #333333;
        }
    }
    .type-ul-li{
        transition: all 0.6s;
    }
</style>

局部刷新实现

场景说明 :父页面 --> list页面--> 详情界面

​ list页面存在多个模块,现在需要实现如果 修改了单个模块,值跟新单个模块的数据

实现: 通过vuex记录 需要缓存的页面,然后手动跟新这个对象,

​ 1. 父页面跳转到 list页面 -- 缓存当前list页面(解决list页面跳转详情界面在回来list页面不会重新加载)

​ 2.详情界面修改跳转回 list页面 --- 局部刷新

​ 3. list页面返回 父页面 --- 清除缓存

image-20230208133815784
image-20230208133815784

store/index.js

import {
	createStore
} from 'vuex'


/*
vuex存储在内存,localstorage(本地存储)则以文件的方式存储在本地,永久保存;sessionstorage( 会话存储 ) ,临时保存.
localStorage和sessionStorage只能存储字符串类型,对于复杂的对象可以使用ECMAScript提供的JSON对象的stringify和parse来处理
Unexpected token u in JSON at position 0
*/
export default createStore({
	state: {
        // 需要缓存的页面, 需要注意和路由中配置的name和path没关系
        cachePageList:['Message', 'Map', 'Work', 'Find', 'My', 'Hiddendanger', 'Defects', 'needUploadDataList', 'ShojamRecord'], // 需要缓存的页面对象 在 index.vue 中定义的
        needUpdateModules:[]// 页面缓存,会导致数据不能及时更新,目前再班组管理中,需要 用到局部刷新,所以定义了 该,用于记录 需要刷新的模块
	},
	mutations: {
        // 需要缓存的页面
        setCachePageListMutation(state, payload) {
			state.cachePageList = payload;
		},
        setCachePageListAddMutation(state, payload) {
		    if(state.cachePageList && state.cachePageList.indexOf(payload)==-1){
                state.cachePageList.push(payload)
            }
		},
        setCachePageListRemoveMutation(state, payload) {
		    if(state.cachePageList && state.cachePageList.indexOf(payload)>=-1){
                state.cachePageList = state.cachePageList.filter((item) => item !== payload);
            }
		},
        //记录需要局部更新 的模块--用于缓存页面局部刷新
        setNeedUpdateModulesMutation(state, payload) {
            state.needUpdateModules = payload;
        },
        //添加需要 局部更新的模块
        setNeedUpdateModulesAddMutation(state, payload) {
            if(state.needUpdateModules && state.needUpdateModules.indexOf(payload)==-1){
                state.needUpdateModules.push(payload)
            }
        },
        //删除需要 局部更新的模块
        setNeedUpdateModulesRemoveMutation(state, payload) {
            if(state.needUpdateModules && state.needUpdateModules.indexOf(payload)>=-1){
                state.needUpdateModules = state.needUpdateModules.filter((item) => item !== payload);
            }
        },
        setNeedUpdateModulesRemoveAllMutation(state, payload) {
            state.needUpdateModules = [];
        },
	},
    // Action 触发 actions 异步任务时携带参数:
	actions: {
		
		setCachePageListActions(context, payload) {
			context.commit('setCachePageListMutation', payload)
		},
	},
	modules: {

	}
})

父页面跳转

gotoDetail(type, item, path) {
    this.setCachePageListAddMutation("towerParameter");
    this.$router.push({ path: path, query: itemObj });
},

list 页面

    // 监听离开,如果返回父页面则清除当前组件的缓存
    beforeRouteLeave: function (to, from, next) {
        this.needActivated = true;
        if (to.name == "lineRunning") { // 返回上一级页面需要 清除当前页面的缓存
            // 因为目前当前页面 路由中配置的 name 和组件name 没对应上,否则可有直接用  from.name
            this.setCachePageListRemoveMutation("towerParameter");// 移除 当前组件的缓存
            this.setNeedUpdateModulesRemoveAllMutation();// 清除一下局部缓存
        }
        next();
    },
    activated() {
        if(!this.needActivated){
            return;
        }
        // 局部刷新,为了解决 修改后,GT台账里面数据更新
        this.partialRefresh();
        // 锚点定位,解决,从详情界面返回还能定位到之前的位置
        this.resetScrollTop(this.scrollTop);
    },
    mounted() {
        this.needActivated = false;
        if(this.$route.query.index){ // 说明需要跳转到 当前指定的模块
            xxxx
        }else{
           xxxxx
            this.resetScrollTop(0); // 重置锚点定位,否在位置不对
        }
        // 监听页面滚动
        var tower = document.getElementById("tower");
        tower.addEventListener("scroll", this.handleScroll);
    },
    methods: {
    ...mapMutations(['setCachePageListRemoveMutation', 'setNeedUpdateModulesRemoveAllMutation']),//  将指定的 mutations 函数,映射为当前组件的 methods 函数  
// 局部 刷新 页面
        partialRefresh() {
            let needUpdateModules = this.$store.state.needUpdateModules;
            if (needUpdateModules && needUpdateModules.length > 0) {
                needUpdateModules.forEach(modules => {
                    try {
                        this.$refs[modules].onLoad()
                    } catch (e) {
                        console.log(e);
                        console.log("局部刷新异常")
                    }
                })
                this.setNeedUpdateModulesRemoveAllMutation();
            }
        },
        resetScrollTop (scrollTop) {
            console.log('定位-'+scrollTop)
            this.$nextTick(() => {
                this.$refs.cont.scrollTop = scrollTop;
            });
        },
                    /**
         * 滚动监听事件
         */
        handleScroll() {
            this.scrollTop = this.$refs.cont.scrollTop;
        },

}

详情界面

        submit() {
            this.updateFilePath(); // 附件路径处理
            // GT验证,因为权限问题,会出现,当前页面GT没有选中,此时要给提示
            if (!this.form.towerId) {
                Toast("请选择GT");
                return;
            }
            this.$formValidation(this.$refs.form).then((res) => {
                // 验证通过
                this.$commonRequestJSON("post", "/teams/basTdBuild/saveData", this.form, (response) => {
                    if (response.code == 200) {
                        Toast("操作成功");
                        this.setNeedUpdateModulesAddMutation('basTd');
                        this.$router.go(-1);
                    } else {
                        Toast(response.msg);
                    }
                });
            });
            console.log(this.form);
        },

pdf 预览

1. vue-pdf

针对只预览 pdf场景

参考:https://blog.csdn.net/prey1025/article/details/123278603open in new window

npm install --save vue-pdf
<template>
<div class="whp100" :key="pdfIndex">
    <pdf ref="pdf" v-for="i in numPages" :key="i" :src="pdfSrc" :page="i"></pdf>
    </div>
</template>
<script>
    import pdf from 'vue-pdf'
    export default {
        components:{ pdf },
        data(){
            return {
                pdfSrc:"http://image.cache.timepack.cn/nodejs.pdf",
                numPages: null, // pdf 总页数
                pdfIndex: '', // 解决跟换src 不生效问题
            }
        },
        mounted() {
            this.getNumPages()
        },
        methods: {
           // 计算pdf页码总数
            getNumPages() {
                let loadingTask = pdf.createLoadingTask(this.pdfSrc)
                loadingTask.promise.then(pdf => {
                    this.numPages = pdf.numPages
                }).catch(err => {
                    console.error('pdf 加载失败', err);
                })
            },
    	},
        // 切换pdf
        switchPdf(url){
            this.pdfIndex++; // 不加可能会出现一些异常
            this.pdfSrc = url;
            this.getNumPages();
        }
    }
</script>

上面的方式好像切换时页面总数计算会有问题

<pdf ref="pdfRef" :src="pdfSrc" v-for="i in numPages" :page="i" :key="i" @num-pages="setNumPages" style="width: 100%; height: auto;"  ></pdf>
initPdfView (url) {
    this.pdfIndex++;
    this.numPages = 1;
    this.pdfSrc = url;
},
    // 就目前看,肯能也有点问题,setNumPages 会被调用多次
    setNumPages (numPages) {
        console.log('PDF 文件总页数为:', numPages)
        if (numPages) {
            this.numPages = numPages;
        }
    },

拿到总页数

<template>
  <div>
    <pdf :src="pdfSrc" :key="pdfKey" @num-pages="setNumPages"></pdf>
    <div>当前 PDF 共 {{ numPages }} 页</div>
    <button @click="loadNewPdf">加载新的 PDF 文件</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      pdfSrc: 'https://cdn.mozilla.net/pdfjs/tracemonkey.pdf',
      pdfKey: 1,
      numPages: 0
    }
  },
  methods: {
    initPdfView (url) {
      console.log(url)
      this.pdfSrc = url
      this.pdfKey += 1
    },
    setNumPages (numPages) {
      console.log('PDF 文件总页数为:', numPages)
      this.numPages = numPages
    },
    loadNewPdf () {
      const url = 'https://cdn.mozilla.net/pdfjs/helloworld.pdf'
      this.initPdfView(url)
    }
  }
}
</script>

vue中使用iframe

<iframe v-if="popupItem.url" id="iframe" ref="iframe" :src="popupItem.url" frameborder="0" width="100%" height="100%" scrolling="no"></iframe>

自定义日历

周和月

dateSelectPopup.vue

效果图

image-20230724171840653
image-20230724171840653
            // 循环 指定年份范围,遍历每个月组织 周日历 数据
            loopYearsAndMonths () {
                const startDate = dayjs(this.minDate) //dayjs(`${this.minDate}-01-01`)
                const endDate = dayjs(this.maxDate)//dayjs(`${this.maxDate}-12-01`)
                const weekDate = []
                let currentDate = startDate
                while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'month')) {
                    const year = currentDate.year()
                    const month = currentDate.month() + 1
                    const weekDateByMonth = getWeeksDate(year, month,this.theme)
                    weekDate.push(...weekDateByMonth)
                    currentDate = currentDate.add(1, 'month')
                }

                return weekDate
            },
            
getWeeksDate (year, month, dataType) {
    const firstDayOfMonth = dayjs(`${year}-${month}-01`)
    const firstDayOfNextMonth = firstDayOfMonth.add(1, 'month')
    let startDate = firstDayOfMonth.startOf('week').day(1) // 将startDate设置为下一个周一
    const weeks = []
    let weekCount = 1
    let startDateStr = ''//每周的开始日期
    let endDateStr = ''//每周的开始日期
    while (startDate.isBefore(firstDayOfNextMonth)) {
        const endDate = startDate.add(6, 'day') // 将 endDate 设置为 startDate 加 6 天,即本周的最后一天(周日)
        const text = `${year}年${month}月第${weekCount}周`
        // 为了保证每走开始和结束 日期不跨月,需要重现进行计算
        //1. 验证当前 周 开始的第一天
        startDateStr = startDate.format('YYYY-MM-DD')// 本周第一天
        // 每周的第一天如果不在本月,则本月的第一天
        if (!startDate.isSame(firstDayOfMonth, 'month')) {
            startDateStr = firstDayOfMonth.format('YYYY-MM-DD')
        }

        //2. 验证每周的最后一天是否在本月,如果不在则取本月最后一天
        endDateStr = endDate.format('YYYY-MM-DD')// 本周最后一天
        // 本周最后一天,如果 不在本月,则说明跨域了,取本月的最后一天
        if (!endDate.isSame(firstDayOfMonth, 'month')) {
            endDateStr = firstDayOfMonth.endOf('month').format('YYYY-MM-DD')
        }
        weeks.push({
            text: text,
            value: text,
            dataType: dataType,
            startDate: startDateStr,
            endDate: endDateStr
        })
        startDate = startDate.add(7, 'day')
        weekCount++
    }
    return weeks
}            

文本展示

场景:文本超长隐藏,但是移动端没有title,所有添加了一个展示/闭合 按钮

image-20230810120241979
image-20230810120241979

LongTextCollapse.vue

<div class="lsh-chart-legend-wrap ">
    <long-text-collapse class="legend-item" v-for="(item,index) in totalData"
                                    :text="item.name"
                                    :value="item.value"
                                    :color="item.color"
                                    :key="index" />
</div>


<style lang="scss" scoped>
        .lsh-chart-legend-wrap {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            overflow: auto;
            /*一行排列2个*/
            .legend-item {
                width: calc(50% - 5px);
                font-family: Source Han Sans CN-Regular, Source Han Sans CN;
                margin-bottom: 8px;
                &:nth-child(2n) {
                    margin-left: 10px; /* 指定每行 2个元素的间隔,是每隔2个设置*/
                }
            }
        }

</style>

一行排列3个

   .legend-item {
                width: calc(33% - 4px);
                font-family: Source Han Sans CN-Regular, Source Han Sans CN;
                margin-bottom: 8px;
                margin-right: 6px; /*  4px*3/2  */
                &:nth-child(3n) {
                    margin-right: 0;
                }
            }

2列排列的数据,如果容器超出则自动换行

默认 card-item 宽度是 50%,然后遍历 每个 card-item 元素,判断scrollWidth 是否大于offsetWidth,则表示 出现滚动了,这时设置 宽度为 100%

<div class="card long_omit">
    <div class="card-item long_omit" :ref="key+'ItemRef'" v-for="(value,key) in detail.extraInfo.data">
        <span class="card-item__label">{{key}}:</span>
        <span class="card-item__value">{{value}}</span>
    </div>
</div>
mounted () {
    if(this.detail.extraInfo && this.detail.extraInfo.data){
        for (var key in this.detail.extraInfo.data) { // this.detail.extraInfo.data 是一个map对象 ,根据自己实际的数据格式进行遍历
            const textElement = this.$refs[key+'ItemRef'][0];
            if (textElement.scrollWidth > textElement.offsetWidth) { // 水平方向出现滚动条了
                textElement.style.width = '100%';
            }
        }
    }

},

文本添加一个标记点

效果图: image-20230811115223509

最优写法,将标记点和文本放到一起通过before,并且通过是否传递color背景色,控制是否显示,可以直接 对父容器定义 before属性,

改写法支持如果color为空,则不显示

<span class="text__content" ref="textRef" @click.stop="toggleText" :class="{'has-tag': color}" 
      :style="{'--background-color': color}" >{{ text }}</span>


<seyle lang="scss" scoped>
        .text__content {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            &:before {
                display: inline-block;
                width: 8px;
                height: 8px;
                border-radius: 50%;
                margin-right: 5px;
            }
            &.has-tag:before{
                content: '';
                background-color: var(--background-color);
            }
        }
</seyle>


常规想法是 父容器下添加2个元素

<div class="text-wrap" :class="{'unfold': unfoldFlag}">
        <div class="text__tag" :style="{'background-color': dataItem.color}" v-if="dataItem.color"></div>
        <span class="text__content" ref="textRef" @click="toggleText">{{ dataItem.name }}</span>
    </div>


<seyle lang="scss" scoped>
    .text__tag {
            content: '';
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 5px;
            flex-shrink: 0;
        }
        .text__content {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
</seyle>

移动端对指定区域进行缩放

通过2指对指定区域进行缩放查看,并且松手后容器会还原,类似于放大镜功能

  1. <template> 部分,为表单容器元素添加一个 touchstart 事件监听器来检测手势开始。
html复制代码<template>
  <div class="form-card" v-if="form.content" ref="formContainer" @touchstart="handleTouchStart">
    <!-- 需要缩放查看的区域 -->
    
  </div>
</template>
  1. 在 Vue 组件的方法部分,定义 handleTouchStart 方法来处理手势开始事件。
javascript复制代码<script>
export default {
  methods: {
    handleTouchStart(event) {
      if (event.touches.length === 2) {
        // 获取双指触摸的起始位置信息
        const touch1 = event.touches[0];
        const touch2 = event.touches[1];
        this.startDistance = getDistance(touch1, touch2);
      }
    },
    // 定义一个辅助函数来计算两个触摸点之间的距离
    getDistance(touch1, touch2) {
      const dx = touch2.pageX - touch1.pageX;
      const dy = touch2.pageY - touch1.pageY;
      return Math.sqrt(dx * dx + dy * dy);
    },
  },
};
</script>
  1. 继续在 Vue 组件中,添加 touchmovetouchend 事件的监听器来检测手势的变化和结束。
javascript复制代码<script>
export default {
  data() {
    return {
      startDistance: 0, // 双指触摸的起始距离
      currentScale: 1, // 当前缩放比例
    };
  },
  methods: {
    handleTouchStart(event) {
      if (event.touches.length === 2) {
        const touch1 = event.touches[0];
        const touch2 = event.touches[1];
        this.startDistance = this.getDistance(touch1, touch2);
      }
    },
    handleTouchMove(event) {
      if (event.touches.length === 2) {
        const touch1 = event.touches[0];
        const touch2 = event.touches[1];
        const currentDistance = this.getDistance(touch1, touch2);
        // 根据触摸点之间的距离变化来计算缩放比例
        const scale = currentDistance / this.startDistance;
        this.currentScale = scale;
        // 根据缩放比例设置表单容器元素的样式
        this.$refs.formContainer.style.transform = `scale(${scale})`;
      }
    },
    handleTouchEnd() {
      // 手势结束时重置缩放比例和表单容器元素的样式
      this.currentScale = 1;
      this.$refs.formContainer.style.transform = 'scale(1)';
    },
    getDistance(touch1, touch2) {
      const dx = touch2.pageX - touch1.pageX;
      const dy = touch2.pageY - touch1.pageY;
      return Math.sqrt(dx * dx + dy * dy);
    },
  },
};
</script>
  1. 在表单容器元素上添加 touchmovetouchend 的事件监听器。
html复制代码<template>
  <div class="form-card" v-if="form.content" ref="formContainer"
       @touchstart="handleTouchStart"
       @touchmove="handleTouchMove"
       @touchend="handleTouchEnd">
    <!-- 自定义表单 -->
    <v-form-render :option-data="optionData" ref="vFormRef" :isViewForm="true"></v-form-render>
  </div>
</template>

这样,当用户在移动端使用双指进行缩放手势操作时,自定义表单容器元素会根据触摸点之间的距离变化来实现缩放效果,并不会影响其他区域。

请注意,上述示例只提供了基本的原理和代码结构,具体的实现需要根据你的项目结构和需求进行适当的调整和扩展。

富文本强行修改白色字体为黑色

场景说明:PC是深色背景,默认字体颜色是白色,但是移动端是白色背景导致字体无法正常显示,所有需要强行修改背景颜色

    <div>
      <quill-editor
              v-model:value="fieldModel" v-if="!formConfig.isViewForm"
              :options="editorOption"
              :disabled="field.options.disabled"
              @blur="handleRichEditorBlurEvent"
              @focus="handleRichEditorFocusEvent"
              @change="handleRichEditorChangeEvent"
              :style="!!field.options.contentHeight ? `height: ${field.options.contentHeight};`: ''"></quill-editor>

      <!-- 最下面的添加了 after属性,构造一个删除按钮,但是无法定义click事件,如果后期还是需要清除按钮,则可以考虑用一个父节点包裹 vue-editor 富文本-->
      <!-- 添加了 预览模式 ,不显示 表单控件-->
      <pre v-if="formConfig.isViewForm"  class="input-text editor-text" v-html="fieldModel"></pre>
    </div>
      processFieldModel() {
        this.$nextTick(() => {
          const preElement = this.$el.querySelector('.input-text.editor-text');
          if (preElement) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(this.fieldModel, 'text/html');
            const elements = doc.querySelectorAll('[style*="color: rgb(255, 255, 255)"]');
            elements.forEach((element) => {
              element.style.color = '#2c3e50'; // 替换为黑色
            });
            const processedFieldModel = doc.body.innerHTML;
            preElement.innerHTML = processedFieldModel;
          }
        });
      }

导出表格

https://blog.csdn.net/m0_51431448/article/details/128630505open in new window

handleExport() {
      const summaryRow = ['汇总'];
      const header = [];
      const columns = this.$refs.tableBox1.columns;
      for (let column of columns) {
        if (column.label) {
          header.push(column.label);
        } else {
          header.push(''); // 如果 label 为空,用空字符串代替
        }
      }
      const exportData = this.list.map((item, index) => {
        let itemArr = [index + 1, item.mName, item.sName, item.unitName];
        this.columns.forEach((inner_item) => {
          itemArr.push(item[inner_item.prop])
        });
        itemArr.push(item.rowTotal)
        return itemArr;
      });
      exportData.unshift(header);
      exportData.unshift(summaryRow);


      const XLSX = require('xlsx');
      let ws = XLSX.utils.aoa_to_sheet(exportData)
      this.setExcelStyle(ws) // 设置样式
      // 给 标题行添加 背景色
      this.setTileBgColre(header,ws);
      ws['!merges'] = [ { s: { r: 0, c: 0 }, e: { r: 0, c:16 } } ];
      let wb = XLSX.utils.book_new()
      XLSX.utils.book_append_sheet(wb, ws)
      let wbout = XLSXS.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' })
      try {
        FileSaver.saveAs( new Blob([this.s2ab(wbout)], { type: "application/octet-stream" }), "exported_data.xlsx");
      } catch(e) {
        console.error(e, wbout, '----->>>')
      }
}


setTileBgColre(headData,ws){
},
    // 设置导出Excel样式 这里主要是关注单元格宽度
    setExcelStyle(data) {
        let borderAll = {
            //单元格外侧框线
            top: {
                style: "thin",
            },
            bottom: {
                style: "thin",
            },
            left: {
                style: "thin",
            },
            right: {
                style: "thin",
            },
        }
        data['!cols'] = []
        for(let key in data) {
            if(data[key].constructor === Object) {
                data[key].s = {
                    border: borderAll,  // 边框
                    alignment: {
                        horizontal: "center", //水平居中对齐
                        vertical: "center", // 垂直居中
                    },
                    font: {
                        sz: 11,
                    },
                    bold: true,
                    numFmt: 0
                }
                data["!cols"].push({ wpx: 120 }); // 单元格宽度
            }
        }
    },
img
img

自定义组件双向绑定实现

1. vue2的实现

通过watch 来实现

  • 父组件
<importTypeSecond v-model="formData.importWay"  />
  • 子组件
<template>
    <div class="dataContent">
        <div class="dataItem flex-col clickable" :class="activeId === item.id? 'active' : ''" 
             v-for="(item, index) in importWay" :key="item.id" @click="handleClick(item)" :style="{ cursor: item.disabled ? 'not-allowed' : 'pointer' }"
         >
            <div class="dataName">{{ item.name }}</div>
            <div class="dataDesc">{{ item.desc }}</div>
        </div>
    </div>
</template>

<script>
export default {
    name: 'importTypeSecond',
  props: {
      value: {
          type: Number,
          default: 1
      }
  },
    data() {
        return {
            importWay: [{
                id: 1,
                name: '带标注导入',
            }, {
                id: 2,
                name: '不带标注导入',
            }],
            activeId: this.value
        }
    },
  watch: {
    value(newVal) {
      this.activeId = newVal;
    },
    activeId(newVal) {
      this.$emit('input', newVal);
    },
  },
    methods: {
        handleClick(item) {
          this.activeId = item.id
        }
    }
}
</script>


2. 通过计算属性实现

通过 computed 实现

  • 父组件
<footer-title  v-model="sampleTags"/>
<!-- 其中sampleTags对象数组 -->
  • 子组件
<template>
  <div class="tagsContainer flex-row">
        <div class="tagItem text_not_select" v-for="(item,indexx) in dataList">
          <span>{{ item.tagName }}</span>
          <i class="el-icon-close" @click="deleteTag(item,indexx)"></i>
        </div>
      </div>
</template>

<script>
export default {
  name: "FooterTitle",
  data() {
    return {}
  },
  props:{
    value: {
      type: Array,
      default: () => []
    }
  },
  computed: {
    dataList(){
      return JSON.parse(JSON.stringify(this.value));
    },
  },
  methods: {
    // 删除标签
    deleteTag(item,index) {
      const newData = [...this.dataList]
      newData.splice(index,1);
      this.$emit("input", newData);
    }
  }
}
</script>

3. 复杂一点的双向绑定实现

涉及到父组件,子组件,子子组件

场景描述:物资选择,父组件只有一个标签,其他业务功能都在中间组件中,中间组件有个弹窗组件,弹窗选择后数据及时回显到父组件中

image-20240712152554411
image-20240712152554411
  • 父组件

其中父组件中物资是以数组对象的形式传递,这里先将数据id提取出来,以逗号合并成字符串(补充:保存的时候需要数组对象,所以返回到父组件的是对象数组,在子组件中处理好返回给父组件)

<multi-select-wrap v-model="checkMatrIds" :defaultOptions="matrList" title="点击添加物资" :isDelBtn="true" @selectItem="selectItem" />
      // 初始化 已选物资ids
      initCheckMatrIds(){
        if (!this.form.emgBasActvMatrMapList){
          this.$set(this.form,'emgBasActvMatrMapList',[]);// 初始化
        }
        const ids = this.form.emgBasActvMatrMapList.map(item=>item.matrId)||[];
        this.checkMatrIds = ids.join(',');
      },

因为在子子组件中,也实现了双向绑定,所以无法直接使用父组件传递过来的vlue对象,这里通过复制localValue: this.value 来复制

    data() {
      return {
        popupshow: false,
        localValue: this.value, // 本地的 value
      }
    },

同时 通过watch来实现对数据变化的监听

    watch: {
      value(newVal) {
        this.localValue = newVal; // 当 prop 的值变化时,更新本地的 value
      },
      localValue(newVal) {
        this.$emit('input', newVal); // 当本地的 value 变化时,触发 input 事件通知父组件
        this.$emit('selectItem', this.computedOptions); //  
      }
    },

拖拽滑块实现布局

思路:拖拽滑块,改变单个图片/容器的宽度【高度通过设计稿设定的宽高比来计算】然后计算一行布局的最大能排列的数量,当前按理默认排列一行6个

111111111
111111111
<template>
  <!-- 容器 -->
  <div ref="imgWrapRef" class="tabs-list-container flex-row" v-loading="loading" :style="getgridTemplateColumns">
    <tabsItem ref="tabsItem" class="tab-item" :style="{ width: widthVal1+'px', height: heightVal1+'px' }"
              v-for="item in list" :key="item.id" :item="item"/>
  </div>

  <!-- 滑块-->
  <el-slider v-model="value" @input="changeValue" :min="validMin" :max="imgWrapWidth" :step="sliderStep"></el-slider>
</template>
<script>
export default {
  data() {
    return {
      value: 0, // 滑块初始化值
      sliderStep: 1,// 滑块步长
      widthVal1: 0,
      heightVal1: 0,
      imgWrapWidth: 0, // 图片预览区总宽度
      gridTemplateColumns: 0,
      validMin: 0, // 最小值s
      list:[]
    }
  },
  computed: {
    getgridTemplateColumns() {
      return `gridTemplateColumns:repeat(${this.gridTemplateColumns}, 1fr)`
    },
  },
  methods: {
    // 点击标签tab,重新计算图片预览区的大小
    initImgeWrapSize() {
      if (this.isLastLevel) {
        this.$nextTick(() => {
          this.initWrapperWidth();
        })
      }
    },
    /**
     * 初始化的时候根据容器的尺寸,计算图片的大小
     */
    initWrapperWidth() {
      this.imgWrapWidth = this.$refs.imgWrapRef.offsetWidth
      this.widthVal1 = this.imgWrapWidth / 6;
      this.heightVal1 = this.widthVal1 / 260 * 342;
      this.validMin = this.widthVal1;
      this.value = this.widthVal1;
      this.sliderStep = (this.imgWrapWidth - this.widthVal1) / 100;
    },

    // 通过滑块,拖拽设置容器大小,实现图片的放大缩小【由初始的 一行10张,变成一行一张】
    changeValue(val) {
      if (!val) {
        return
      }
      this.widthVal1 = val;
      this.heightVal1 = this.widthVal1 / 260 * 342;
      // 更新网格列数
      this.gridTemplateColumns = this.calculateGridColumns(this.widthVal1);
    },
    // 计算网格列数的方法
    calculateGridColumns(widthVal) {
      if (widthVal * 2 > this.imgWrapWidth) return 1; // 2个放不下 故只显示1个
      if (widthVal * 3 > this.imgWrapWidth) return 2; // 3个放不下 故只显示2个
      if (widthVal * 4 > this.imgWrapWidth) return 3; //....
      if (widthVal * 5 > this.imgWrapWidth) return 4;
      if (widthVal * 6 > this.imgWrapWidth) return 5;
      return 6;
    },
  }
}

</script>
<style lang="scss" scoped>
.tabs-list-container {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: repeat(6, 1fr); /* 初始时 6 列 */
  gap: 0px;
  justify-items: center; /* 每个网格项在各自的单元格内居中 */
  .tab-item {
    width: 100%;
    height: auto;
    object-fit: cover;
    transition: width 0.4s, height 0.4s; /* 增加平滑过渡效果 */
    padding: 5px;
    box-sizing: border-box;
  }
}
</style>