11、Vue之分页组件(勾选、过滤)、五级联动组件、两种多层弹窗组件、问题与解决|深度选择器,收缩展开、各种滚动、拖拽、vue从虚拟DOM到真实DOM的过程, 自定义组件之图标|月季度半年全年|图片水印|pdf水印(5500行)

一、vue之分页组件(含勾选、过滤)
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>勾选和分页组件之vue2.6.10版</title>
      <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
      <style>
        table{
          border-collapse: collapse;
          border: 1px solid #cbcbcb;
          width: 1000px;
        }
        table td,table th {
          padding: 5px;
          border: 1px solid #cbcbcb;
        }
        table thead {
          background-color: #e0e0e0;
          color: #000;
          text-align: left;
        }
        .filter{
          width:998px;
          border:1px solid gray;
          padding:10px 0px;
        }
        .line{
          display:flex
        }
        .group{
          width:330px;
        }
        .label{
          display: inline-block;
          width:120px;
          height: 24px;
          line-height: 24px;
          text-align: right;
        }
        .input{
          display: inline-block;
          width:180px;
          height: 24px;
          line-height: 24px;
          border-radius: 3px;
        }
        .select{
          display: inline-block;
          width:188px;
          height: 26px;
          line-height: 26x;
          border-radius: 3px;
        }
      </style>
    </head>
    <body>
      <div id="app">
        <div style="padding-bottom:5px;color:red">
          <button style="color:red" @click="checkDatasOne.getResultOfCheckAndFilter(divideDatasOne.isShowFilter,divideDatasOne.filterOptions)">获取勾选和过滤结果</button>
          <span>{{checkDatasOne.toServerDatas}}</span>
        </div>
        <div style="padding-bottom:5px">
          <img :src="checkDatasOne.stateAllPages&&checkDatasOne.allExcludedIds.length===0?checkImg.yes:checkImg.no" @click="checkDatasOne.clickAllPages(divideDatasOne.tableDatas)"/>
          <span>{{checkDatasOne.textAllPages}}</span>
        </div>
        <div style="padding-bottom:5px">
          <button @click="divideDatasOne.toggleShowFilter()">{{divideDatasOne.isShowFilter?'关闭过滤':'使用过滤'}}</button>
          <button @click="divideDatasOne.emptyFilterOptions({value5:10})">清空过滤</button> 
          <button @click="divideDatasOne.request(1,divideDatasOne.eachPageItemsNum)">刷新</button>
        </div>
        <div style="margin-bottom:5px" class="filter" v-show="divideDatasOne.isShowFilter">
          <div class="line">
            <div class="group">
              <label class="label">标签</label>
              <input class="input" type="text" v-model="divideDatasOne.filterOptions.value1" />
            </div>
            <div class="group">
              <label class="label">这就是长标签</label>
              <input class="input" type="text" v-model="divideDatasOne.filterOptions.value2" />
            </div>
            <div class="group">
              <label class="label">标签</label>
              <input class="input" type="text" v-model="divideDatasOne.filterOptions.value3" />
            </div>
          </div>
          <div class="line" style="padding-top: 10px;">
            <div class="group">
              <label class="label">这就是长标签</label>
              <input class="input" type="text" v-model="divideDatasOne.filterOptions.value4" />
            </div>
            <div class="group">
              <label class="label">下拉框</label>
              <select class="select" v-model="divideDatasOne.filterOptions.value5">
                <option v-for="item in selectOptions" :value="item.back">{{item.front}}</option>
              </select>
            </div>
            <div class="group">
              <label class="label"></label>
              <button style="width:188px;height:28px" @click="divideDatasOne.request(1,divideDatasOne.eachPageItemsNum)">过滤</button>
            </div>
          </div>
        </div>
        <div style="width: 1000px">
          <table>
            <thead>
            <tr>
              <th><img :src="checkDatasOne.stateThisPage?checkImg.yes:checkImg.no"
                @click="checkDatasOne.clickThisPage(divideDatasOne.tableDatas,divideDatasOne.allItemsNum)"/></th>
              <th>序号</th>
              <th>数据1</th>
              <th>数据2</th>
              <th>数据3</th>
              <th>数据4</th>
              <th>数据5</th>
              <th>数据6</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="(data,index) in divideDatasOne.tableDatas">
              <td><img :src="data.state?checkImg.yes:checkImg.no" @click="checkDatasOne.clickSingleItem(data,divideDatasOne.tableDatas,divideDatasOne.allItemsNum)"/></td>
              <td>{{(divideDatasOne.nowPageNum-1)*divideDatasOne.eachPageItemsNum + (index+1)}} </td>
              <td>{{ data.key1 }}</td>
              <td>{{ data.key2 }}</td>
              <td>{{ data.key3 }}</td>
              <td>{{ data.key4 }}</td>
              <td>{{ data.key5 }}</td>
              <td>{{ data.key6 }}</td>
            </tr>
            </tbody>
          </table>
        </div>
        <divide-page :divide-datas="divideDatasOne" :check-datas="checkDatasOne" :fixed-datas="fixedDatas"></divide-page>
      </div>
    </body>
    <script>
      new Vue({
        el: '#app',
        data(){
          return {
            divideDatasOne:{
              nowPageNum:0,
              allPagesNum:0,
              allItemsNum:0,
              eachPageItemsNum:0,
              tableDatas:[],
              filterOptions:{value5:10},
              isShowFilter:false,
              otherDatas:{}
            },
            checkDatasOne:{
              idKey: 'id',//每条数据的唯一标志
              stateThisPage: false,//当前页所有项是否全选
              allIncludedIds: [],//所有被选中数据的ID构成的数组
              allExcludedIds: [],//所有没被选中数据的ID构成的数组
              textAllPages: '全选未启用,没有选择任何项!',//复选框被点击后的提示文字。
              stateAllPages: false,//复选框被点击后的提示文字。
              toServerDatas: null,
            },
          }
        },
        methods: {
    
        },
        created(){
          this.fixedDatas = {};
          this.selectOptions = [
            { back: 10, front: '来' },
            { back: 20, front: '来自于' },
            { back: 30, front: '来自于国内' },
            { back: 40, front: '来自于国内攻击' },
            { back: 50, front: '来自于国内攻击-2' }
          ];
          this.checkImg = {
            yes: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAADqADAAQAAAABAAAADgAAAAC98Dn6AAAA+UlEQVQoFZWSMU4DMRBF/584G7QSRcIxuAZKEykNEiUVHVTQRaKh4AIcgAvQpkukVDlBOAYNSGSlXXuwpViyYYFdS9aMZ/6bsezh5HZ3T2KhqkfosEhWqnjkyd1u3xWKdQMsfaEAB0Zilf8swfdU0w0klmpGpz1BvpbHcklbPf8Okts0CfJtWBTz/Yc++Jc8S3PZVQfKGwiuvMD6XYsMzm1dT/1jXKdQ8E0asHRrAzOzbC6UGINWHPQp1UQ/6wjF2LpmJSKfhti4Bi8+lhWP4I+gAqV1uqSi8j9WRuF3m3eMWVUJBeKxzUoYn7bEX7HDyPmB7QEHbRjyL+/+VnuXDUFOAAAAAElFTkSuQmCC',
            no: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAADqADAAQAAAABAAAADgAAAAC98Dn6AAAAbklEQVQoFWM8c+ZMLQMDQxUQcwAxMeAHUFEbC5CoYmNj02ZmZn5FjK6/f/+K/fr16ypIIwdIk7a29hdiNF69ehWkjIOJGMXY1IxqxBYqULEhFDiglPMDlIygKQKPryBSILUgPSCNbaC0B6RJSuQAbowizhJuOsAAAAAASUVORK5CYII=',
          }
        },
        components: {
          dividePage: {
            props: {
              divideDatas: {
                type: Object,
                default: {}
              },
              checkDatas: {
                type: Object,
                default: {}
              },
              fixedDatas: {
                type: Object,
                default: {}
              }
            },
            template: `
              <div v-show="divideDatas.allPagesNum>=1" style="display:flex;width:1000px;margin-top:20px;">
                <div>
                  <button
                    v-show="divideDatas.allPagesNum>10"
                    @click="clickDividePage('front') "
                    :disabled="divideDatas.nowPageNum===1"
                  >上一页</button>
                  <button 
                    :disabled="number==='...'"
                    v-for="number in divideArray"
                    @click="clickDividePage(number)"
                    :style="{marginRight:'5px',color:number===divideDatas.nowPageNum?'red':'gray'}"
                  >{{ number }}</button>
                  <button
                    v-show="divideDatas.allPagesNum>10"
                    @click="clickDividePage('back')" 
                    :disabled="divideDatas.nowPageNum===divideDatas.allPagesNum"
                  >下一页</button>
                </div>
                <div style="display:flex; flex:1; justify-content:flex-end;">
                  <div style="margin-right:20px;">
                    <span>转到第</span>
                    <input type="text" v-model="customString" @keydown="clickDividePage('leap',$event)" style="width:30px;">
                    <span>页</span>
                    <button @click="clickDividePage('leap',{which:13})">Go</button>
                  </div>
                  <div>
                    <span>每页显示</span>
                    <select v-model="divideDatas.eachPageItemsNum" @change="selectChange(divideDatas.eachPageItemsNum)">
                      <option v-for="item in numOptions" :value="item.back">{{item.front}}</option>
                    </select>
                    <span>条,</span>
                  </div>
                  <div>
                    <span>{{frontMoreText}}</span>
                    <span>{{totalText}}</span>
                    <span>{{divideDatas.allItemsNum}}</span>
                    <span>{{totalUnit}}</span>
                    <span>{{backMoreText}}</span>
                  </div>
                </div>  
              </div
            `,
            data() {
              return {
                customString:''
              }
            },
            created(){
              var that = this;
              //1、请求配置
              this.url = this.fixedDatas.url || '';
              this.method = this.fixedDatas.method || 'post';
              this.isShowParams = this.fixedDatas.isShowParams || false;//显式还是隐式传参。有时需要在请求发出前手动改变。
              //2、响应配置(前端通过这个配置,获取后台的数据)
              this.nowPageNum = this.fixedDatas.nowPageNum || 'nowPageNum';//来自服务器的当前页码
              this.allPagesNum = this.fixedDatas.allPagesNum || 'allPagesNum';//来自服务器的所有页页数
              this.allItemsNum = this.fixedDatas.allItemsNum || 'allItemsNum';//来自服务器的所有页数据数
              this.eachPageItemsNum = this.fixedDatas.eachPageItemsNum || 'eachPageItemsNum';//来自服务器的每页最多数据数
              this.tableDatas = this.fixedDatas.tableDatas || 'tableDatas';//来自服务器的表格数据
              //3、以下配置使用哪种转圈方式(前端根据需要决定,不受后台影响)
              this.partCircle = this.fixedDatas.partCircle;//局部是否转圈。this.fixedDatas.partCircle=$scope.partCircle={isShow =false}。
              this.isUsePartCircle = this.fixedDatas.isUsePartCircle;//局部是否转圈,由当前页的一个变量控制
              this.isUseWholeCircle = this.fixedDatas.isUseWholeCircle;//全局是否转圈,由本项目的一个服务控制
              //4、初始化以下数据,供页面使用(前端根据需要决定,不受后台影响)
              this.frontMoreText = this.fixedDatas.frontMoreText || "";//('文字 ')或者("文字 "+result.numOne+" 文字 ")
              this.totalText = this.fixedDatas.totalText || "";//'共'
              this.totalUnit = this.fixedDatas.totalUnit || '条';//总数据的单位
              this.backMoreText = this.fixedDatas.backMoreText || "";//(' 文字')或者("文字 "+result.numThree+" 文字")
              this.numOptions = [
                { back: 10, front: 10 },
                { back: 20, front: 20 },
                { back: 30, front: 30 },
                { back: 40, front: 40 },
                { back: 50, front: 50 }
              ];
              this.request = this.divideDatas.request = function (nowPageNum,eachPageItemsNum) {
                var isOnce = true;
                //1、向后台发送请求,
                //2、返回正确结果result
                var data=[];
                var allItemsNum = 193;
                var nowPageNum = nowPageNum||1;
                var eachPageItemsNum = eachPageItemsNum||10;
                var allPagesNum = Math.ceil(allItemsNum/eachPageItemsNum);
                for(var i=1;i<=allItemsNum;i++){
                  var obj={
                    id:'id'+i,
                    key1:'数据'+(i+0),
                    key2:'数据'+(i+1),
                    key3:'数据'+(i+2),
                    key4:'数据'+(i+3),
                    key5:'数据'+(i+4),
                    key6:'数据'+(i+5),
                    key7:'数据'+(i+6),
                  };
                  data.push(obj)
                }
                var tableDatas = data.slice((nowPageNum-1)*eachPageItemsNum,nowPageNum*eachPageItemsNum);
                //3、使用正确结果result
                that.customString = nowPageNum;
                that.divideDatas.nowPageNum = nowPageNum;
                that.divideDatas.allPagesNum = allPagesNum;
                that.divideDatas.allItemsNum = allItemsNum;
                that.divideDatas.eachPageItemsNum = eachPageItemsNum;
                that.divideDatas.tableDatas = tableDatas;//正常逻辑
                if(that.divideDatas.once && isOnce){//只使用一次,常用于初始化一些数据,比如过滤条件
                  that.divideDatas.once();
                  isOnce = false;
                }
                if(that.divideDatas.trueCb){//异常逻辑
                  that.divideDatas.trueCb()
                }
                if(that.checkDatas && that.checkDatas.signCheckbox){//处理勾选
                  that.checkDatas.signCheckbox(that.divideDatas.tableDatas)
                }
                that.createDividePage();//创建分页
                //4、处理错误结果
                if(that.divideDatas.errorCb){
                  that.divideDatas.errorCb()
                }
              };
              if (!this.divideDatas.isAbandonInit) {
                this.request(1,this.divideDatas.eachPageItemsNum);
              };
              this.divideDatas.toggleShowFilter = function () {
                this.isShowFilter = !this.isShowFilter;
                if (!this.isShowFilter) {
                  this.request(1,that.divideDatas.eachPageItemsNum);
                }
              };
              this.divideDatas.emptyFilterOptions = function (extraObject) {
                //清空选项时,所有值恢复成默认
                for(var key in this.filterOptions){
                  this.filterOptions[key] = undefined;
                };
                if (extraObject) {
                  //小部分选项的默认值不是undefined
                  for(var key in extraObject){
                    this.filterOptions[key] = extraObject[key];
                  };
                };
                this.request(1,that.divideDatas.eachPageItemsNum);
              };
              this.checkDatas.init=function(){//点击“刷新”、“过滤”、“清除过滤”时执行
                this.idKey = idKey ? idKey : 'id';
                this.allIncludedIds = [];
                this.allExcludedIds = [];
                this.textAllPages = '全选未启用,没有选择任何项!';
                this.stateAllPages = false;
                this.stateThisPage = false;
              };
              this.checkDatas.clickAllPages = function (itemArray) {//所有页所有条目全选复选框被点击时执行的函数
                if(this.stateAllPages){
                  if(this.allExcludedIds.length>0){
                    this.stateAllPages = true;
                    this.stateThisPage = true;
                    this.textAllPages= '全选已启用,没有排除任何项!';
                    itemArray.forEach(function (item) {
                      item.state = true;
                    });
                  }else if(this.allExcludedIds.length==0){
                    this.stateAllPages = false;
                    this.stateThisPage = false;
                    this.textAllPages= '全选未启用,没有选择任何项!';
                    itemArray.forEach(function (item) {
                      item.state = false;
                    });
                  }
                }else{
                  this.stateAllPages = true;
                  this.stateThisPage = true;
                  this.textAllPages= '全选已启用,没有排除任何项!';
                  itemArray.forEach(function (item) {
                    item.state = true;
                  });
                }
                this.allExcludedIds = [];
                this.allIncludedIds = [];
              };
              this.checkDatas.clickThisPage = function (itemsArray,allItemsNum) {//当前页所有条目全选复选框被点击时执行的函数
                var that = this;
                this.stateThisPage = !this.stateThisPage
                itemsArray.forEach(function (item) {
                  item.state = that.stateThisPage;
                  if (item.state) {
                    that.delID(item[that.idKey], that.allExcludedIds);
                    that.addID(item[that.idKey], that.allIncludedIds);
                  } else {
                    that.delID(item[that.idKey], that.allIncludedIds);
                    that.addID(item[that.idKey], that.allExcludedIds);
                  }
                });
                if(this.stateAllPages){
                  if(this.stateThisPage && this.allExcludedIds.length === 0){
                    this.textAllPages = '全选已启用,没有排除任何项!';
                  }else{
                    this.textAllPages = '全选已启用,已排除'+ this.allExcludedIds.length + '项!排除项的ID为:' + this.allExcludedIds;
                  }
                }else{
                  if(!this.stateThisPage && this.allIncludedIds.length === 0){
                    this.textAllPages='全选未启用,没有选择任何项!';
                  }else{
                    this.textAllPages = '全选未启用,已选择' + this.allIncludedIds.length + '项!选择项的ID为:' + this.allIncludedIds;
                  }
                }
              };
              this.checkDatas.clickSingleItem = function (item, itemsArray, allItemsNum) {//当前页单个条目复选框被点击时执行的函数
                var that = this;
                item.state = !item.state;
                if (item.state) {
                  this.stateThisPage = true;
                  this.addID(item[this.idKey], this.allIncludedIds);
                  this.delID(item[this.idKey], this.allExcludedIds);
                  itemsArray.forEach( function (item) {
                    if (!item.state) {
                      that.stateThisPage = false;
                    }
                  });
                } else {
                  this.stateThisPage = false;
                  this.addID(item[this.idKey], this.allExcludedIds);
                  this.delID(item[this.idKey], this.allIncludedIds);
                }
                if(this.stateAllPages){
                  if(this.stateThisPage && this.allExcludedIds.length === 0){
                    this.textAllPages = '全选已启用,没有排除任何项!';
                  }else{
                    this.textAllPages = '全选已启用,已排除'+ this.allExcludedIds.length + '项!排除项的ID为:' + this.allExcludedIds;
                  }
                }else{
                  if(!this.stateThisPage && this.allIncludedIds.length === 0){
                    this.textAllPages='全选未启用,没有选择任何项!';
                  }else{
                    this.textAllPages = '全选未启用,已选择' + this.allIncludedIds.length + '项!选择项的ID为:' + this.allIncludedIds;
                  }
                }
              };
              this.checkDatas.signCheckbox = function (itemsArray) {//标注当前页被选中的条目,在翻页成功后执行。
                var that = this;
                if(this.stateAllPages){
                  this.stateThisPage = true;
                  itemsArray.forEach(function (item) {
                    var thisID = item[that.idKey];
                    var index = that.allExcludedIds.indexOf(thisID);
                    if (index > -1) {
                      item.state = false;
                      that.stateThisPage = false;
                    } else {
                      item.state = true;
                    }
                  });
                }else{
                  this.stateThisPage = true;
                  itemsArray.forEach( function (item) {
                    var thisID = item[that.idKey];
                    var index = that.allIncludedIds.indexOf(thisID);
                    if (index === -1) {
                      item.state = false;
                      that.stateThisPage = false;
                    }
                  });
                }
              };
              this.checkDatas.addID = function (id, idArray) {
                var index = idArray.indexOf(id);
                if (index === -1) {
                  idArray.push(id);//如果当前页的单项既有勾选又有非勾选,这时勾选当前页全选,需要这个判断,以免重复添加
                }
              };
              this.checkDatas.delID = function (id, idArray) {
                var index = idArray.indexOf(id);
                if (index > -1) {
                  idArray.splice(index, 1)
                }
              };
              this.checkDatas.getResultOfCheckAndFilter = function (isShowFilter,filterOptions) {//获取发送给后台的所有参数。
                var toServerDatas;
                var allIncludedIds = that.deepClone(this.allIncludedIds);
                var allExcludedIds = that.deepClone(this.allExcludedIds);
                if (!this.stateAllPages) {
                  if (allIncludedIds.length === 0) {
                    //return 弹窗告知:没有勾选项
                  }
                  toServerDatas = {
                    isSelectAll: false,
                    allIncludedIds: allIncludedIds,
                  }
                }else {
                  toServerDatas = { //exclude
                    isSelectAll: true,
                    allExcludedIds: allExcludedIds,
                  };
                }
                if (isShowFilter) {
                  for(var key in filterOptions){
                    toServerDatas[key]=filterOptions[key]
                  }
                }
                this.toServerDatas=toServerDatas;//这行代码在实际项目中不需要
                return toServerDatas;
              }
            },
            methods: {
              deepClone : function (arrayOrObject) {
                function isArray(value) { return {}.toString.call(value) === "[object Array]"; }
                function isObject(value) { return {}.toString.call(value) === "[object Object]"; }
                var target = null;
                if (isArray(arrayOrObject)) target = [];
                if (isObject(arrayOrObject)) target = {};
                for (var key in arrayOrObject) {
                  var value = arrayOrObject[key];
                  if (isArray(value) || isObject(value)) {
                    target[key] = deepClone(value);
                  } else {
                    target[key] = value;
                  }
                }
                return target;
              },
              selectChange:function(eachPageItemsNum){
                this.divideDatas.eachPageItemsNum = eachPageItemsNum;
                this.request(1,eachPageItemsNum);
              },
              createDividePage : function () {
                var divideArray = [];
                var allPagesNum = this.divideDatas.allPagesNum;
                var nowPageNum = this.divideDatas.nowPageNum;
                if (allPagesNum >= 1 && allPagesNum <= 10) {
                  for (var i = 1; i <= allPagesNum; i++) {
                    divideArray.push(i);
                  }
                } else if (allPagesNum >= 11) {
                  if (nowPageNum > 6) {
                    divideArray.push(1);
                    divideArray.push(2);
                    divideArray.push(3);
                    divideArray.push('...');
                    divideArray.push(nowPageNum - 1);
                    divideArray.push(nowPageNum);
                  } else {
                    for (i = 1; i <= nowPageNum; i++) {
                      divideArray.push(i);
                    }
                  }
                  //以上当前页的左边,以下当前页的右边
                  if (allPagesNum - nowPageNum >= 6) {
                    divideArray.push(nowPageNum + 1);
                    divideArray.push(nowPageNum + 2);
                    divideArray.push('...');
                    divideArray.push(allPagesNum - 2);
                    divideArray.push(allPagesNum - 1);
                    divideArray.push(allPagesNum);
                  } else {
                    for (var i = nowPageNum + 1; i <= allPagesNum; i++) {
                      divideArray.push(i);
                    }
                  }
                }
                this.divideArray = divideArray;
              },
              clickDividePage : function (stringOfNum, event) {
                var allPagesNum = this.divideDatas.allPagesNum;
                var nowPageNum = this.divideDatas.nowPageNum;
                if (stringOfNum === 'front' && nowPageNum != 1) {
                  nowPageNum--;
                } else if (stringOfNum === 'back' && nowPageNum != allPagesNum) {
                  nowPageNum++;
                } else if (stringOfNum === 'leap') {
                  if (event.which != 13) return;//不拦截情形:(1)聚焦输入框、按“Enter”键时;(2)点击“GO”时
                  var customNum = Math.ceil(parseFloat(this.customString));
                  if (customNum < 1 || customNum == 'NaN') {
                    nowPageNum = 1;//不给提示
                  } else if(customNum > allPagesNum) {
                    nowPageNum = allPagesNum;//不给提示
                  } else {
                    nowPageNum = customNum;
                  }
                } else {
                  nowPageNum = Math.ceil(parseFloat(stringOfNum));
                }
                this.request(nowPageNum,this.divideDatas.eachPageItemsNum);
              },
            }
          }
        },
      })
    </script>
  </html>

二、Vue之五级联动组件-省、市、县、乡、村
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>Vue之五级联动组件-省、市、县、乡、村</title>
    </head>
    <style type="text/css">
      .edit{
        height:40px;
        line-height: 40px;
      }
      .paddingBottom{
        padding-bottom: 40px;
      }
      .question-select {
        height: 60px;
      }
      .question-select select {
        border-radius: 5px;
        box-shadow: 0 0 5px #666;
        appearance: none;
        -webkit-appearance: none;
        -moz-appearance: none;
        border: none;
        outline: none;
        height: 40px;
        padding: 0 20px;
        color: #333;
        font-size: 22px;
      }
      .question-select select.short {
        width: 120px;
      }
      .question-select select.long {
        width: 240px;
      }
      .birth-year{
        width:90px;
        margin-right: 20px;
        height: 100px;
        overflow-y: scroll;
      }
      .birth-month{
        width:70px;
        margin-right: 20px;
      }
      .birth-date{
        width:70px;
      }
    </style>
    <body>
      <div id="component">
        <five-grade :all-datas="allDatas"></five-grade>
      </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- Vue.js v2.6.10 -->
    <script type="text/javascript">
      Vue.component('five-grade', {
        template: `
          <div>
            <div class="question-select">
              <select v-model="singleProvince" class="short" @change="selectName(singleProvince)">
                <option v-for="key in allProvinces" :value="key" v-text="key"></option>
              </select>
              <select v-model="singleCity" v-show="singleProvince" class="short" @change="selectName(singleProvince,singleCity)">
                <option v-for="key in allCitys" :value="key" v-text="key"></option>
              </select>
              <select v-model="singleCounty" v-show="singleCity" class="short" @change="selectName(singleProvince,singleCity,singleCounty)">
                <option v-for="key in allCountys" :value="key" v-text="key"></option>
              </select>
              <select v-model="singleTown" v-show="singleCounty" class="long" @change="selectName(singleProvince,singleCity,singleCounty,singleTown)">
                <option v-for="key in allTowns" :value="key" v-text="key"></option>
              </select>
              <select v-model="singleVillage" v-show="singleTown" class="long" @change="selectName(singleProvince,singleCity,singleCounty,singleTown,singleVillage)">
                <option v-for="key in allVillages" :value="key" v-text="key"></option>
              </select>
            </div>
            <div class="paddingBottom">{{address}}</div>
          </div>
        `,
        props: {      
          allDatas: {
            type: Object
          }
        },
        data: function(){
          return {
            allProvinces: [],
            singleProvince: '',
            allCitys: [],
            singleCity: '',
            allCountys: [],
            singleCounty: '',
            allTowns: [],
            singleTown: '',
            allVillages: [],
            singleVillage: '',
            address: '',
          }
        },
        beforeMount: function () {
          this.selectName();
        },
        methods: {
          selectName: function(singleProvince,singleCity,singleCounty,singleTown,singleVillage){
            var siteArray = [singleProvince,singleCity,singleCounty,singleTown,singleVillage];
            var modelArray = ['singleProvince','singleCity','singleCounty','singleTown','singleVillage'];
            var optionsArray = ['allProvinces','allCitys','allCountys','allTowns','allVillages'];
            var allDatasNext;
            var address = "你选择的地址是:";
            for(var i=0;i<siteArray.length;i++){//遍历数组所有项
              if(!siteArray[i]) this[modelArray[i]] = '';
            }
            for(var i=0;i<siteArray.length;i++){//遍历数组至undefined第1次出现时停止
              allDatasNext = i == 0 ? this.allDatas : allDatasNext[siteArray[i-1]];
              if(!siteArray[i]) {
                var array = [];
                for (var key in allDatasNext) {
                  array.push(key)
                }
                this[optionsArray[i]] = array;
                break;
              }
            }
            if(this.singleProvince) address += this.singleProvince;
            if(this.singleCity) address += '-' + this.singleCity;
            if(this.singleCounty) address += '-' + this.singleCounty;
            if(this.singleTown) address += '-' + this.singleTown;
            if(this.singleVillage) address += '-' + this.singleVillage;
            this.address = address;
          }
        },
      });
      new Vue({
        el: '#component',
        data(){
          return {
            allDatas : makeallDatas()
          }
        },
        methods:{
        }
      })
      function makeallDatas() {
        var allDatas = {
          "北京市": {
            "区": {
              "通州区": {
                "中仓街道办事处": {
                  "滨河社区居委会": "110112001029",
                  "运河湾社区居委会": "110112001030"
                },
                "漷县镇": {
                  "后元化村委会": "110112106260",
                  "前元化村委会": "110112106261"
                }
              },
              "昌平区": {
                "天通苑南街道办事处":{
                  "东辰社区居委会":"110114009001",
                  "佳运园社区居委会":"110114009002",
                  "天通苑第二社区居委会":"110114009003",
                  "天通西苑第一社区居委会":"110114009004",
                  "天通东苑第一社区居委会":"110114009005",
                  "天通东苑第二社区居委会":"110114009006",
                  "天通苑第一社区居委会":"110114009007",
                  "嘉诚花园社区居委会":"110114009008",
                },
                "霍营街道办事处": {
                  "华龙苑南里社区居委会": "110114010001",
                  "华龙苑北里社区居委会": "110114010002",
                  "蓝天园社区居委会": "110114010003",
                  "天鑫家园社区居委会": "110114010004",
                  "霍营小区社区居委会": "110114010005",
                  "上坡佳园社区居委会": "110114010006",
                  "华龙苑中里社区居委会": "110114010007",
                }
              }
            },
            "县": {
              "密云县": {
                "密云镇": {
                  "小唐庄社区居委会": "110228100001",
                  "李各庄社区居委会": "110228100002",
                  "大唐庄社区居委会": "110228100003",
                  "季庄村委会": "110228100205",
                },
                "溪翁庄镇": {
                  "东智北村委会": "110228101209",
                  "石墙沟村委会": "110228101210",
                  "黑山寺村委会": "110228101211",
                  "立新庄村委会": "110228101212",
                },
              },
              "延庆县": {
                "旧县镇": {
                  "常里营村委会": "110229104215",
                  "盆窑村委会": "110229104216",
                  "团山村委会": "110229104217",
                  "大柏老村委会": "110229104218",
                },
                "珍珠泉乡": {
                  "双金草村委会": "110229214209",
                  "小川村委会": "110229214210",
                  "小铺村委会": "110229214211",
                  "仓米道村委会": "110229214212",
                }
              }
            }
          },
          "河南省": {
            "郑州市": {
              "金水区": {
                "凤凰台街道办事处": {
                  "凤凰台社区居民委员会": "410105013004",
                  "王庄社区居民委员会": "410105013005",
                  "张庄社区居民委员会": "410105013006",
                  "凤凰城社区居民委员会": "410105013007",
                },
                "金光路街道办事处": {
                  "徐庄村委会": "410105564201",
                  "贾陈村委会": "410105564202",
                  "柳园口村委会": "410105564203",
                  "马楼村委会": "410105564204",
                }
              },
              "登封市": {
                "嵩阳街道办事处": {
                  "玉溪路居委会": "410185001014",
                  "苹果园居委会": "410185001015",
                  "颖河路居委会": "410185001016",
                  "守敬路居委会": "410185001017",
                },
                "少林街道办事处": {
                  "塔沟居委会": "410185002001",
                  "少林居委会": "410185002002",
                  "耿庄村委会": "410185002203",
                  "王庄村委会": "410185002204",
                },
                "送表矿区": {
                  "东送表村委会": "410185400201",
                  "梁庄村委会": "410185400202",
                  "安庄村委会": "410185400203",
                  "刘楼村委会": "410185400204",
                }
              }
            },
            "信阳市": {
              "浉河区": {
                "老城街道办事处": {
                  "义阳居委会": "411502001001",
                  "三里店居委会": "411502001003",
                  "东方红居委会": "411502001004",
                  "浉河居委会": "411502001007",
                },
                "民权街道办事处": {
                  "民权居委会": "411502002001",
                  "新生居委会": "411502002002",
                  "成功居委会": "411502002003",
                  "白果树居委会": "411502002004",
                },
                "车站街道办事处": {
                  "工区东居委会": "411502003001",
                  "工区路居委会": "411502003002",
                  "六里棚居委会": "411502003003",
                  "新公房居委会": "411502003004",
                }
              },
              "固始县": {
                "段集镇": {
                  "街道居委会": "411525109001",
                  "段集村委会": "411525109201",
                  "棠树岗村委会": "411525109202",
                  "青峰村委会": "411525109203",
                  "下楼村委会": "411525109204",
                  "桂岭村委会": "411525109205",
                  "窑沟村委会": "411525109206",
                  "五尖山村委会": "411525109207",
                  "柳林村委会": "411525109208",
                  "齐山村委会": "411525109209",
                  "钓鱼台村委会": "411525109210",
                  "蒋营村委会": "411525109211",
                  "庙山村委会": "411525109212",
                  "汪旱庄村委会": "411525109213",
                  "乐道村委会": "411525109214",
                  "姚老家村委会": "411525109215",
                  "赵营村委会": "411525109216",
                  "童庙村委会": "411525109217",
                  "高庙村委会": "411525109218"
                },
                "武庙集镇": {
                  "街道居委会": "411525113001",
                  "汪小庄村委会": "411525113201",
                  "黄土岭村委会": "411525113202",
                  "平阳村委会": "411525113203",
                  "长江河村委会": "411525113204",
                  "锁口村委会": "411525113205",
                  "皮冲村委会": "411525113206",
                  "刘中楼村委会": "411525113207",
                  "迎水寺村委会": "411525113208",
                  "余楼村委会": "411525113209",
                  "钱老楼村委会": "411525113210",
                  "新店村委会": "411525113211",
                  "徐小店村委会": "411525113212",
                  "邓岭村委会": "411525113213",
                  "太平村委会": "411525113214",
                  "汪楼村委会": "411525113215",
                  "李瓦房村委会": "411525113216"
                },
                "祖师庙镇": {
                  "祖师庙居委会": "411525117001",
                  "仰天洼社区居委会": "411525117002",
                  "小店村委会": "411525117201",
                  "王行村委会": "411525117202",
                  "大冲村委会": "411525117203",
                  "松林村委会": "411525117204",
                  "刘楼村委会": "411525117205",
                  "羁马村委会": "411525117206",
                  "黄楼村委会": "411525117207",
                  "彭畈村委会": "411525117208",
                  "童圩村委会": "411525117209",
                  "仓房村委会": "411525117210",
                  "毛店村委会": "411525117211",
                  "万岗村委会": "411525117212",
                  "七冲村委会": "411525117213",
                  "三区村委会": "411525117214",
                  "杨楼村委会": "411525117215"
                }
              },
              "息县": {
                "杨店乡": {
                  "杨店村委会": "411528204200",
                  "安寨村委会": "411528204201",
                  "何庄村委会": "411528204202",
                  "李大庄村委会": "411528204203",
                },
                "张陶乡": {
                  "张陶村委会": "411528205200",
                  "曹林村委会": "411528205201",
                  "陈圈行村委会": "411528205202",
                  "大陈庄村委会": "411528205203",
                },
                "白土店乡": {
                  "白土店村委会": "411528206200",
                  "白衣阁村委会": "411528206201",
                  "大江庄村委会": "411528206202",
                  "桂庄村委会": "411528206203",
                },
                "岗李店乡": {
                  "岗李店村委会": "411528207200",
                  "大彭庄村委会": "411528207201",
                  "方老庄村委会": "411528207202",
                  "贾后寨村委会": "411528207203",
                }
              }
            },
          },
        };
        return allDatas;
      }
    </script>
  </html>

三、2种三层弹窗组件之simple-dialog
1、不可拖拽
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Vue多层弹窗</title>
      <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
      <style>
        .simpleDialog {
          position: fixed;
          width: 100%;
          height: 100%;
          top: 0;
          left: 0;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .simpleDialog .mask {
          position: fixed;
          width: 100%;
          height: 100%;
          top: 0;
          left: 0;
          background: black;
          opacity: 0.5;
        }
        .simpleDialog .content {
          position: fixed;
          background: white;
          opacity: 1;
          display: flex;
          flex-direction: column;
        }
        .simpleDialog .content .title {
          display: flex;
          background: blue;
          color: white;
          padding: 10px;
          cursor: pointer;
        }
        .simpleDialog .content .conform {
          display: flex;
          justify-content: center;
          padding: 10px;
          background: blue;
        }
      </style>
    </head>
    <body>
      <div id="el">
        <button @click="clickButton()" style="margin-top: 30px;">
          点击-出现-弹窗
        </button>
        <simple-dialog :required-data="requiredDataOut">
          插槽一
          <simple-dialog :required-data="requiredDataMid">
            插槽二
            <simple-dialog :required-data="requiredDataIn">
              插槽三
            </simple-dialog>
          </simple-dialog>
        </simple-dialog>
      </div>
      <script>
        Vue.component('simple-dialog', {
          template: `
            <div>
              <div class="simpleDialog" v-show="requiredData.isShow">
                <div class="mask" v-show="requiredData.isShow"></div>
                <div class="content" v-show="requiredData.isShow">
                  <div class="title">
                    <span>系统消息</span>
                  </div>
                  <div :style="{width:requiredData.width||'800px',height:requiredData.height||'400px'}">
                    <slot></slot>
                  </div>
                  <div class="conform">
                    <button v-on:click="close()">关闭</button>
                    <button v-on:click="open()">打开</button>
                  </div>
                <div>
              </div>
            </div>
          `,
          props: {      
            requiredData: {
              type: Object
            }
          },
          data: function() {
            return {}
          },
          methods: {
            close: function () {
              this.requiredData.isShow = false;
              if(this.requiredData.closeFn) this.requiredData.closeFn();
            },
            open: function () {
              if(this.requiredData.openFn) this.requiredData.openFn();
            }                              
          }
        });
        new Vue({
          el: '#el',
          data(){
            var that = this;
            return {
              requiredDataOut : {
                isShow : false,
                width : '900px',
                height : '600px',
                openFn : function () {
                  that.requiredDataMid.isShow = true;
                }
              },
              requiredDataMid : {
                isShow : false,
                width : '600px',
                height : '400px',
                openFn : function () {
                  that.requiredDataIn.isShow = true;
                },
              },
              requiredDataIn : {
                isShow : false,
                width : '300px',
                height : '200px',
              },
              //一层弹窗可以如下使用
              //requiredDataOut = {
              //  isShow : false,
              //};
              //clickButton() {
              //  requiredDataOut.isShow = true;
              //};
            }
          },
          methods:{
            clickButton() {
              this.requiredDataOut.isShow = true;
            }
          }
        })
      </script>
    </body>
  </html>
2、可拖拽(含插槽)
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Vue多层弹窗</title>
      <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
      <script>
        function drag(wholeTitleId, wholeContentId) {
          var wholeTitleId = wholeTitleId||'titleId';
          var wholeContentId = wholeContentId||'contentId';
          var oDiv = document.getElementById(wholeContentId);
          if(!oDiv) return;
          oDiv.onmousedown = down;
          function processThis(fn, nowThis) {
            return function (event) {
              fn.call(nowThis, event);
            };
          }
          function down(event) {
            event = event || window.event;
            if (event.target.id != wholeTitleId) return;
            this.initOffsetLeft = this.offsetLeft;
            this.initOffsetTop = this.offsetTop;
            this.initClientX = event.clientX;
            this.initClientY = event.clientY;
            this.maxOffsetWidth =
              (document.documentElement.clientWidth || document.body.clientWidth) -
              this.offsetWidth;
            this.maxOffsetHeight =
              (document.documentElement.clientHeight ||
                document.body.clientHeight) - this.offsetHeight;
            if (this.setCapture) {
              this.setCapture();
              this.onmousemove = processThis(move, this);
              this.onmouseup = processThis(up, this);
            } else {
              document.onmousemove = processThis(move, this);
              document.onmouseup = processThis(up, this);
            }
          }
          function move(event) {
            var nowLeft = this.initOffsetLeft + (event.clientX - this.initClientX);
            var nowTop = this.initOffsetTop + (event.clientY - this.initClientY);
            this.style.left = nowLeft + 'px';
            this.style.top = nowTop + 'px';
          }
          function up() {
            if (this.releaseCapture) {
              this.releaseCapture();
              this.onmousemove = null;
              this.onmouseup = null;
            } else {
              document.onmousemove = null;
              document.onmouseup = null;
            }
          }
        };
      </script>
      <style>
        .simpleDialog {
          position: fixed;
          width: 100%;
          height: 100%;
          top: 0;
          left: 0;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .simpleDialog .mask {
          position: fixed;
          width: 100%;
          height: 100%;
          top: 0;
          left: 0;
          background: black;
          opacity: 0.5;
        }
        .simpleDialog .content {
          position: fixed;
          background: white;
          opacity: 1;
          display: flex;
          flex-direction: column;
        }
        .simpleDialog .content .title {
          display: flex;
          background: blue;
          color: white;
          padding: 10px;
          cursor: pointer;
        }
        .simpleDialog .content .conform {
          display: flex;
          justify-content: center;
          padding: 10px;
          background: blue;
        }
      </style>
    </head>
    <body>
      <div id="el">
        <button @click="clickButton()" style="margin-top: 30px;">
          点击-出现-弹窗
        </button>
        <simple-dialog :required-data="requiredDataOut">
          插槽一
          <simple-dialog :required-data="requiredDataMid">
            插槽二
            <simple-dialog :required-data="requiredDataIn">
              插槽三
            </simple-dialog>
          </simple-dialog>
        </simple-dialog>
      </div>
      <script>
        Vue.component('simple-dialog', {
          template: `
            <div>
              <div class="simpleDialog" v-show="requiredData.isShow">
                <div class="mask" v-show="requiredData.isShow"></div>
                <div class="content" v-show="requiredData.isShow" :id="requiredData.contentId||'contentId'">
                  <div class="title" :id="requiredData.titleId||'titleId'">
                    <span>系统消息</span>
                  </div>
                  <div :style="{width:requiredData.width||'800px',height:requiredData.height||'400px'}">
                    <slot></slot>
                  </div>
                  <div class="conform">
                    <button v-on:click="close()">关闭</button>
                    <button v-on:click="open()">打开</button>
                  </div>
                <div>
              </div>
            </div>
          `,
          props: {      
            requiredData: {
              type: Object
            }
          },
          data: function() {
            return {}
          },
          methods: {
            close: function () {
              this.requiredData.isShow = false;
              if(this.requiredData.closeFn) this.requiredData.closeFn();
              var content = this.requiredData.contentId;
              document.getElementById(content).style.cssText = "position:fixed;display:flex;";
            },
            open: function () {
              if(this.requiredData.openFn) this.requiredData.openFn();
            }                              
          },
        });
        new Vue({
          el: '#el',
          data(){
            var that = this;
            return {
              requiredDataOut : {
                isShow : false,
                width : '900px',
                height : '600px',
                titleId : "titleId1",
                contentId : "contentId1",
                openFn : function () {
                  that.requiredDataMid.isShow = true;
                  drag(that.requiredDataMid.titleId,that.requiredDataMid.contentId);
                }
              },
              requiredDataMid : {
                isShow : false,
                width : '600px',
                height : '400px',
                titleId : "titleId2",
                contentId : "contentId2",
                openFn : function () {
                  that.requiredDataIn.isShow = true;
                  drag(that.requiredDataIn.titleId,that.requiredDataIn.contentId);
                },
              },
              requiredDataIn : {
                isShow : false,
                width : '300px',
                height : '200px',
                titleId : "titleId3",
                contentId : "contentId3",
              },
              //一层弹窗可以如下使用
              //requiredDataOut = {
              //  isShow : false,
              //};
              //clickButton() {
              //  requiredDataOut.isShow = true;
              //};
            }
          },
          methods:{
            clickButton() {
              this.requiredDataOut.isShow = true;
              drag(this.requiredDataOut.titleId,this.requiredDataOut.contentId);
            }
          }
        })
      </script>
    </body>
  </html>
 
四、2种三层弹窗组件之el-dialog
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>vue2.6.10组件之el-dialog多层弹窗</title>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.10.1/index.js"></script>
    <link href="https://cdn.bootcss.com/element-ui/2.10.1/theme-chalk/index.css" rel="stylesheet">
    <style>
      #app{
        display: flex;
        justify-content: space-between;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <el-button type="text" @click="outerVisible = true">点击打开外层弹窗</el-button>
      <el-dialog
        width="70%"
        title="外层"
        :visible.sync="outerVisible"
      > 
        以下是外层弹窗的插槽<br/>
        <div style="height:200px;border:1px solid #ccc;padding:20px;">
          width="70%",决定弹-窗的宽<br/>
          style="height:200px",通过给弹-窗插槽的部分标签设置高,决定弹-窗的高,如此框<br/>
          :visible.sync="middleVisible",传递引用,此处改变,别处也改变<br/>
          :visible="middleVisible",传递普通值,此处改变,别处不改变<br/>
          <el-dialog
            width="50%"
            title="中层"
            :visible.sync="middleVisible"
            append-to-body
          >
            以下是中层弹-窗的插槽<br/>
            <el-dialog
              width="30%"
              title="内层"
              :visible.sync="innerVisible"
              append-to-body
            >
              以下是内层弹-窗的插槽<br/>
              <div slot="footer" class="dialog-footer">
                <el-button @click="innerVisible = false">关闭内层</el-button>
                <el-button type="primary" @click="innerVisible = false">关闭内层</el-button>
              </div>
              以上是内层弹-窗的插槽<br/>
            </el-dialog>
            <div slot="footer" class="dialog-footer">
              <el-button @click="middleVisible = false">关闭中层</el-button>
              <el-button type="primary" @click="innerVisible = true">打开内层</el-button>
            </div>
            以上是中层弹-窗的插槽<br/>
          </el-dialog>
        </div>
        <div slot="footer" class="dialog-footer">
          <el-button @click="outerVisible = false">关闭外层</el-button>
          <el-button type="primary" @click="middleVisible = true">打开中层</el-button>
        </div>
        以上是外层弹-窗的插槽(代码中,此句位于按钮下面)<br/>
      </el-dialog>
    </div>
  </body>
  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          outerVisible: false,
          middleVisible: false,
          innerVisible: false
        };
      },
      methods: {
        
      },
      components: {
        
      },
    })
  </script>
  </html>

五、vue问题与解决
1、问题1,用插件vod-js-sdk-v6上传视频时
   来源,https://cloud.tencent.com/developer/ask/260695
  (1)现象,报错“Error: ugc upload | signature verify forbidden”
  (2)原因,
    A、前端向后台索要签名,后台用小权限“腾讯账户”向腾讯索要签名,
    B、腾讯给后台小权限签名,后台给前端小权限签名,前端将小权限签名发给腾讯,腾讯给前端报错
  (3)解决,让后台换用大权限“腾讯账户”
2、问题2
  (1)现象,不是第一页的最后一页为空白
  (2)原因,最后一页不是第一页,删除最后一页的唯一项,然后请求当前页数据
  (3)解决, 
		delModelTrainings({trainingId: row.trainingId}).then(() => {
			ElMessage.success('删除成功');
			var index = (page.value.pageNum - 1)*page.value.pageSize + 1;
			//当前页第1项的序号>每页条数(当前页不是第1页) && 当前页第1项的序号=数据总数(当前页是最后1页且只有1项)
			if(index>page.value.pageSize && index===page.value.count){
				page.value.pageNum--;
			}
			listData();
		})
3、问题3,
  (1)现象,:src 文件路径错误问题的解决方法
  (2)解决,
    <template>
      <img :src="item.url" alt="logo" />
    </template>
    import background from '@/assets/image/background.png'
    var collect = reactive([
      {
        url: background, //url: '/src/assets/images/logo.png' 注释这样的写法会出错
        teacher: "讲师:李老师",
      }
    ]);
4、问题4
  (1)报错信息:[VUE ERROR] Invalid default value for prop "slides": 
    Props with type Object/Array must use a factory function to return the default value
  (2)错误原因:对于数组和对象类型的prop,Vue官方从最初的版本就推荐使用工厂函数返回默认值,而不是直接赋值空数组[]或空对象{}
  (3)错误示例
    props: {
      sizeRadio1: {
        type: Array,
        default: []
      }
      sizeRadio2: {
        type: Object,
        default: {}
      }
    },
  (4)正确示例
    props: {
      sizeRadio1: {
        type: Array,
        default: function() {
          return []
        }
      }
      sizeRadio2: {
        type: Object,
        default: function() {
          return {}
        }
      }
    },
5、问题5,汉化(汉字、中文、配置)
  (1)方案一,在App.vue中
    <script >
      import { defineComponent } from 'vue'
      import { ElConfigProvider } from 'element-plus'
      import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
      export default defineComponent({
        components: {
          ElConfigProvider,
        },
        setup() {
          return {
            locale: zhCn,
          }
        },
      })
    </script>
    <template>
      <div style="height:100%">
        <el-config-provider :locale="locale">
          <router-view v-slot="{ Component }">
            <keep-alive>
              <component :is="Component" />
            </keep-alive>
          </router-view>
      </el-config-provider>
      </div>
    </template>
  (2)方案二,在main.js中
    import ElementPlus from 'element-plus'
    import zhCn from 'element-plus/es/locale/lang/zh-cn'
    app.use(ElementPlus, { locale: zhCn })
    app.mount('#app')
6、问题6,深度选择器(也叫穿透选择器)!!!
  (1)>>>(直接穿透),在Vue3中已废弃,
    A、适用场景:Vue2的scoped样式中,不支持大多数CSS预处理器(如Sass、Less、Stylus)
    B、示例:
      <style scoped>
        /* 穿透到子组件中类名为.child 的元素 */
        .parent >>> .child {
          color: red;
        }
      </style>
  (2)/deep/(通用穿透),在Vue3中已废弃,与>>>类似,但兼容性更好
    A、适用场景:Vue2的scoped样式,支持大多数CSS预处理器(如Sass、Less、Stylus)
    B、示例:
      <style scoped lang="scss">
        /* 穿透到深层的 .deep-element 元素 */
        .container /deep/ .deep-element {
          font-size: 16px;
        }
      </style>
  (3)::v-deep(Vue专用穿透),Vue官方推荐的深度选择器,兼容性最佳
    A、适用场景:Vue2.7+和Vue3,支持所有预处理器
    B、示例:
      <style scoped>
        /* Vue2.7+语法 */
        ::v-deep .child-component button {
          background: blue;
        }
        /* Vue3推荐语法 */
        :deep(.child-component) button {
          background: blue;
        }
      </style>

六、收缩展开
1、多行收缩为2行,点击则“用动画”展开
  (1)隐藏Outer,不能触发点击事件
  (2)获取初始数据;内层显示full区域,异步计算full区域的高度并存储、显示fold区域、隐藏full区域
  (3)显示Outer
  (4)缺少1、3步骤,页面会闪烁
  (5)点击箭头,内层“用动画”显示溢出区域,隐藏初始区域
    <template>
      <div :style="{visibility: isOuterShow ? 'visible':'hidden'}">
        <div v-for="(item,index) in allNote" :style="{'background':item.readStatus?'#eaeef5':'#f5f6f9'}" class="mynote" @click="clickNoteItem(item)"  :key="index">
          <div class="mynote-item">
            <div>{{item.title}}</div>
            <div>
              <span style="padding-right: 30px;">{{millsecondsToDate(item.receiveTime)}}</span> 
              <el-icon :style="{visibility: item.isShowArrow ? 'visible':'hidden'}"  class="cursor">
                <ArrowRight style="color: gray" v-show="item.isFoldShow" @click.stop="clickItemArrow(item)" />
                <ArrowDown style="color: gray" v-show="!item.isFoldShow" @click.stop="clickItemArrow(item)" />
              </el-icon>
            </div>
          </div>
          <div v-show="isFullShow" class="isFullShow">{{item.content}}</div>
          <div v-show="item.isFoldShow" class="fold_">{{item.content}}</div>
          <!-- 下面,from-height,to-height(过程伴随overflow:hidden,应当与style-height一致) -->
          <div v-show="!item.isFoldShow" 
              :class="!item.isFoldShow? 'divDown' + item.index:''" 
              :style="{'height':item.height + 'px','overflow':'hidden'}" 
          >{{item.content}}</div>
        </div>
      </div>
    </template>
    <script setup>
      var isOuterShow = ref(false);//防止闪烁
      var isFullShow = ref(false);//计算行高
      var clickItemArrow = function(item){
        item.isFoldShow = !item.isFoldShow;
      };
      var getData = function(){
        getAllNotes(data).then(function(result){
          isFullShow.value = true;
          allNote.length = 0;
          total.value = result.data.total;
          var list = result.data.list;
          for(var i = 0; i<list.length; i++){
            list[i].isShowArrow = false; 
            list[i].isFoldShow = false; 
            allNote.push(list[i])
          } 
          setTimeout(function(){
            var all = document.getElementsByClassName('isFullShow')
            for(var i = 0; i<all.length; i++){
              if(all[i].clientHeight > 50) {
                allNote[i].isShowArrow = true;
                allNote[i].isFoldShow = true;
                allNote[i].index = i;
                allNote[i].height = all[i].clientHeight;
                document.styleSheets[0].insertRule(
                  "@keyframes moveDown" + i +
                  "{" +
                    "from { height: 40px; }" +
                    "to { height: " + all[i].clientHeight + "px; }" +
                  "}"
                )
                document.styleSheets[0].insertRule(
                  ".divDown" + i + 
                  "{" +
                    "animation: moveDown" + i + " 2s ease 0s 1 normal;" + /* 非常重要:2s前面有空格 */
                  "}"
                )
              }
            }
            isFullShow.value = false;
            isOuterShow.value = true;
          });
        })
      };
      onMounted(function() {
        getData();
      })
    </script>
    <style lang="scss">
      .cursor{
        cursor: pointer;
      }
      .fold_{
        overflow: hidden;
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
      }
    </style>

七、各种滚动
1、滚动滚轮,元素底部从视口底部出现
  (1)可演示
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>元素底部出现,触发事件</title>
        <style>
          .div{
            height: 500px;
          }
          #elementToTrack {
            height: 400px;
            background: gray;
          }
        </style>
      </head>
      <body>
        <div class="div"></div>
        <div class="div"></div>
        <div class="div"></div>
        <div id="elementToTrack"></div>
        <div class="div"></div>
      </body>
      <script>
        var element = document.getElementById('elementToTrack');
        window.onscroll = function() {
          var viewportHeight = window.innerHeight || document.documentElement.clientHeight;//获取视口高度
          var viewportBottom = window.scrollY + viewportHeight;//页面顶部到视口底部的距离
          var elementBottom = element.offsetTop + element.offsetHeight;//页面顶部到元素底部的距离
          if (elementBottom < viewportBottom) {//如果元素底部高于视口底部(元素完全进入视口)!!!
            console.log(viewportBottom, elementBottom);
          }
        };
      </script>
    </html>
  (2)真实项目
    需求:
      A、表格底部消失在页面下面,左右滚动条出现在页面底部;
      B、表格底部出现,滚动条的位置恢复为默认的表格底部
    //src/view/base/search-topical-table.vue;57--59;202--230;233
    watch(() => props, (newVal) => {
      setTimeout(() => {
        checkScroll()
      },100)
    }, { immediate: true, deep: true })
    var tableBody = null;
    var container = null;
    var isOver = ref(false)
    onMounted(() => {
      container = document.querySelector('.main-container');
      tableBody = document.querySelector('.el-table__body');
      container.addEventListener('scroll', checkScroll);
      checkScroll();
    })
    onUnmounted(() => {
      container.removeEventListener('scroll', checkScroll)
    })
    function checkScroll() {
      //获取目标元素的底部位置
      var tableBottom = tableBody.getBoundingClientRect().bottom;
      var documentBottom = document.body.clientHeight;
      //表格底部位于页面底部上方
      if ( tableBottom < documentBottom ) {
        isOver.value = true;
      } else {
        isOver.value = false;
      }
    } 
    <div class="search-topical-table" :class="{'search-topical-table-fixed':!isOver}">
      <el-table></el-table>
      <el-pagination/>
    </div>
    <style lang="scss">
      .search-topical-table-fixed{
        .el-table__body-wrapper .el-scrollbar__bar.is-horizontal{
          position: fixed;
          bottom: 10px;
          left:165px;
        }
      }
    </style>
  (3)真实项目
    需求:滚动条的位置由默认的表格底部改为页面底部
    <template>
      <div class="search-topical-table">
        <el-table></el-table>
        <el-pagination/>
      </div>
    </template>
    <style lang="scss">
      .search-topical-table{
        .el-table__body-wrapper .el-scrollbar__bar.is-horizontal{
          position: fixed;
          bottom: 10px;
          height: 10px;
        }
      }
    </style>   
2、触发按钮,“页面”交替回到顶部底部
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>页面滚动条回到顶部示例</title>
      <style>
        button {
          color: red;
          font-size: 50px;
          border-radius: 10px;
          position: fixed;
          bottom: 0;
          right: 0;
        }
        .black {
          background: #000;
          color: #fff;
        }
        .div {
          height: 2000px;
          background-color: gray;
        }
      </style>
    </head>
    <body>
      <button id="btn">顶部--底部</button>
      <div class="black">顶部</div>
      <div class="div"></div>
      <div class="black">底部</div>
    </body>
    <script>
      var y = 0 ;
      var btn = document.getElementById('btn');
      btn.addEventListener('click', function() {
        y = y == 0 ? 2000 : 0;
        window.scrollTo({
          left: 0,
          top: y,
          behavior: 'smooth'
        });
      });
    </script>
  </html>
3、触发按钮,“元素”交替回到顶部底部
  (1)演示示例
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>元素滚动条回到顶部示例</title>
        <style>
          button {
            color: red;
            font-size: 50px;
            border-radius: 10px;
            position: fixed;
            top: 50px;
            right: 50px;
          }
          #myDiv {
            height: 500px;
            overflow: auto;
          }
          .black {
            background: #000;
            color: #fff;
          }
          #myDiv .myDiv {
            height: 1000px;
            background: gray;
          }
        </style>
      </head>
      <body>
        <button onclick="clickBtn()">顶部--底部</button>
        <div id="myDiv">
          <div class="black">顶部</div>
          <div class="myDiv"></div>
          <div class="black">底部</div>
        </div>
      </body>
      <script>
        var y = 0 ;
        function clickBtn() {
          y = y == 0? 1000 : 0;
          var div = document.getElementById('myDiv');
          div.scrollTo(0, y);
        }
      </script>
    </html>
  (2)项目示例 
    .app-main {
      /*50 = navbar */
      width: 100%;
      padding-bottom: 85px;
      margin-bottom: 20px;
      box-sizing: border-box;
      position: relative;
        //以下是,新增内容
      overflow: auto;
    }
    method(params).then(res => {
      allTableData.value = res.data.rows
      page.value.pageNum = res.data.pageNum
      page.value.pageSize = res.data.pageSize
      page.value.count = res.data.recordCount
      loading.value = false
      //以下是,新增内容
      var height = window.innerHeight - 100
      var ele = document.getElementsByClassName('app-main')[0];
      ele.style.height = height + 'px'
      ele.scrollTo({ top: 0, behavior: 'smooth' })
    })
4、初始化时,“元素”滚动条回到底部
  (1)html
    <div class="training-result-code">
      <div class="training-result-title">
        训练日志
      </div>
      <div class="training-result-code_content1" ref="divShow">
        <div ref="divSonShow">
          {{ modelLogs }}
        </div>
      </div>
      <div class="training-result-code_content2" ref="divHide" >
        {{ modelLogs }}
      </div>
    </div>
    /* 以下弹窗中使用 */
    <el-dialog title="查看日志" v-model="dialogVisible" width="70%">
      <div class="record-detail-code">
        <div class="record-detail-code_content1" ref="divShow">
          <div ref="divSonShow">
            {{ modelLogs }}
            <div style="padding-bottom: 100px"></div>
          </div>
        </div>
        <div class="training-result-code_content2" ref="divHide" >
          {{ modelLogs }}
        </div>
      </div>
      <template #footer>
        <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
      </template>
    </el-dialog>
  (2)css
    &-code{
      background-color: #fff;
      border-radius: 10px;
      height: 500px;
      padding: 30px;
      &_content1{
        background-color: #000;
        overflow: scroll;
        color: #fff;
        white-space: pre-wrap;
        height: 420px;
      }
      &_content2{
        background-color: #000;
        overflow: scroll;
        color: #fff;
        white-space: pre-wrap;
        visibility: hidden;
      }
    }
    /* 以下弹窗中使用 */
    .record-detail-code{
      background-color: #fff;
      border-radius: 10px;
      height: 500px;
      padding: 10px;
      overflow: hidden;
      &_content1{
        background-color: #000;
        overflow: scroll;
        color: #fff;
        white-space: pre-wrap;
        height: 490px;
      }
      &_content2{
        background-color: #000;
        overflow: hidden;
        color: #fff;
        white-space: pre-wrap;
        visibility: hidden;
      }
    }
  (3)js
    const modelLogs = ref('');
    const divShow = ref(null);
    const divSonShow = ref(null);
    const divHide = ref(null);
    const timerId = ref(null);
    const getLogs = function () {
      getModelTrainingLogs({trainingId: route.query.trainingId}).then(res => {
        modelLogs.value = res.data.runningLogs||'没有训练日志';
        setTimeout(() => {
          divSonShow.value.style.height = divHide.value.clientHeight + 100 + 'px';
          divShow.value.scrollTop = divHide.value.clientHeight ;
        });
      })
    }
    getLogs()
    var textAry = ['训练失败','训练完成'];
    if(textAry.indexOf(route.query.text) === -1){
      timerId.value = setInterval(() => {
        getLogs()
      }, 500);
    }	
    /* 以下弹窗中使用 */
    const modelLogs = ref('');
    const divShow = ref(null);
    const divSonShow = ref(null);
    const divHide = ref(null);
    const showRecord = function(row){
      dialogVisible.value = true;
      getModelTrainingLogs({trainingId: row.trainingId}).then(res => {
        modelLogs.value = res.data.runningLogs||'没有训练日志';
        setTimeout(() => {
          divSonShow.value.style.height = divHide.value.clientHeight + 'px';
          divShow.value.scrollTop = divHide.value.clientHeight*5 ;
        });
      })
    }

八、拖拽
1、真实案例(与el-checkbox-group联合使用,拖拽之勾选表格列表)
  <script setup>
    import draggable from 'vuedraggable'
    const checkList = ref([])
    const columns = ref([
        {
            "label": "序号",
            "prop": "processVersionSid",
            "canChange": false,
            "tip": "数据示例:2346"
        },
        {
            "label": "栏目代码",
            "prop": "columnCode",
            "canChange": false,
            "tip": "数据示例:J875023"
        },
        {
            "label": "详细情况说明",
            "prop": "reasonDetail",
            "canChange": true,
            "tip": "数据示例:数据示例示例示例"
        },
        {
            "label": "系统备案栏目首播时间",
            "prop": "planPremiereTime",
            "canChange": true,
            "tip": "数据示例:每周四 7:00 - 8:00"
        },
        {
            "label": "中心核对栏目首播信息",
            "prop": "checkPremiereTime",
            "canChange": true,
            "tip": "数据示例:每周四 7:00 - 8:00"
        },
        {
            "label": "栏目重播信息",
            "prop": "replayTime",
            "canChange": true,
            "tip": "数据示例:每周四 7:00 - 8:00"
        },
        {
            "label": "完成率",
            "prop": "completeRate",
            "canChange": true,
            "tip": "数据示例:98%"
        },
        {
            "label": "备注",
            "prop": "remark",
            "canChange": false,
            "tip": ""
        }
    ])
  </script>
  <template>
    <div class="column-select-warp">
      <el-checkbox-group v-model="checkList">
        <draggable
          v-model="columns"
          :force-fallback="true"
          chosen-class="chosen"
          draggable=".drag"
          animation="300"
          item-key="prop"
        ><!-- 
          draggable=".drag",拖拽项的class;
          :class="{'drag': element.canChange}",该项是否添加class  -->
          <template #item="{ element, index }">
            <div class="column-select-item" :class="{'drag': element.canChange}">
              <svg-icon iconClass="move" class="move-icon"></svg-icon>
              <el-checkbox :disabled="!element.canChange" :value="element.prop"  size="large" >
                <span>{{ element.label }}</span>
                <span class="column-select-item__tip">{{ element.tip }}</span>
              </el-checkbox>
            </div>
          </template>
        </draggable>
      </el-checkbox-group>
    </div>
  </template>
2、可运行案例(与table联合使用,可拖动表头栏和表体行)
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8" />
      <title>vue.draggable、表格、vue3联合示例</title>
      <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
      <script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
      <script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
      <script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>
    </head>
    <body style="padding:10px;">
      <div id="app">
        <itxst-component></itxst-component>
      </div>
    </body>
    <script type="x-template" id="itxst">
      <div>教程,https://www.itxst.com/vue-draggable-next/tutorial.html</div>
      <div>案例,https://debug.itxst.com/js/zfzeny7f</div>
      <table class="tb">
        <thead>
          <draggable
            v-model="headers"
            animation="200"
            tag="tr"
            :item-key="(key) => key"
          >
            <template #item="{ element: header, index: index}">
              <th class="move">
                {{ header }}
              </th>
            </template>
          </draggable>
        </thead>
        <draggable
          :list="list"
          handle=".move"
          animation="300"
          @start="onStart"
          @end="onEnd"
          tag="tbody"
          item-key="name"
        >
          <template #item="{ element, index }">
            <tr>
              <td
                class="move"
                v-for="(header, index) in headers"
                :key="header"
              >
                {{ element[header] }}
              </td>
            </tr>
          </template>
        </draggable>
      </table>
    </script>
    <script>
      const app = {
        //注册draggable组件
        components: {
          'itxst-component': {
            template: "#itxst",
            components: {
              "draggable": window.vuedraggable,
            },
            data() {
              return {
                //列的名称
                headers: ["id", "name", "intro"],
                //需要拖拽的数据,拖拽后数据的顺序也会变化
                list: [
                  { name: "www.itxst.com", id: 0, intro: "慢吞吞的蜗牛" },
                  { name: "www.baidu.com", id: 1, intro: "中文搜索引擎" },
                  { name: "www.google.com", id: 3, intro: "安卓操作系统" },
                ],
              }
            },
            methods: {
              //开始拖拽事件
              onStart() {},
              //结束拖拽事件
              onEnd() {}
            }
          }
        },
      }
      Vue.createApp(app).mount('#app')
    </script>
    <style>
      .title {
        padding: 3px;
        font-size: 13px;
      }
      .itxst {
        width: 600px;
      }
      .move {
        cursor: move;
      }
      table.tb {
        color: #333;
        border: solid 1px #999;
        font-size: 13px;
        border-collapse: collapse;
        min-width: 500px;
        user-select: none;
      }
      table.tb th {
        background: rgb(168 173 217);
        border-width: 1px;
        padding: 8px;
        border-style: solid;
        border-color: #999;
        text-align: left;
      }
      table.tb th:nth-of-type(1) {
        text-align: center;
      }
      table.tb td {
        background: #d6c8c8;
        border-width: 1px;
        padding: 8px;
        border-style: solid;
        border-color: #999;
      }
      table.tb td:nth-of-type(1) {
        text-align: center;
      }
    </style>
  </html>
3、可运行案例(与vue3联合使用,无序列表)
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8" />
      <title>vue.draggable、无序列表、vue3联合示例</title>
      <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
      <script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
      <script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
      <script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>
    </head>
    <body>
      <div id="app">
        <div class="itxst">
          <div class="group">
            <draggable 
              :list="modules.group1" group="group1"
              handle=".move" filter=".forbid" 
              @start="onStart" @end="onEnd" :move="onMove"
              ghost-class="ghost" chosen-class="chosenClass" animation="300"
              :force-fallback="true" :touch-start-threshold="50"  
              :fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
            >
              <template #item="{ element, index }">
                <div :class="element.disabledMove ? 'forbid item' : 'item'">
                  <label class="move">{{ element.name }}</label>
                  <p v-show="element.disabledPark" style="color:red">此处不允许拖拽和停靠</p>
                  <p v-show="!element.disabledPark">内容......</p>
                </div>
              </template>
            </draggable>
          </div>
          <div class="group">
            <draggable
              :list="modules.group2" group="group1"
              handle=".move" filter=".forbid" 
              @start="onStart" @end="onEnd" :move="onMove"
              ghost-class="ghost" chosen-class="chosenClass" animation="300"
              :force-fallback="true" :touch-start-threshold="50"  
              :fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
            >
              <template #item="{ element, index }">
                <div :class="element.disabledMove ? 'forbid item' : 'item'">
                  <label class="move">{{ element.name }}</label>
                  <p>内容....</p>
                </div>
              </template>
            </draggable>
          </div>
          <div class="group">
            <draggable
              :list="modules.group3" group="group1"
              handle=".move" filter=".forbid" 
              @start="onStart" @end="onEnd" :move="onMove"
              ghost-class="ghost" chosen-class="chosenClass" animation="300"
              :force-fallback="true" :touch-start-threshold="50"  
              :fallback-class="true" :fallback-on-body="true" :fallback-tolerance="50"
            >
              <template #item="{ element, index }">
                <div :class="element.disabledMove ? 'forbid item' : 'item'">
                  <label class="move">{{ element.name }}</label>
                  <p>内容....</p>
                </div>
              </template>
            </draggable>
          </div>
        </div>
        <div>
          <div>教程,https://www.itxst.com/vue-draggable-next/tutorial.html</div>
          <div>案例,https://debug.itxst.com/js/byamn2ja</div>
          <div>属性说明:</div>
          <div style="display: flex;">
            <pre>
              animation:拖动时过渡动画持续时间
              chosen-class:选中的样式
              disabled:是否禁用拖拽组件
              delay:鼠标按下多少秒之后可以拖拽元素
              drag-class:拖动元素的样式
              draggable:通过样式设置拖拽,:draggable=".item",样式类为item的元素才能被拖动
              fallback-class:克隆选中元素的样式到跟随鼠标的样式
              fallback-on-body:克隆的元素添加到文档的body中
              fallback-tolerance:按下鼠标移动多少个像素才能拖动元素
              filter:通过样式设置不拖拽,:filter=".unmover",设置了unmover样式的元素不允许拖动
              force-fallback:忽略HTML5的拖拽行为
              ghost-class:宿主样式
              group:相同的组名可以相互拖拽
              handle:通过样式设置拖拽,:handle=".mover",只有当鼠标在样式类为mover类的元素上才能触发拖动
            </pre>
            <pre>
              list:数据源
              scroll:有滚动区域是否允许拖拽
              scroll-fensitivity:距离滚动区域多远时,滚动滚动条
              scroll-fn:滚动回调函数
              scroll-speed:滚动速度
              sort:是否开启排序
              touch-start-threshold:鼠标按下移动多少px才能拖动元素交换间隔的大小,可以查看菜单对应的属性说明
              @start:开始拖拽事件
              :move:拖拽中事件,可以用来控制是否允许停靠
              @end:结束拖拽事件
              vue-draggable组件的disabled、filter、handle、draggable这几个属性,优先级从高到低的排序是什么 
              disabled > handle > draggable(也可配置在插槽中) > filter
            </pre>
          </div>
        </div>
      </div>
      <script>
        const app = {
          data() {
            return {
              modules: {
                group1: [
                  { name: "第1组-1", id: 1, disabledMove: true, disabledPark: true },
                  { name: "第1组-2", id: 2, disabledMove: false, disabledPark: false },
                  { name: "第1组-3", id: 3, disabledMove: false, disabledPark: false },
                ],
                group2: [
                  { name: "第2组-1", id: 5, disabledMove: false, disabledPark: false },
                  { name: "第2组-2", id: 6, disabledMove: false, disabledPark: false },
                  { name: "第2组-3", id: 7, disabledMove: false, disabledPark: false },
                ],
                group3: [
                  { name: "第3组-1", id: 8, disabledMove: false, disabledPark: false },
                  { name: "第3组-2", id: 9, disabledMove: false, disabledPark: false },
                ],
              },
            }
          },
          //注册draggable组件
          components: {
            'draggable': window.vuedraggable
          },
          methods: {
            //拖拽开始的事件
            onStart() {
              console.log("开始拖拽");
            },
            //拖拽结束的事件
            onEnd() {
              console.log("结束拖拽");
            },
            onMove(e) {
              //不允许停靠
              if (e.relatedContext.element.disabledPark == true) return false;
              return true;
            }
          }
        }
        Vue.createApp(app).mount('#app')
      </script>
      <style>
        body {
          padding: 0px;
          margin: 0px;
          background-color: #f1f1f1;
        }
        .itxst {
          background-color: #f1f1f1;
          display: flex;
          justify-content: space-between;
          align-content: space-around;
          padding: 20px;
        }
        .group {
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
          align-content: center;
          width: 32%;
        }
        .item {
          border: solid 1px #ddd;
          padding: 0px;
          text-align: left;
          background-color: #fff;
          margin-bottom: 10px;
          display: flex;
          flex-direction: column;
          height: 100px;
          user-select: none;
        }
        .item>label {
          border-bottom: solid 1px #ddd;
          padding: 6px 10px;
          color: #333;
        }
        .item>label:hover {
          cursor: move;
        }
        .item>p {
          padding: 0 10px;
          color: #666;
        }
        .ghost {
          border: solid 1px rgb(19, 41, 239) !important;
        }
        .chosenClass {
          opacity: 1;
          border: solid 1px red;
        }
        .fallbackClass {
          background-color: aquamarine;
        }
      </style>
    </body>
  </html>
  
九、vue从虚拟DOM到真实DOM的过程!!!
附、说明:
  A、虚拟DOM是用JS对象模拟真实DOM结构的内存数据,它会占用浏览器的内存资源,且其Diff算法会消耗CPU资源
  B、但相比频繁操作真实DOM(触发大量重排重绘),虚拟DOM能显著降低浏览器的整体性能开销
  C、Slate.js,是编辑器里的虚拟DOM
1、定义虚拟DOM节点构造函数
  class VNode {
    constructor(tag, data = {}, children = [], text = null) {
      this.tag = tag; //标签名
      this.data = data; //属性数据(class、style、事件等)
      this.children = children; //子节点
      this.text = text; //文本内容
      this.key = data.key; //用于Diff的key
    }
  }
2、创建虚拟DOM树(设计图)
  const vdomTree = new VNode(
    'div',
    { id: 'app', class: 'container' },
    [
      new VNode('h1', { class: 'title' }, null, '虚拟DOM转真实DOM示例'),
      new VNode(
        'ul',
        { style: { 'list-style': 'none' } },  //Vue规范:style必须是对象
        [
          new VNode('li', { key: 1, class: 'item' }, null, '第一项'),
          new VNode('li', { key: 2, class: 'item' }, null, '第二项'),
          new VNode(
            'li',
            { key: 3 },
            [new VNode('span', { style: { color: 'blue' } }, null, '带子元素的项')]
          )
        ]
      ),
      new VNode(
        'button',
        { 
          class: 'btn',
          on: { click: () => alert('按钮被点击') }  //事件处理
        },
        null,
        '点击我'
      )
    ]
  );
  console.log(vdomTree);的执行结果如下
  { 
    tag: 'div',
    data: {
      id: 'app',
      class: 'container',
      key: undefined  //未指定key时为undefined
    },
    children: [
      //第一个子节点:h1标题
      {
        tag: 'h1',
        data: {
          class: 'title',
          key: undefined
        },
        children: null,
        text: '虚拟DOM转真实DOM示例',
        key: undefined
      },
      //第二个子节点:ul列表
      {
        tag: 'ul',
        data: {
          style:  { 'list-style': 'none' },
          key: undefined
        },
        children: [
          //ul的第一个子节点:li
          {
            tag: 'li',
            data: {
              key: 1,
              class: 'item'
            },
            children: null,
            text: '第一项',
            key: 1
          },
          //ul的第二个子节点:li
          {
            tag: 'li',
            data: {
              key: 2,
              class: 'item'
            },
            children: null,
            text: '第二项',
            key: 2
          },
          //ul的第三个子节点:包含span的li
          {
            tag: 'li',
            data: {
              key: 3
            },
            children: [
              //li的子节点:span
              {
                tag: 'span',
                data: {
                  style: { color: 'blue' },
                  key: undefined
                },
                children: null,
                text: '带子元素的项',
                key: undefined
              }
            ],
            text: null,
            key: 3
          }
        ],
        text: null,
        key: undefined
      },
      //第三个子节点:button按钮
      {
        tag: 'button',
        data: {
          class: 'btn',
          on: {
            click: () => alert('按钮被点击')  //绑定的点击事件
          },
          key: undefined
        },
        children: null,
        text: '点击我',
        key: undefined
      }
    ],
    text: null,
    key: undefined
  }
3、实现虚拟DOM转真实DOM的函数(按图施工)
  function createElement(vnode) {
    //处理文本节点
    if (vnode.text !== null) {
      return document.createTextNode(vnode.text);
    }
    //创建元素节点
    const el = document.createElement(vnode.tag);
    //处理属性
    if (vnode.data) {
      Object.keys(vnode.data).forEach(key => {
        if (key === 'class') {
          el.className = vnode.data.class;
        } 
        else if (key === 'style') {
          Object.assign(el.style, vnode.data.style);  //style必须是对象
        }
        else if (key === 'on' && vnode.data.on) {
          //绑定事件
          Object.keys(vnode.data.on).forEach(event => {
            el.addEventListener(event, vnode.data.on[event]);
          });
        }
        else if (key !== 'key') {  //key不渲染到真实DOM
          el.setAttribute(key, vnode.data[key]);
        }
      });
    }
    //递归处理子节点
    if (vnode.children && vnode.children.length) {
      vnode.children.forEach(child => {
        el.appendChild(createElement(child));
      });
    }
    return el;
  }
4、转换为真实DOM并插入到页面
  //转换为真实DOM
  const realDOM = createElement(vdomTree);
  //插入到页面
  document.body.appendChild(realDOM);
  console.log('虚拟DOM转换完成,已插入页面');
5、最终渲染结果
  <div id="app" class="container">
    <h1 class="title">虚拟DOM转真实DOM示例</h1>
    <ul style="list-style: none;">
      <li class="item">第一项</li>
      <li class="item">第二项</li>
      <li>
        <span style="color: blue;">带子元素的项</span>
      </li>
    </ul>
    <button class="btn">点击我</button>
  </div>
  
十、自定义组件
 注、在Vue2、Vue3的单文件组件(SFC)模板中,支持驼峰命名和短横线分隔命名,
  A、import CctvEllipsis from '../base/cctv-ellipsis.vue',
  B、<CctvEllipsis />,
  C、<cctv-ellipsis />
1、自定义组件-图标svg-icon,来源,ai-web
  (1)配置
    A、package.json,引入依赖
      //npm i vite-plugin-svg-icons -D
      {
        "devDependencies": {
          "vite-plugin-svg-icons": "^2.0.1",
        }
      }
    B、vite.config.js,使用插件
      import createVitePlugins from './vite/plugins'
      export default defineConfig(
        ({command,mode})=>{
          plugins: [
            createVitePlugins(),
          ],
        }
      )
    C、createVitePlugins,存入插件
      import vue from '@vitejs/plugin-vue'
      import createSvgIcon from './svg-icon'
      export default function createVitePlugins(viteEnv, isBuild = false) {
        const vitePlugins = [vue()]//插件的存储容器
        vitePlugins.push(createSvgIcon(isBuild))
        return vitePlugins
      }
    D、createSvgIcon,生成插件
      import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
      import path from 'path'
      export default function createSvgIcon(isBuild) {
        return createSvgIconsPlugin({
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
          symbolId: 'icon-[dir]-[name]',//与iconName关联,注意iconName的前缀#
          svgoOptions: isBuild
        })
      }
    附、B,C,D步骤可以简化为本步骤
      来源,https://blog.csdn.net/weixin_53731501/article/details/125478380 
      //vite.config.js中配置,使用vite-plugin-svg-icons插件显示本地svg图标
      import path from 'path'
      import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'
      export default defineConfig((command) => {
        return {
          plugins: [
            createSvgIconsPlugin({
              iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// 指定要缓存的文件夹
              symbolId: '[name]'// 指定symbolId格式
            })
          ],
        }
      })
  (2)定义
    <template>
      <svg :class="svgClass">
        <use :xlink:href="iconName" :fill="color" />
      </svg>
    </template>
    <script>
      export default defineComponent({
        props: {
          nameIcon: {
            type: String,
            required: true
          },
          className: {
            type: String,
            default: ''
          },
          color: {
            type: String,
            default: ''
          },
        },
        setup(props) {
          return {
            iconName: computed(() => `#icon-${props.nameIcon}`),
            svgClass: computed(() => {
              if (props.className) {
                return `svg-icon ${props.className}`
              }
              return 'svg-icon'
            })
          }
        }
      })
    </script>
  (3)注入
    import { createApp } from 'vue'
    import App from './App.vue'
    import SvgIcon from '@/components/SvgIcon'
    const app = createApp(App)
    app.component('svg-icon', SvgIcon)//全局引入使用
    app.mount('#app')
  (4)使用,
    <svg-icon name-icon="clock" />,clock指向clock.svg
2、自定义组件-月季度半年全年,来源:bcbf-web
  (1)定义组件,many-date
    <script setup>
      import { 
        DArrowLeft,
        DArrowRight,
        Calendar,
      } from '@element-plus/icons-vue'
      const props = defineProps({
        showPeriod: ''
      });
      const emit = defineEmits(['dateOk'])
      const period = ref(''); //统计周期
      var date = new Date;
      var initMonth = date.getMonth();
      var initYear = date.getFullYear();
      var clickYear = ref(initYear);//被使用的年份
      var initItem = ref('');//当前所在的季节或半年
      var clickItem = ref('');//默认的或被点击的季节或半年
      var twoRef = ref('');
      var fourRef = ref('');
      var dateRange = ref('');
      var isShowTwo = ref(false);
      var isShowFour = ref(false);
      // 以下处理统计周期改变
      watch(() => props.showPeriod, (newValue, oldValue) => {
        if (newValue !== oldValue) {
          period.value = newValue;
          changePeriod(period.value)
          getInitItem()
          if (period.value === '月'|| period.value === '全年') {
            changeInput()
          }
        }
      }, { immediate: true, deep: true })
      function changePeriod(period) {
        if(period === '月'){
          if (initMonth == 0) {//如果现在处在今年一月,那么默认为去年十二月
            initMonth = 12;
            initYear--;
          }
          dateRange.value = [
            initYear + '-' + String(1).padStart(2, '0'), 
            initYear + '-' + String(initMonth).padStart(2, '0')
          ];//initMonth可以用于表示上个月
        }else if(period === '全年') {
          initYear--;
          dateRange.value = initYear.toString();
        }else if(period === '季度') {
          var four = {
            3: 'four',
            6: 'one',
            9: 'two',
            12: 'three',
          };
          for (var key in four) {
            if (initMonth < key) {
              if (key == 3) initYear--;//如果现在处在今年第一季度,那么默认为去年第四季度
              clickFourAndTwo(four[key]);
              break;
            }
          }
        }else if(period === '半年') {
          var two = {
            6: 'down',
            12: 'up',
          }
          for (var key in two) {
            if (initMonth < key) {
              if (key == 6) initYear--;//如果现在处在今年上半年,那么默认为去年下半年
              clickFourAndTwo(two[key]);
              break;
            }
          }
        }
      }
      function getInitItem() {//获取当前所在的季节或半年
        var type = clickItem.value;
        var four = ['one','two','three','four','one'];
        var two = ['up','down','up'];
        if(four.indexOf(type) > -1){
          initItem.value = four[four.indexOf(type) + 1]
        }
        if(two.indexOf(type) > -1){
          initItem.value = two[two.indexOf(type) + 1]
        }
      }
      function changeInput() {
        emit('dateOk', dateRange.value)
      }
      // 以下处理季度和半年
      function focusInput() {
        clickYear.value = initYear;
        if( period.value == '半年' ){
          isShowTwo.value = true;
          isShowFour.value = false;
          setTimeout(function(){
            twoRef.value.focus();
          }, 150);
        }
        if( period.value == '季度' ){
          isShowTwo.value = false;
          isShowFour.value = true;
          setTimeout(function(){
            fourRef.value.focus();
          }, 150);
        }
      }
      function clickArrow(type) {
        if(type === 'left') clickYear.value--;
        if(type === 'right') clickYear.value++;
      }
      function getBlur() {
        isShowTwo.value = false;
        isShowFour.value = false;
      }
      function clickFourAndTwo(type) {
        clickItem.value = type;
        var obj = {
          one: {
            start: clickYear.value + '-01',
            end: clickYear.value + '-03'
          },
          two: {
            start: clickYear.value + '-04',
            end: clickYear.value + '-06'
          },
          three: {
            start: clickYear.value + '-07',
            end: clickYear.value + '-09'
          },
          four: {
            start: clickYear.value + '-10',
            end: clickYear.value + '-12'
          },
          up: {
            start: clickYear.value + '-01',
            end: clickYear.value + '-06'
          },
          down: {
            start: clickYear.value + '-07',
            end: clickYear.value + '-12'
          },
        };
        var start = obj[type].start;
        var end = obj[type].end;
        var temp = addSpace(6) + start + addSpace(8) + '-' + addSpace(8) + end;
        isShowTwo.value = false;
        isShowFour.value = false;
        dateRange.value = temp; 
        emit('dateOk', [start, end])
      }
      function addSpace(num) { //添加空格
        var str = '\xa0';
        for(var i = 0; i < num; i++) str += '\xa0'
        return str;
      }
    </script>
    <template>
      <div style="width: 100%;">
        <!-- 以下调用已有组件,实现'月'和'全年'的功能 -->
        <el-date-picker 
          v-if="period === '月'" 
          v-model="dateRange" 
          type="monthrange" 
          :editable="false" 
          start-placeholder="开始日期"
          end-placeholder="结束日期" 
          style="width: 100%;" 
          format="YYYY-MM" 
          value-format="YYYY-MM"  
          @change="changeInput()" 
        />
        <el-date-picker 
          v-if="period === '全年'" 
          v-model="dateRange" 
          type="year" 
          :editable="false" 
          start-placeholder="开始日期"
          end-placeholder="结束日期" 
          style="width: 100%;" 
          format="YYYY" 
          value-format="YYYY"  
          @change="changeInput()" 
        />
        <!-- 以下加工已有组件,实现'季度'和'半年'的功能 -->
        <el-input 
          v-model="dateRange" 
          :disabled="isDisabled" 
          :readonly="true" 
          @focus="focusInput()" 
          v-if="period === '季度'|| period === '半年'"
        >
          <template #prefix>
            <el-icon><Calendar /></el-icon>
          </template>
        </el-input>
        <div class="many-date" tabindex="-1" v-if="isShowFour"  ref="fourRef" @blur="getBlur">
          <div class="many-date-up">
            <el-icon class="many-date-up-content" @click="clickArrow('left')" ><DArrowLeft /></el-icon>
            <div class="many-date-up-content" >{{ clickYear }}</div>
            <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon>
          </div>
          <el-divider class="many-date-divider"/>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickFourAndTwo('one')" 
              :class="[
                (initYear==clickYear-1 && initItem=='one')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='one')?'many-date-down-content-bgClick':null,
              ]"
            >第一季度</div>
            <div class="many-date-down-content" @click="clickFourAndTwo('two')" 
              :class="[
                (initYear==clickYear && initItem=='two')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='two')?'many-date-down-content-bgClick':null,
              ]"
            >第二季度</div>
          </div>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickFourAndTwo('three')" 
              :class="[
                (initYear==clickYear && initItem=='three')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='three')?'many-date-down-content-bgClick':null,
              ]"
            >第三季度</div>
            <div class="many-date-down-content" @click="clickFourAndTwo('four')" 
              :class="[
                (initYear==clickYear && initItem=='four')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='four')?'many-date-down-content-bgClick':null,
              ]"
            >第四季度</div>
          </div>
        </div>
        <div class="many-date" tabindex="-1" v-if="isShowTwo"  ref="twoRef" @blur="getBlur">
          <div class="many-date-up">
            <el-icon class="many-date-up-content" @click="clickArrow('left')"><DArrowLeft /></el-icon>
            <div class="many-date-up-content" >{{ clickYear }}</div>
            <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon>
          </div>
          <el-divider class="many-date-divider"/>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickFourAndTwo('up')"  
              :class="[
                (initYear==clickYear-1 && initItem=='up')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='up')?'many-date-down-content-bgClick':null,
              ]"
            >上半年</div>
            <div class="many-date-down-content" @click="clickFourAndTwo('down')"  
              :class="[
                (initYear==clickYear && initItem=='down')?'many-date-down-content-bgNow':null,
                (initYear==clickYear && clickItem=='down')?'many-date-down-content-bgClick':null,
              ]"
            >下半年</div>
          </div>
        </div>
      </div>
    </template>
    <style lang="scss">
      .many-date{
        width: 60%; 
        margin-left: 20%;
        border-radius: 4px;
        margin-top: 10px;
        position: absolute;
        z-index: 100;
        background: var(--el-bg-color-overlay);
        box-shadow: var(--el-box-shadow-light);
        &-up {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 10px 10% ;
          &-content {
            cursor: pointer;
            user-select: none;
            color: var(--el-text-color-regular);
          }
          &-content:hover {
            color: var(--el-color-primary);
          }
        }
        &-divider {
          margin: 0 10%; 
          width: 80%
        }
        &-down {
          display: flex; 
          justify-content: space-around;
          align-items: center;
          padding: 10px 10% ;
          &-content {
            padding: 4px 20px ;
            cursor: pointer;
            user-select: none;
            color: var(--el-text-color-regular);
            &-bgClick {
              background: #2162db;
              border-radius: 18px;
              color: #ffffff !important;
              font-weight: bolder;
            }
            &-bgNow {
              color: #2162db;
              font-weight: bolder;
            }
          }
          &-content:hover {
            color: var(--el-color-primary);
          }
        }
      }
    </style>
  (2)使用组件!!!
    <script setup>
      const ruleShowForm = ref(false)
      const operateText = ref('')
      const ruleVersionForm = reactive({
        period: '',
        myDate: [],
      })
      const rules = {
        period: [{ required: true, trigger: "blur", message: "统计周期不能为空" }],
        myDate: [{ required: true, trigger: "blur", message: "日期范围不能为空" }],
      };
      const formRef = ref(null)
      const showDialogRule = () => {
        ruleShowForm.value = true;
        operateText.value = '创建新版本'
        formRef.value.clearValidate(); 
      }
      const createOrEdit = () => {
        formRef.value.validate((valid) => {
          if (valid) {
            var data = {
              period: ruleVersionForm.period,
              myDate: ruleVersionForm.myDate,
            }
            updateVersion(data).then((res) => {
              ruleShowForm.value = false;
              getList();
            }).catch(()=>{
              ruleShowForm.value = false;
              ElMessage("出错了");
            });
          } else {
            ElMessage.error('表单验证失败');
          }
        });
      }
      function handleDateOk(data) {
        console.log( data );
        ruleVersionForm.myDate = data
        formRef.value.validateField('myDate');
      }
      function clearSelect(key) {
        formRef.value.clearValidate(key);
      }
      function changePeriod() {
        formRef.value.clearValidate('myDate');
      }
      function validateSelect(key) {
        formRef.value.validateField(key);
      }
    </script>
    <template>
      <div class="content-table">
        <div class="content-table-header" style="padding-left: 0;">
            <el-button type="primary" @click="showDialogRule()">创建新版本</el-button>
        </div>
        <el-dialog v-model="ruleShowForm" :title="operateText"  width="600">
          <el-form :model="ruleVersionForm" class="rebut-form" ref="formRef" :rules="rules" label-width="80px">
            <el-form-item label="统计周期" prop="period">
              <el-select 
                v-model="ruleVersionForm.period" 
                placeholder="请选择统计周期" 
                @focus="clearSelect('period')" 
                @change="changePeriod"
                @blur="validateSelect('period')"
              >
                <el-option label="月" value="月" />
                <el-option label="季度" value="季度" />
                <el-option label="半年" value="半年" />
                <el-option label="全年" value="全年" />
              </el-select>
            </el-form-item>
            <el-form-item 
              label="日期范围" 
              prop="myDate" 
              v-if="ruleVersionForm.period"
            >
              <many-date :showPeriod="ruleVersionForm.period" @dateOk="handleDateOk" />
            </el-form-item>
          </el-form>
          <template #footer>
            <div class="dialog-footer">
              <el-button @click="ruleShowForm = false">取消</el-button>
              <el-button type="primary" @click="createOrEdit">确定</el-button>
            </div>
          </template>
        </el-dialog>
      </div>
    </template>
3、自定义组件-图片水印组件Water1mark,改编自:bcbf-web!!!
  附、初始逻辑,监听props.url变化,以默认配置为参数
    A、new Image并监听其下载事件,给该实例的url赋值。在下载事件里
    B、给canvas标签设置宽高(会清空画布)
    C、用标签的drawImage方法把实例绘制成图片,用标签的各种方法绘制文字
  附、更新逻辑,监听配置变化,以更新配置为参数
    A、给canvas标签设置宽高(会清空画布)
    B、用标签的drawImage方法把实例绘制成图片,用标签的各种方法绘制文字
  附、为什么vue3的有些逻辑写在watch里,而不写在updated里
    A、逻辑写在updated里,watch数据没有改变时,逻辑也会执行
  (1)定义组件
    <template>
      <div class="image-watermark-container">
        <div class="image-section">
          <div v-if="!imageData" class="empty-state">
            <el-empty description="请上传图片" />
          </div>
          <div v-else class="image-preview">
            <div class="thumbnail-item" :class="{ 
              watermarked: hasWatermark
            }">
              <!-- isPreview为true时:固定尺寸容器 -->
              <div v-if="props.isPreview" class="preview-fixed-container">
                <img 
                  :src="getThumbnailUrl()" 
                  :alt="`图片预览`" 
                  class="preview-fixed-img"
                />
                <div class="preview-btn top-right">
                  <el-button 
                    size="small" 
                    type="text" 
                    @click.stop="openPreviewDialog()"
                  >
                    预览
                  </el-button>
                </div>
              </div>
              <!-- isPreview为false时:原始大小容器 -->
              <div v-else class="original-size-container">
                <img 
                  :src="getThumbnailUrl()" 
                  :alt="`图片预览`" 
                  class="original-size-img"
                />
              </div>
            </div>
          </div>
        </div>
        <!-- 预览大图弹窗 -->
        <el-dialog
          v-model="previewDialogVisible"
          width="35%"
          top="5vh"
          :show-close="false"
          :close-on-click-modal="true"
          @close="handlePreviewDialogClose"
        >
          <div class="preview-dialog-content">
            <div class="large-preview-container">
              <div v-if="showPreviewLoading" class="loading-container">
                <el-icon class="loading-icon"><Loading /></el-icon>
                <span class="loading-text">大图加载中...</span>
              </div>
              <img
                v-else
                :src="currentPreviewImage"
                alt="预览大图"
                class="large-preview-image"
              />
            </div>
          </div>
          <template #title>
              <div class="position-fixed">
                  <div class="padding">{{ props.title || '图片预览' }}</div>
                  <div> 
                    <el-button 
                      type="primary" 
                      @click="downloadPreviewImage"
                      :loading="previewDownloading"
                    >下载</el-button>
                    <el-button @click="previewDialogVisible = false">关闭</el-button>
                  </div>
              </div>
          </template>
        </el-dialog>
        <!-- 添加水印弹窗 -->
        <el-dialog
          v-model="watermarkDialogVisible"
          title="添加水印"
          width="1000px"
          :close-on-click-modal="false"
        >
          <!-- 水印设置 -->
          <div class="watermark-dialog-content">
            <!-- 预览区域 -->
            <div class="preview-panel">
              <div class="preview-container">
                <div v-if="showWatermarkLoading" class="loading-container">
                  <el-icon class="loading-icon"><Loading /></el-icon>
                  <span class="loading-text">图片加载中...</span>
                </div>
                <img
                  v-else
                  :src="currentWatermarkImage"
                  alt="预览图"
                  class="preview-image"
                />
              </div>
            </div>
            <!-- 设置区域 -->
            <div class="settings-panel">
              <el-form :model="usedWatermarkSettings" label-width="100px" label-position="top">
                <el-form-item label="水印信息">
                  <el-input
                    v-model="usedWatermarkSettings.text"
                    placeholder="请输入水印信息"
                    type="textarea"
                    :rows="4"
                    :maxlength="50"
                    show-word-limit 
                  />
                </el-form-item>
                <el-form-item label="颜色">
                  <el-color-picker v-model="usedWatermarkSettings.color" show-alpha />
                </el-form-item>
                <el-form-item label="字号">
                  <el-slider v-model="usedWatermarkSettings.fontSize" :min="5" :max="120" />
                </el-form-item>
                <el-form-item label="旋转">
                  <el-slider v-model="usedWatermarkSettings.rotate" :min="-180" :max="180" />
                </el-form-item>
                <el-form-item label="行距">
                  <el-slider v-model="usedWatermarkSettings.lineSpace" :min="1" :max="5" :step="0.1"  />
                </el-form-item>
                <el-form-item label="偏移">
                  <el-space>
                    <el-input-number v-model="usedWatermarkSettings.left" placeholder="左偏移" controls-position="right" />
                    <el-input-number v-model="usedWatermarkSettings.top" placeholder="上偏移" controls-position="right" />
                  </el-space>
                </el-form-item>
              </el-form>
              <div class="apply-btn">
                <el-button
                  type="primary"
                  @click="handleSave"
                  :loading="loadingUp"
                >确定</el-button> 
              </div>
            </div>
          </div>
        </el-dialog>
      </div>
    </template>
    <script setup>
      import { ref, reactive, computed, onMounted, watch, onUnmounted, nextTick } from 'vue'
      import { ElMessage } from 'element-plus'
      import { Loading } from '@element-plus/icons-vue'
      import useUserStore from '@/store/modules/user';
      /* ==================== 1、响应式数据定义 ==================== */
      const user = useUserStore().userInfo
      const watermarkText = user.realName + ' ' + user.userName
      // 定义属性
      const props = defineProps({ 
        url: {
          type: String,
          required: true
        },
        name: {
          type: String,
          default: ''
        },
        title: {
          type: String,
          default: ''
        },
        isPreview: {
          type: Boolean,
          default: true
        },
        isText: {
          type: Boolean,
          default: true
        },
        settingObj: {
          type: Object,
          default: () => ({})
        }
      })
      // 事件定义
      const emit = defineEmits(['watermarkUpdate'])
      // 状态变量
      const imageData = ref(null)
      const originalImage = ref(null)
      const watermarkedCanvas = ref(null)
      const hasWatermark = ref(false)
      const previewDialogVisible = ref(false)
      const watermarkDialogVisible = ref(false)
      const largePreviewLoading = ref(false)
      const imgLoadingForWatermark = ref(false)
      const previewDownloading = ref(false)
      const loadingUp = ref(false)
      const allPreviewCanvases = ref(new Map())
      // 计算属性:默认水印配置
      const defaultWatermarkSettings = computed(() => ({
        text: props.isText ? watermarkText : (props.settingObj.text || ''),
        color: props.settingObj.color || '#999999',
        fontSize: props.settingObj.fontSize || 30,
        rotate: props.settingObj.rotate || -30,
        lineSpace: props.settingObj.lineSpace || 1,
        left: props.settingObj.left || 0,
        top: props.settingObj.top || 0
      }))
      // 当前使用的水印配置
      const usedWatermarkSettings = reactive({ ...defaultWatermarkSettings.value })
      // 参数初始化与恢复初始化!!!:Object.assign(usedWatermarkSettings, defaultWatermarkSettings.value)
      /* ==================== 2、工具函数 ==================== */
      // 创建图片元素的Promise封装
      const createImageElement = (src) => {
        return new Promise((resolve, reject) => {
          const img = new Image()
          img.crossOrigin = 'anonymous'
          img.onload = () => resolve(img)
          img.onerror = () => reject(new Error('图片加载失败'))
          img.src = src
        })
      }
      // 创建Canvas元素
      const createCanvasFromImage = (img, scale = 1) => {
        const canvas = document.createElement('canvas')
        canvas.width = img.width * scale
        canvas.height = img.height * scale
        const context = canvas.getContext('2d')
        context.drawImage(img, 0, 0, canvas.width, canvas.height)
        return canvas
      }
      // 重置图片状态
      const resetImageState = () => {
        imageData.value = null
        originalImage.value = null
        watermarkedCanvas.value = null
        hasWatermark.value = false
        allPreviewCanvases.value.clear()
      }
      // 给当前canvas添加水印
      const addWatermarkToCanvas = (context, canvasWidth, canvasHeight) => {
        const { text, color, fontSize, rotate, lineSpace, left, top } = usedWatermarkSettings
        if (!text || text.trim() === '') return
        // 设置水印样式
        context.fillStyle = color
        context.font = `${fontSize}px Arial`
        context.textAlign = 'left'
        context.textBaseline = 'middle'
        // 处理换行文本
        const lines = text.split('\n').filter(line => line.trim() !== '')
        if (lines.length === 0) return
        // 计算每行文本的宽度并找出最长的一行
        let maxLineWidth = 0
        lines.forEach(line => {
          const lineWidth = context.measureText(line.trim()).width
          if (lineWidth > maxLineWidth) {
            maxLineWidth = lineWidth
          }
        })
        // 计算单行文本高度
        const textHeight = fontSize * lineSpace
        const lineSpacing = 300
        // 保存当前状态
        context.save() 
        // 应用偏移和旋转
        context.translate(canvasWidth / 2 + left, canvasHeight / 2 + top)
        context.rotate(rotate * Math.PI / 180)
        // 绘制网格水印
        const horizontalGap = maxLineWidth * 1.5
        for (let x = -canvasWidth; x < canvasWidth * 2; x += horizontalGap) {
          for (let y = -canvasHeight; y < canvasHeight * 2; y += lineSpacing) {
            lines.forEach((line, index) => {
              const yOffset = y + index * textHeight
              context.fillText(line.trim(), x, yOffset)
            })
          }
        }
        // 恢复状态
        context.restore()
      }
      // 更新图片显示
      const updateImageDisplay = () => {
        if (imageData.value) {
          imageData.value = { ...imageData.value }
        }
      }
      /* ==================== 3、核心业务函数 ==================== */
      // 加载图片
      const loadImage = async () => {
        if (!props.url) return
        try {
          resetImageState()
          // 加载图片
          const img = await createImageElement(props.url)
          // 创建原始图片canvas
          const canvas = createCanvasFromImage(img)
          // 存储原始图片数据
          originalImage.value = canvas
          imageData.value = {
            width: img.width,
            height: img.height,
            dataUrl: canvas.toDataURL()
          }
          // 如果默认有水印文字,为图片添加水印
          if (usedWatermarkSettings.text?.trim()) {
            await addWatermarkToCurrentImage(canvas)
          }
          // 如果是非预览模式,预加载全尺寸预览
          if (!props.isPreview) {
            allPreviewCanvases.value.set('preview', canvas)
          }
        } catch (error) {
          console.error('图片加载失败:', error)
          ElMessage.error('图片加载失败')
        }
      }
      // 为图片添加水印
      const addWatermarkToCurrentImage = async (sourceCanvas = originalImage.value) => {
        const text = usedWatermarkSettings.text?.trim()
        if (!text) return
        if (!sourceCanvas) {
          console.error('原始图片不存在')
          return
        }
        try {
          const newCanvas = document.createElement('canvas')
          newCanvas.width = sourceCanvas.width
          newCanvas.height = sourceCanvas.height
          const context = newCanvas.getContext('2d')
          // 绘制原始内容
          context.drawImage(sourceCanvas, 0, 0)
          // 添加水印
          addWatermarkToCanvas(context, newCanvas.width, newCanvas.height)
          // 更新状态
          watermarkedCanvas.value = newCanvas
          hasWatermark.value = true
          originalImage.value = newCanvas
          allPreviewCanvases.value.set('preview', newCanvas)
        } catch (error) {
          console.error('添加水印失败:', error)
          ElMessage.error('添加水印失败')
        }
      }
      // 生成高质量图片(用于水印编辑)
      const generateHighQualityImage = async () => {
        if (!props.url) return null
        try {
          const img = await createImageElement(props.url)
          return createCanvasFromImage(img)
        } catch (error) {
          console.error('生成高质量图片失败:', error)
          return null
        }
      }
      /* ==================== 4、预览相关函数 ==================== */
      // 处理弹窗关闭
      const handlePreviewDialogClose = () => {
        largePreviewLoading.value = false
      }
      // 打开预览大图弹窗
      const openPreviewDialog = async () => {
        try {
          previewDialogVisible.value = true
          await nextTick()
          await generateLargePreview()
        } catch (error) {
          console.error('打开预览弹窗失败:', error)
          ElMessage.error('打开预览失败:' + error.message)
          previewDialogVisible.value = false
        }
      }
      // 生成预览大图
      const generateLargePreview = async () => {
        if (!props.url) {
          largePreviewLoading.value = false
          ElMessage.error('图片URL为空')
          return
        }
        if (allPreviewCanvases.value.has('preview')) {
          return true
        }
        try {
          largePreviewLoading.value = true
          const img = await createImageElement(props.url)
          const canvas = createCanvasFromImage(img)
          // 如果有水印文字并且该图片已经有水印,才添加水印
          if (usedWatermarkSettings.text?.trim() && hasWatermark.value) {
            addWatermarkToCanvas(canvas.getContext('2d'), canvas.width, canvas.height)
          }
          allPreviewCanvases.value.set('preview', canvas)
          return true
        } catch (error) {
          console.error('生成预览大图失败:', error)
          ElMessage.error('预览大图生成失败:' + error.message)
          return false
        } finally {
          largePreviewLoading.value = false
        }
      }
      // 获取图片URL
      const getThumbnailUrl = () => {
        if (!imageData.value) return ''
        if (hasWatermark.value && watermarkedCanvas.value) {
          return watermarkedCanvas.value.toDataURL()
        }
        if (originalImage.value) {
          return originalImage.value.toDataURL()
        }
        return imageData.value.dataUrl
      }
      // 下载预览图片
      const downloadPreviewImage = async () => {
        const canvas = allPreviewCanvases.value.get('preview')
        if (!canvas) {
          ElMessage.warning('暂无预览图片可下载')
          return
        }
        try {
          previewDownloading.value = true
          const timestamp = new Date().getTime()
          const fileName = `${props.name || 'image'}_${timestamp}.png`
          const link = document.createElement('a')
          link.download = fileName
          link.href = canvas.toDataURL('image/png')
          link.style.display = 'none'
          document.body.appendChild(link)
          link.click()
          setTimeout(() => {
            document.body.removeChild(link)
            URL.revokeObjectURL(link.href)
          }, 100)
          ElMessage.success('图片下载成功')
        } catch (error) {
          console.error('下载图片失败:', error)
          ElMessage.error(`下载失败: ${error.message}`)
        } finally {
          previewDownloading.value = false
        }
      }
      /* ==================== 5、水印弹窗相关函数 ==================== */
      // 打开添加水印弹窗
      const openWatermarkDialog = async () => {
        watermarkDialogVisible.value = true
        Object.assign(usedWatermarkSettings, defaultWatermarkSettings.value)
        await getImgForAddWatermark()
      }
      // 获取图片为添加水印做准备
      const getImgForAddWatermark = async () => {
        if (!props.url) return
        try {
          imgLoadingForWatermark.value = true
          // 如果图片原本高清就跳过,否则就重新加载
          if (originalImage.value?.width > 800) {
            imgLoadingForWatermark.value = false
            return
          }
          const img = await createImageElement(props.url)
          const canvas = createCanvasFromImage(img)
          // 如果该图片已有水印,先添加水印
          if (hasWatermark.value && watermarkedCanvas.value) {
            originalImage.value = watermarkedCanvas.value
          } else {
            originalImage.value = canvas
          }
        } catch (error) {
          console.error('生成高质量预览图失败:', error)
        } finally {
          imgLoadingForWatermark.value = false
        }
      }
      // 将水印应用到当前图片
      const applyWatermarkToCurrentImage = async () => {
        try {
          // 清除旧水印
          hasWatermark.value = false
          watermarkedCanvas.value = null
          allPreviewCanvases.value.delete('preview')
          const currCanvas = await generateHighQualityImage()
          if (!currCanvas) return
          if (!usedWatermarkSettings.text?.trim()) {
            originalImage.value = currCanvas
            updateImageDisplay()
            return
          }
          await addWatermarkToCurrentImage(currCanvas)
          updateImageDisplay()
        } catch (error) {
          console.error('应用水印失败:', error)
          ElMessage.error('水印应用失败')
        }
      }
      /* ==================== 6、保存和导出函数 ==================== */
      const handleSave = async () => {
        try {
          loadingUp.value = true
          const currCanvas = await generateHighQualityImage()
          if (!currCanvas) {
            throw new Error('无法加载原始图片')
          }
          // 清除旧水印
          hasWatermark.value = false
          watermarkedCanvas.value = null
          if (!usedWatermarkSettings.text?.trim()) {
            originalImage.value = currCanvas
            updateImageDisplay()
            emit('watermarkUpdate', {
              success: true,
              dataUrl: currCanvas.toDataURL('image/png'),
              hasWatermark: false,
              watermarkSettings: { ...usedWatermarkSettings }
            })
          } else {
            await addWatermarkToCurrentImage(currCanvas)
            emit('watermarkUpdate', {
              success: true,
              dataUrl: watermarkedCanvas.value.toDataURL('image/png'),
              hasWatermark: true,
              watermarkSettings: { ...usedWatermarkSettings }
            })
          }
          ElMessage.success('水印添加成功')
        } catch (error) {
          console.error('添加水印失败:', error)
          const errorResult = {
            success: false,
            error: error.message,
            watermarkSettings: { ...usedWatermarkSettings }
          }
          emit('watermarkUpdate', errorResult)
          ElMessage.error('水印添加失败:' + error.message)
        } finally {
          loadingUp.value = false
          watermarkDialogVisible.value = false
        }
      }
      // 获取带水印的图片数据
      const getWatermarkedImageData = () => {
        if (!hasWatermark.value || !watermarkedCanvas.value) {
          return null
        }
        return {
          dataUrl: watermarkedCanvas.value.toDataURL('image/png'),
          width: watermarkedCanvas.value.width,
          height: watermarkedCanvas.value.height,
          fileName: `已加水印_${props.name || '图片'}.png`
        }
      }
      // 下载带水印的图片
      const downloadWatermarkedImage = async () => {
        const imageData = getWatermarkedImageData()
        if (!imageData) {
          ElMessage.warning('没有找到已添加水印的图片')
          return
        }
        try {
          const link = document.createElement('a')
          link.download = imageData.fileName
          link.href = imageData.dataUrl
          link.style.display = 'none'
          document.body.appendChild(link)
          link.click()
          setTimeout(() => {
            document.body.removeChild(link)
            URL.revokeObjectURL(link.href)
          }, 100)
          ElMessage.success('水印图片下载成功')
        } catch (error) {
          console.error('下载水印图片失败:', error)
          ElMessage.error('水印图片下载失败')
        }
      }
      /* ==================== 7、计算属性 ==================== */
      const showPreviewLoading = computed(() => largePreviewLoading.value || !currentPreviewImage.value)
      const showWatermarkLoading = computed(() => imgLoadingForWatermark.value || !currentWatermarkImage.value)
      const currentPreviewImage = computed(() => {
        const canvas = allPreviewCanvases.value.get('preview')
        return canvas ? canvas.toDataURL() : getThumbnailUrl()
      })
      const currentWatermarkImage = computed(() => {
        if (watermarkedCanvas.value) {
          return watermarkedCanvas.value.toDataURL()
        }
        return originalImage.value ? originalImage.value.toDataURL() : ''
      })
      /* ==================== 8、监听与生命周期 ==================== */
      // 监听URL变化
      watch(() => props.url, (newUrl) => { 
        if (newUrl) {
          loadImage()
        }
      }, { immediate: true })
      // 监听水印设置变化
      watch(() => ({ ...usedWatermarkSettings }), () => {
        if (watermarkDialogVisible.value) {
          applyWatermarkToCurrentImage()
        }
      }, { deep: true })
      // 组件挂载时加载图片
      onMounted(() => {
        if (props.url) {
          loadImage()
        }
      })
      // 组件卸载时清理资源
      onUnmounted(() => {
        if (originalImage.value) {
          originalImage.value = null
        }
        if (watermarkedCanvas.value) {
          watermarkedCanvas.value = null
        }
        allPreviewCanvases.value.clear()
      })
      /* ==================== 9、暴露给父组件的方法和属性 ==================== */
      defineExpose({
        openWatermarkDialog,
        getWatermarkedImageData,
        downloadWatermarkedImage,
        hasWatermark: () => hasWatermark.value,
        getWatermarkSettings: () => ({ ...usedWatermarkSettings })
      })
    </script>
    <style scoped>
      /* 1. 容器和布局样式 */
      .image-watermark-container {
        margin: 0 auto;
      }
      .image-section {
        margin-bottom: 30px;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
      }
      .empty-state {
        text-align: center;
        padding: 40px 0;
      }
      .image-preview {
        display: flex;
        justify-content: center;
      }
      /* 2. 缩略图基础样式 */
      .thumbnail-item {
        cursor: pointer;
        transition: all 0.3s ease;
        width: 200px;
        margin: 20px;
      }
      .thumbnail-item:hover {
        border-color: #409eff;
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      }
      .thumbnail-item.watermarked {
        border-color: #67c23a;
      }
      /* ========== isPreview为true时的样式 ========== */
      /* 预览模式:固定尺寸容器 */
      .preview-fixed-container {
        width: 300px;
        height: 150px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #f8f9fa;
        border: 1px solid #e4e7ed;
        border-radius: 8px;
        overflow: hidden;
        position: relative;
      }
      .preview-fixed-img {
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        object-fit: contain;
      }
      /* ========== isPreview为false时的样式 ========== */
      /* 非预览模式:原始大小容器 */
      .original-size-container {
        max-width: 100%;
        max-height: 80vh;
        display: flex;
        justify-content: center;
        align-items: center;
        background: transparent;
      }
      .original-size-img {
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        object-fit: contain;
      }
      /* ========== 共用样式 ========== */
      /* 预览按钮位置调整 */
      .preview-btn.top-right {
        position: absolute;
        top: 2px;
        right: 2px;
        z-index: 10;
      }
      .preview-btn.top-right button {
        color: #5321FF !important;
      }
      /* 3. 缩略图全尺寸模式 */
      .thumbnail-item.full-size {
        width: auto;
        margin: 0 auto;
        display: flex;
        justify-content: center;
        max-width: 100%;
      }
      /* 4. 固定尺寸容器(isPreview为false时) */
      .fixed-size-container {
        width: 500px;
        height: 250px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #f8f9fa;
        border: 1px solid #e4e7ed;
        border-radius: 8px;
        overflow: hidden;
        position: relative;
      }
      .fixed-size-img {
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        object-fit: contain;
      }
      /* 5. 预览模式缩略图(isPreview为true时) */
      .thumbnail-image {
        padding: 24px 12px 12px 12px;
        text-align: center;
        position: relative;
        background: white;
      }
      .thumbnail-image img {
        max-width: 100%;
        height: auto;
        max-height: 300px;
      }
      /* 6. 预览按钮 */
      .preview-btn.top-right {
        position: absolute;
        top: 2px;
        right: 2px;
      }
      .preview-btn.top-right button {
        color: #5321FF !important;
      }
      /* 7. 水印弹窗样式 */
      .watermark-dialog-content {
        display: flex;
        gap: 30px;
        min-height: 600px;
      }
      .preview-panel {
        flex: 2;
        display: flex;
        flex-direction: column;
      }
      .preview-container {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
        border: 1px solid #e4e7ed;
        border-radius: 8px;
        margin-bottom: 20px;
        background: #f8f9fa;
        max-height: 600px;
        position: relative;
        min-height: 400px;
      }
      .preview-image {
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        object-fit: contain;
      }
      .settings-panel {
        width: 420px;
        display: flex;
        flex-direction: column;
      }
      .apply-btn {
        margin-top: auto;
        display: flex;
        justify-content: flex-start;
        gap: 10px;
      }
      /* 8. 预览大图弹窗样式 */
      .preview-dialog-content {
        display: flex;
        flex-direction: column;
        gap: 20px;
      }
      .large-preview-container {
        display: flex;
        justify-content: center;
        align-items: center;
        background: #f8f9fa;
        border-radius: 8px;
        padding: 20px;
        min-height: 500px;
        position: relative;
      }
      .large-preview-image {
        max-width: 100%;
        width: auto;
        height: 70vh;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        object-fit: contain;
        cursor: pointer;
        transition: transform 0.3s ease;
      }
      .large-preview-image:hover {
        transform: scale(1.01);
      }
      /* 9. 弹窗标题固定位置样式 */
      .position-fixed {
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        justify-content: space-between;
        width: 100%;
        background-color: #ffffff;
        z-index: 1000;
        padding: 10px;
      }
      .position-fixed .padding {
        padding-top: 6px;
      }
      /* 10. Loading加载样式 */
      .loading-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 70vh;
        color: #909399;
      }
      .loading-icon {
        font-size: 40px;
        margin-bottom: 16px;
        animation: rotating 2s linear infinite;
      }
      .loading-text {
        font-size: 14px;
      }
      @keyframes rotating {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(360deg);
        }
      }
      /* 11. Element Plus弹窗样式覆盖(深度选择器放在最后) */
      :deep(.el-dialog__body) {
        padding: 20px;
      }
      :deep(.el-dialog) {
        max-width: 90%;
        margin-top: 5vh !important;
      }
      :deep(.el-dialog__header) {
        padding: 20px 20px 10px;
      }
      :deep(.el-dialog__title) {
        font-size: 18p
        font-weight: bold;
      }
    </style>
  (2)使用组件
    <Water1mark
      :ref="el => pdfThumbnailRefs[index] = el"
      :url="item.base64Image"
      @watermarkUpdate="updateWatermark"
      :isText="true"
      :isPreview="true"
    />
4、自定义组件-PDF水印组件PdfThumbnail,来源:nm-web!!!
  附、PDF文件加载后,
    A、页面展示多张缩略图
    B、在弹窗预览展示中,展示大图
    C、在弹窗分页展示中,给选中的缩略图分别添加水印,暴露方法给父组件,供其获取所有的水印图片、合并所有的水印图片为pdf
    D、总:PDF组件,分为页面展示和弹窗展示,隐现弹窗时弹窗没有销毁,以前的弹窗数据仍然保留
  附、pdfjs-dist各版本发布时间与功能简介
    A、1.0.1040,2015-09,1.x正式版,奠定PDF.js核心功能框架(基础渲染、分页、文本选择),兼容IE9+等旧浏览器
    B、1.10.100,2018-05,1.x最后一个稳定版,冻结核心功能,仅保留后续安全补丁支持,兼容IE9+
    C、2.0.943,2019-07,2.x正式发布,重构渲染引擎,提升性能,支持更多PDF2.0标准特性,兼容IE11+
    D、2.16.105,2022-09,2.x最后一个维护版,保留IE11支持,后续版本(3.x+)放弃IE兼容,仅修复严重安全漏洞
    E、3.0.279,2022-01,3.x正式发布版,升级依赖库,重构部分核心模块,首次引入.mjs模块化文件(兼容保留.js)
    F、3.11.174,2023-05,3.x最后一个重要维护版,修复安全漏洞,优化PDF表单渲染兼容性
    G、4.0.189,2023-07,4.x系列初始测试后的迭代版,主要优化ES模块(.mjs)的加载稳定性
    H、4.10.38,2025-01,4.x偏向基础优化的版本,适配后续主流构建工具的兼容性调整
  (1)定义组件
    <template>
      <div class="pdf-thumbnail-container">
        <div class="thumbnails-section">
          <div v-if="thumbnailList.length === 0" class="empty-state">
            <el-empty description="正在上传,请耐心等待......" />
          </div>
          <div v-else class="thumbnails-flex">
            <div
              v-for="(thumbnail, index) in thumbnailList"
              :key="index"
              class="thumbnail-item"
              :class="{ 
                selected: selectedPagesNum.includes(thumbnail.pageNum),
                watermarked: hasWatermark(thumbnail.pageNum)
              }"
              @click="togglePageSelection(thumbnail.pageNum)"
            >
              <div class="thumbnail-image">
                <img :src="getThumbnailUrl(thumbnail.pageNum)" :alt="`第${thumbnail.pageNum}页`" />
                <div class="preview-btn top-right" v-show='props.isPreview'>
                  <el-button 
                    size="small" 
                    type="text" 
                    @click.stop="openPreviewDialog(thumbnail.pageNum)"
                  >
                    预览
                  </el-button>
                </div>
              </div>
              <div class="thumbnail-info" v-show='props.isCheckbox'>
                <el-checkbox
                  :model-value="selectedPagesNum.includes(thumbnail.pageNum)"
                  @click.stop
                  @change="togglePageSelection(thumbnail.pageNum)"
                >
                  P{{ thumbnail.pageNum }}
                </el-checkbox>
              </div>
            </div>
          </div>
        </div>
        <!-- 预览大图弹窗 -->
        <el-dialog
          v-model="previewDialogVisible"
          width="35%"
          top="5vh"
          :show-close="false"
          :close-on-click-modal="true"
          @close="handlePreviewDialogClose"
        >
          <div class="preview-dialog-content">
            <div class="large-preview-container">
              <div v-if="showPreviewLoading" class="loading-container">
                <el-icon class="loading-icon"><Loading /></el-icon>
                <span class="loading-text">大图加载中...</span>
              </div>
              <img
                v-else
                :src="currentPreviewImage"
                alt="预览大图"
                class="large-preview-image"
                @click="previewImageClick"
              />
            </div>
            <!-- 添加翻页按钮 -->
            <div class="preview-navigation">
              <el-button
                :disabled="currentPreviewPageNum <= 1"
                @click="showPrevPreviewPage"
                class="nav-btn"
                type="text"
              >
                <el-icon><ArrowLeft /></el-icon>
                上一页
              </el-button>
              <div class="page-indicator">
                第 {{ currentPreviewPageNum }} 页 / 共 {{ thumbnailList.length }} 页
              </div>
              <el-button
                :disabled="currentPreviewPageNum >= thumbnailList.length"
                @click="showNextPreviewPage"
                class="nav-btn"
                type="text"
              >
                下一页
                <el-icon><ArrowRight /></el-icon>
              </el-button>
            </div>
          </div>
          <template #title>
              <div class="position-fixed">
                  <div class="padding">{{ props.title|| ('第'+ currentPreviewPageNum +'页预览') }}</div>
                  <div> 
                    <el-button 
                      type="primary" 
                      @click="downloadPreviewImage"
                      :loading="previewDownloading"
                    >下载</el-button>
                    <el-button @click="previewDialogVisible = false">关闭</el-button>
                  </div>
              </div>
          </template>
        </el-dialog>
        <!-- 添加水印弹窗 -->
        <el-dialog
          v-model="watermarkDialogVisible"
          title="添加水印"
          width="1000px"
          :close-on-click-modal="false"
        >
          <!-- 水印设置 -->
          <div class="watermark-dialog-content">
            <!-- 预览区域 -->
            <div class="preview-panel">
              <div class="preview-container">
                <div v-if="showWatermarkLoading" class="loading-container">
                  <el-icon class="loading-icon"><Loading /></el-icon>
                  <span class="loading-text">图片加载中...</span>
                </div>
                <img
                  v-else
                  :src="currentWatermarkImage"
                  alt="预览图"
                  class="preview-image"
                />
              </div>
              <div class="pagination">
                <el-button
                  :disabled="watermarkSelectedIndex === 0"
                  @click="showPrevPreview"
                  class="arrow-btn"
                  type="text"
                >
                  <el-icon><ArrowLeft /></el-icon>
                </el-button>
                <div class="page-counter">
                  {{ watermarkSelectedIndex + 1 }} / {{ selectedPagesNum.length }}
                </div>
                <el-button
                  :disabled="watermarkSelectedIndex === selectedPagesNum.length - 1"
                  @click="showNextPreview"
                  class="arrow-btn"
                  type="text"
                >
                  <el-icon><ArrowRight /></el-icon>
                </el-button>
              </div>
            </div>
            <!-- 设置区域 -->
            <div class="settings-panel">
              <el-form :model="usedWatermarkSettings" label-width="100px" label-position="top">
                <el-form-item label="水印信息">
                  <el-input
                    v-model="usedWatermarkSettings.text"
                    placeholder="请输入水印信息"
                    type="textarea"
                    :rows="4"
                    :maxlength="50"
                    show-word-limit 
                  />
                </el-form-item>
                <el-form-item label="颜色">
                  <el-color-picker v-model="usedWatermarkSettings.color" show-alpha />
                </el-form-item>
                <el-form-item label="字号">
                  <el-slider v-model="usedWatermarkSettings.fontSize" :min="5" :max="120" />
                </el-form-item>
                <el-form-item label="旋转">
                  <el-slider v-model="usedWatermarkSettings.rotate" :min="-180" :max="180" />
                </el-form-item>
                <el-form-item label="行距">
                  <el-slider v-model="usedWatermarkSettings.lineSpace" :min="1" :max="5" :step="0.1"  />
                </el-form-item>
                <el-form-item label="偏移">
                  <el-space>
                    <el-input-number v-model="usedWatermarkSettings.left" placeholder="左偏移" controls-position="right" />
                    <el-input-number v-model="usedWatermarkSettings.top" placeholder="上偏移" controls-position="right" />
                  </el-space>
                </el-form-item>
              </el-form>
              <div class="apply-btn">
                <el-button
                  type="primary"
                  @click="handleSave"
                  :loading="loadingUp"
                >确定</el-button> 
              </div>
            </div>
          </div>
        </el-dialog>
      </div>
    </template>
    <script setup> 
      import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
      import * as pdfjsLib from 'pdfjs-dist'
      import { jsPDF } from 'jspdf' //"jspdf": "^3.0.4",
      import { ElMessage, ElMessageBox } from 'element-plus'
      import { ArrowLeft, ArrowRight, CircleCheck, Loading } from '@element-plus/icons-vue'
      import useUserStore from '@/store/modules/user';
      const user = useUserStore().userInfo;
      const watermarkText = user.realName + ' ' + user.userName;
      const http = 'http://10.71.33.88'; //pdf文件的服务器存放地址!!!
      /* ==================== 1、配置pdf.js ==================== */
      pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.env.BASE_URL + 'pdfjs/pdf.worker.min.js'
      //配置pdf.js说明,我已经把下面(1)处的文件复制到(2)处,你只需拉下代码即可
      //目前使用的pdfjs-dist@3.11.174是本项目能运行的最高版本,不要试图使用此后的版本
      //(1)C:\Users\Haier\Desktop\nm-web\node_modules\pdfjs-dist\legacy\build\pdf.worker.min.js
      //(2)C:\Users\Haier\Desktop\nm-web\public\pdfjs\pdf.worker.min.js
      /* ==================== 2、定义全局属性、变量、方法 ==================== */
      //定义属性
      const props = defineProps({ 
        // PDF的URL
        url: {
          type: String,
          required: true
        },
        // 下载文件名
        name: {
          type: String,
          default: ''
        },
        // 弹窗标题
        title: {
          type: String,
          default: ''
        },
        // 是否可以预览
        isPreview: {
          type: Boolean,
          default: true
        },
        // 是否添加水印
        isText: {
          type: Boolean,
          default: true
        },
        // 是否显示复选框
        isCheckbox: {
          type: Boolean,
          default: true
        },
      })
      //pdf文件的基本信息
      const pdfInfo = reactive({
        name: '', //PDF文件名
        totalPages: 0 //PDF总页数
      })
      //默认水印配置
      const defaultWatermarkSettings = {
        text: props.isText ? watermarkText : '', //默认水印文字
        color: '#999999', //默认水印颜色
        fontSize: 30, //默认字体大小
        rotate: -30, //默认旋转角度
        lineSpace: 1, //默认行间距
        left: 0, //默认水平偏移
        top: 0 //默认垂直偏移
      }
      //当前使用的水印配置 - 所有页面都使用这个配置
      const usedWatermarkSettings = reactive({ ...defaultWatermarkSettings })
      //定义存储器
      const thumbnailList = ref([]) //存储所有缩略图(用于页面展示)
      const allThumbnails = ref(new Map()) //存储所有原始图片的元数据(用于生成页面缩略图),key为页码,value包含宽、高、dataURL
      const allWatermarkImg = ref(new Map()) //存储所有原始图片的副本(用于生成水印弹窗展示图),key为页码,value为canvas对象
      const allPreviewCanvases = ref(new Map()) //存储所有缩略图的预览图(用于生成预览弹窗展示图),key为页码,value为canvas对象
      const selectedPagesNum = ref([]) //存储已勾选的缩略图页码
      const watermarkedCanvases = ref(new Map()) //存储已添加水印的缩略图,key为页码,value为已添加水印的canvas对象
      const watermarkedPagesNum = ref(new Set()) //存储已添加水印的缩略图页码
      //给当前canvas添加水印 - 始终使用统一的watermarkSettings
      const addWatermarkToCanvas = (context, canvasWidth, canvasHeight) => {
        const { text, color, fontSize, rotate, lineSpace, left, top } = usedWatermarkSettings
        if (!text || text.trim() === '') return //如果没有水印文字,不添加水印
        //设置水印样式
        context.fillStyle = color
        context.font = `${fontSize}px Arial`
        context.textAlign = 'left'
        context.textBaseline = 'middle'
        //处理换行文本 - 将文本按换行符分割成多行
        const lines = text.split('\n').filter(line => line.trim() !== '')
        if (lines.length === 0) return
        //计算每行文本的宽度并找出最长的一行
        let maxLineWidth = 0
        lines.forEach(line => {
          const lineWidth = context.measureText(line.trim()).width
          if (lineWidth > maxLineWidth) {
            maxLineWidth = lineWidth
          }
        })
        //计算单行文本高度(包括行间距)
        const textHeight = fontSize * lineSpace
        //const lineSpacing = textHeight * (lines.length+1) //使用配置的行距或默认值
        const lineSpacing = 300 //使用配置的行距或默认值
        //保存当前状态
        context.save() 
        //应用偏移和旋转
        context.translate(canvasWidth / 2 + left, canvasHeight / 2 + top)
        context.rotate(rotate * Math.PI / 180)
        //绘制网格水印 - 使用最长行宽度的1.5倍作为水平间距
        const horizontalGap = maxLineWidth * 1.5
        for (let x = -canvasWidth; x < canvasWidth * 2; x += horizontalGap) {
          for (let y = -canvasHeight; y < canvasHeight * 2; y += lineSpacing) {
            //绘制多行文本
            lines.forEach((line, index) => {
              const yOffset = y + index * textHeight
              context.fillText(line.trim(), x, yOffset)
            })
          }
        }
        //恢复状态
        context.restore()
      }
      /* ==================== 3、页面初始化 ==================== */
      //加载PDF文档
      const loadPdfDocument = async () => {
        if (!props.url) return
        try {
          let pdfUrl = props.url
          /* const isDev = import.meta.env.DEV */
          const isDev = process.env.NODE_ENV === 'development';
          if (isDev && pdfUrl.includes(http)) {
            pdfUrl = pdfUrl.replace(http, '/api')
          }
          const fileName = pdfUrl.split('/').pop() || '证照.pdf'
          pdfInfo.name = fileName
          pdfInfo.totalPages = 0
          // 重置状态
          thumbnailList.value = []
          allThumbnails.value.clear()
          allWatermarkImg.value.clear()
          selectedPagesNum.value = []
          // 重置水印状态
          watermarkedPagesNum.value.clear()
          watermarkedCanvases.value.clear()
          allPreviewCanvases.value.clear()
          const pdfDoc = await pdfjsLib.getDocument(pdfUrl).promise
          pdfInfo.totalPages = pdfDoc.numPages
          const displayPages = pdfDoc.numPages
          // 生成所有缩略图
          for (let pageNum = 1; pageNum <= displayPages; pageNum++) {
            await generateThumbnail(pdfDoc, pageNum)
          }
          // 如果默认有水印文字,为所有页面添加水印
          if (usedWatermarkSettings.text && usedWatermarkSettings.text.trim() !== '') {
            for (let pageNum = 1; pageNum <= displayPages; pageNum++) {
              await addWatermarkToPage(pageNum)
            }
          }
        } catch (error) {
          console.error('PDF处理错误:', error)
          ElMessage.error('PDF文件处理失败')
        }
      }
      //生成缩略图
      const generateThumbnail = async (pdfDoc, pageNum) => {
        try {
          // 获取PDF页面
          const page = await pdfDoc.getPage(pageNum)
          const viewport = page.getViewport({ scale: 0.2 })
          // 创建canvas
          const eleCanvas = document.createElement('canvas')
          const context = eleCanvas.getContext('2d')
          eleCanvas.width = viewport.width
          eleCanvas.height = viewport.height
          // 渲染页面
          await page.render({
            canvasContext: context,
            viewport: viewport
          }).promise
          // 保存原始缩略图信息(不带水印)
          thumbnailList.value.push({
            pageNum,
            dataUrl: eleCanvas.toDataURL()
          })
          allThumbnails.value.set(pageNum, {
            width: eleCanvas.width,
            height: eleCanvas.height,
            dataUrl: eleCanvas.toDataURL()
          })
          // 创建高质量副本用于水印编辑
          const highQualityCanvas = document.createElement('canvas')
          const highQualityViewport = page.getViewport({ scale: 1.0 })
          highQualityCanvas.width = highQualityViewport.width
          highQualityCanvas.height = highQualityViewport.height
          const highQualityContext = highQualityCanvas.getContext('2d')
          await page.render({
            canvasContext: highQualityContext,
            viewport: highQualityViewport
          }).promise
          allWatermarkImg.value.set(pageNum, highQualityCanvas)
        } catch (error) {
          console.error(`生成第${pageNum}页缩略图失败:`, error)
          ElMessage.error(`第${pageNum}页缩略图生成失败`)
        }
      }
      // 为指定页面添加水印
      const addWatermarkToPage = async (pageNum) => {
        // 如果没有水印文字,直接返回,不添加水印
        if (!usedWatermarkSettings.text || usedWatermarkSettings.text.trim() === '') {
          return
        }
        try {
          // 获取高质量原始图片
          const canvas = allWatermarkImg.value.get(pageNum)
          if (!canvas) {
            console.error(`第${pageNum}页的高质量图片不存在`)
            return
          }
          // 创建新canvas用于添加水印
          const watermarkedCanvas = document.createElement('canvas')
          watermarkedCanvas.width = canvas.width
          watermarkedCanvas.height = canvas.height
          const context = watermarkedCanvas.getContext('2d')
          // 先绘制原始内容
          context.drawImage(canvas, 0, 0, canvas.width, canvas.height)
          // 给当前canvas添加水印
          addWatermarkToCanvas(context, watermarkedCanvas.width, watermarkedCanvas.height)
          // 存储已添加水印的canvas对象
          watermarkedCanvases.value.set(pageNum, watermarkedCanvas)
          watermarkedPagesNum.value.add(pageNum)
          // 更新高质量图片副本
          allWatermarkImg.value.set(pageNum, watermarkedCanvas)
          // 更新缩略图显示
          updateThumbnailDisplay(pageNum)
        } catch (error) {
          console.error(`为第${pageNum}页添加水印失败:`, error)
        }
      }
      //点击缩略图
      const togglePageSelection = (pageNum) => {
        const index = selectedPagesNum.value.indexOf(pageNum)
        if (index === -1) {
          selectedPagesNum.value.push(pageNum)
        } else {
          selectedPagesNum.value.splice(index, 1)
        }
      }
      const getCurrentSelectionStatus = () => {
        return {
          selectedPages: [...selectedPagesNum.value],
          selectedCount: selectedPagesNum.value.length
        }
      }
      /* ==================== 4、预览大图弹窗 ==================== */
      const previewDialogVisible = ref(false) 
      const largePreviewLoading = ref(false)
      const currentPreviewPageNum = ref(1) //当前预览弹窗图片在缩略图中的页码
      // 处理弹窗关闭
      const handlePreviewDialogClose = () => {
        largePreviewLoading.value = false
      }
      // 打开预览大图弹窗
      const openPreviewDialog = async (pageNum) => {
        try {
          currentPreviewPageNum.value = pageNum
          previewDialogVisible.value = true
          // 等待弹窗完全打开
          await nextTick()
          // 生成高质量预览大图
          await navigateToPreviewPage(pageNum)
        } catch (error) {
          console.error('打开预览弹窗失败:', error)
          ElMessage.error('打开预览失败:' + error.message)
          previewDialogVisible.value = false
        }
      }
      //生成预览大图
      const generateLargePreview = async (pageNum) => {
        if (!props.url) {
          largePreviewLoading.value = false
          ElMessage.error('PDF URL为空')
          return
        }
        // 如果已经生成过,直接返回
        if (allPreviewCanvases.value.has(pageNum)) {
          return true
        }
        try {
          largePreviewLoading.value = true
          let pdfUrl = props.url
          /* const isDev = import.meta.env.DEV */
          const isDev = process.env.NODE_ENV === 'development';
          if (isDev && pdfUrl.includes(http)) {
            pdfUrl = pdfUrl.replace(http, '/api')
          }
          const pdfDoc = await pdfjsLib.getDocument(pdfUrl).promise
          const page = await pdfDoc.getPage(pageNum)
          const viewport = page.getViewport({ scale: 2.0 })
          const canvas = document.createElement('canvas')
          const context = canvas.getContext('2d')
          canvas.width = viewport.width
          canvas.height = viewport.height
          await page.render({
            canvasContext: context,
            viewport: viewport
          }).promise
          // 如果有水印文字,并且该页面已经有水印,才添加水印
          if (usedWatermarkSettings.text && usedWatermarkSettings.text.trim()!=='' && watermarkedPagesNum.value.has(pageNum)){
            addWatermarkToCanvas(context, canvas.width, canvas.height)
          }
          allPreviewCanvases.value.set(pageNum, canvas)
          return true
        } catch (error) {
          console.error('生成预览大图失败:', error)
          ElMessage.error('预览大图生成失败:' + error.message)
          return false
        } finally {
          largePreviewLoading.value = false
        }
      }
      // 显示上一页预览大图
      const showPrevPreviewPage = async () => {
        if (currentPreviewPageNum.value > 1) {
          const newPageNum = currentPreviewPageNum.value - 1
          await navigateToPreviewPage(newPageNum)
        }
      }
      // 显示下一页预览大图
      const showNextPreviewPage = async () => {
        if (currentPreviewPageNum.value < thumbnailList.value.length) {
          const newPageNum = currentPreviewPageNum.value + 1
          await navigateToPreviewPage(newPageNum)
        }
      }
      // 导航到指定页码的预览图
      const navigateToPreviewPage = async (pageNum) => {
        try {
          // 更新当前页码
          currentPreviewPageNum.value = pageNum
          // 检查是否需要生成预览大图
          if (!allPreviewCanvases.value.has(pageNum)) {
            await generateLargePreview(pageNum)
          }
          // 等待UI更新
          await nextTick()
        } catch (error) {
          console.error('切换预览页面失败:', error)
          ElMessage.error('切换页面失败:' + error.message)
        }
      }
      // 处理图片点击事件(可选:点击图片左右区域翻页)
      const previewImageClick = (event) => {
        const imageRect = event.target.getBoundingClientRect()
        const clickX = event.clientX - imageRect.left
        const imageWidth = imageRect.width
        // 点击左三分之一区域:上一页
        if (clickX < imageWidth / 3) {
          showPrevPreviewPage()
        }
        // 点击右三分之一区域:下一页
        else if (clickX > imageWidth * 2 / 3) {
          showNextPreviewPage()
        }
      }
      //当前预览大图弹窗图片加载
      const showPreviewLoading = computed(() => {
        return largePreviewLoading.value || !currentPreviewImage.value
      })
      //当前预览大图弹窗图片
      const currentPreviewImage = computed(() => {
        const canvas = allPreviewCanvases.value.get(currentPreviewPageNum.value)
        if (canvas) {
          return canvas.toDataURL() //返回预览大图的dataURL
        }
        return getThumbnailUrl(currentPreviewPageNum.value) //如果没有预览大图,返回缩略图URL
      })
      //当前预览大图弹窗图片尺寸
      const currentPreviewSize = computed(() => {
        const canvas = allPreviewCanvases.value.get(currentPreviewPageNum.value)
        if (canvas) {
          return `${canvas.width} × ${canvas.height} 像素` //返回图片尺寸信息
        }
        const thumbnail = allThumbnails.value.get(currentPreviewPageNum.value)
        if (thumbnail) {
          return `${thumbnail.width} × ${thumbnail.height} 像素`
        }
        return '未知尺寸' //默认返回未知尺寸
      })
      //获取缩略图URL(优先返回带水印的缩略图)
      const getThumbnailUrl = (pageNum) => {
        const thumbnail = thumbnailList.value.find(t => t.pageNum === pageNum)
        if (!thumbnail) return ''
        // 如果有水印版本,返回水印版本
        if (hasWatermark(pageNum)) {
          const watermarkedCanvas = watermarkedCanvases.value.get(pageNum)
          if (watermarkedCanvas) {
            // 创建缩略图尺寸的canvas
            const thumbCanvas = document.createElement('canvas')
            const pastImage = allThumbnails.value.get(pageNum)
            if (pastImage) {
              thumbCanvas.width = pastImage.width
              thumbCanvas.height = pastImage.height
              const context = thumbCanvas.getContext('2d')
              context.drawImage(
                watermarkedCanvas, 
                0, 0, watermarkedCanvas.width, watermarkedCanvas.height,
                0, 0, thumbCanvas.width, thumbCanvas.height
              )
              return thumbCanvas.toDataURL()
            }
          }
        }
        //返回原始缩略图
        return thumbnail.dataUrl
      }
      //下载预览图片
      const previewDownloading = ref(false) //下载状态,控制下载按钮的loading效果
      const downloadPreviewImage = async () => {
        const canvas = allPreviewCanvases.value.get(currentPreviewPageNum.value)
        if (!canvas) {
          ElMessage.warning('暂无预览图片可下载')
          return
        }
        try {
          previewDownloading.value = true
          const link = document.createElement('a')
          link.download = `第${currentPreviewPageNum.value}张图片.png`
          link.href = canvas.toDataURL('image/png')
          document.body.appendChild(link)
          link.click()
          document.body.removeChild(link) 
          ElMessage.success('图片下载成功')
        } catch (error) {
          console.error('下载图片失败:', error)
          ElMessage.error('图片下载失败')
        } finally {
          previewDownloading.value = false
        }
      }
      /* ==================== 5、添加水印弹窗 ==================== */
      const watermarkDialogVisible = ref(false) 
      const imgLoadingForWatermark = ref(false) 
      const watermarkSelectedIndex = ref(0) //当前水印弹窗图片在被勾选图中的索引
      //打开添加水印弹窗
      const openWatermarkDialog = async () => {
        if (selectedPagesNum.value.length === 0) return
        selectedPagesNum.value.sort((a, b) => a - b)
        watermarkSelectedIndex.value = 0
        watermarkDialogVisible.value = true
        // 重置为默认水印配置
        Object.assign(usedWatermarkSettings, defaultWatermarkSettings)
        await getImgForAddWatermark()
      }
      //获取图片为添加水印做准备
      const getImgForAddWatermark = async () => {
        if (!props.url) return
        try {
          imgLoadingForWatermark.value = true //开始加载
          const pdfDoc = await pdfjsLib.getDocument(props.url).promise
          for (const pageNum of selectedPagesNum.value) { //遍历所有已选中的PDF缩略图
            //如果图片原本高清就跳过,否则就强化且替掉原图
            if (allWatermarkImg.value.get(pageNum)?.width > 800) continue
            const page = await pdfDoc.getPage(pageNum)
            const viewport = page.getViewport({ scale: 1.5 }) //提高预览质量!!!
            const canvas = document.createElement('canvas')
            const context = canvas.getContext('2d')
            canvas.width = viewport.width
            canvas.height = viewport.height
            await page.render({ //把pdf页渲染到canvas上
              canvasContext: context,
              viewport: viewport
            }).promise
            // 如果该页面已有水印,先添加水印
            if (watermarkedPagesNum.value.has(pageNum)) {
              const watermarkedCanvas = watermarkedCanvases.value.get(pageNum)
              if (watermarkedCanvas) {
                allWatermarkImg.value.set(pageNum, watermarkedCanvas)
                continue
              }
            }
            allWatermarkImg.value.set(pageNum, canvas)
          }
        } catch (error) {
          console.error('生成高质量预览图失败:', error)
        } finally {
          imgLoadingForWatermark.value = false //加载完成
        }
      } 
      //当前添加水印弹窗图片在缩略图中的页码
      const watermarkThumbnailPageNum = computed(() => {
        return selectedPagesNum.value[watermarkSelectedIndex.value]
      })
      //当前添加水印弹窗图片加载
      const showWatermarkLoading = computed(() => { 
        return imgLoadingForWatermark.value || !currentWatermarkImage.value
      }) 
      //当前添加水印弹窗图片
      const currentWatermarkImage = computed(() => { 
        // 获取当前页面的图片
        const currentPageNum = watermarkThumbnailPageNum.value
        const watermarkedCanvas = watermarkedCanvases.value.get(currentPageNum)
        if (watermarkedCanvas) {
          return watermarkedCanvas.toDataURL() //返回带水印图片的dataURL
        }
        const canvas = allWatermarkImg.value.get(currentPageNum)
        return canvas ? canvas.toDataURL() : '' //返回原始图片的dataURL或空字符串
      })
      //检查页面是否有水印
      const hasWatermark = (pageNum) => {
        return watermarkedPagesNum.value.has(pageNum)
      }
      //上一页预览
      const showPrevPreview = () => {
        if (watermarkSelectedIndex.value > 0) {
          watermarkSelectedIndex.value--
          // 翻页时直接将最新的水印设置应用到当前图片
          applyWatermarkToCurrentPage()
        }
      }
      //下一页预览
      const showNextPreview = () => {
        if (watermarkSelectedIndex.value < selectedPagesNum.value.length - 1) {
          watermarkSelectedIndex.value++
          // 翻页时直接将最新的水印设置应用到当前图片
          applyWatermarkToCurrentPage()
        }
      }
      //将水印应用到当前页面
      const applyWatermarkToCurrentPage = async () => {
        const currentPageNum = selectedPagesNum.value[watermarkSelectedIndex.value]
        try {
          // 清除旧水印(如果存在)
          if (hasWatermark(currentPageNum)) {
            watermarkedPagesNum.value.delete(currentPageNum)
            watermarkedCanvases.value.delete(currentPageNum)
            allPreviewCanvases.value.delete(currentPageNum) //清除预览大图缓存
          }
          // 重新生成高质量原始图片
          const currCanvas = await generateHighQualityPage(currentPageNum)
          if (!currCanvas) return
          // 如果没有水印文字,直接保存原始图片,不添加水印
          if (!usedWatermarkSettings.text || usedWatermarkSettings.text.trim() === '') {
            allWatermarkImg.value.set(currentPageNum, currCanvas)
            // 更新缩略图显示
            updateThumbnailDisplay(currentPageNum)
            return
          }
          // 创建新canvas用于添加水印,保持高质量尺寸
          const watermarkedCanvas = document.createElement('canvas')
          watermarkedCanvas.width = currCanvas.width
          watermarkedCanvas.height = currCanvas.height
          const context = watermarkedCanvas.getContext('2d')
          // 先绘制原始内容
          context.drawImage(currCanvas, 0, 0)
          // 给当前canvas添加水印
          addWatermarkToCanvas(context, watermarkedCanvas.width, watermarkedCanvas.height)
          // 存储已添加水印的canvas对象
          watermarkedCanvases.value.set(currentPageNum, watermarkedCanvas)
          watermarkedPagesNum.value.add(currentPageNum)
          // 更新高质量图片副本
          allWatermarkImg.value.set(currentPageNum, watermarkedCanvas)
          // 更新缩略图显示
          updateThumbnailDisplay(currentPageNum)
        } catch (error) {
          console.error('添加水印失败:', error)
          ElMessage.error('水印添加失败')
        }
      }
      //生成高质量页面
      const generateHighQualityPage = async (pageNum) => {
        if (!props.url) return null
        try {
          const pdfDoc = await pdfjsLib.getDocument(props.url).promise
          const page = await pdfDoc.getPage(pageNum)
          const viewport = page.getViewport({ scale: 1.5 }) //使用高质量缩放!!!
          const canvas = document.createElement('canvas')
          const context = canvas.getContext('2d')
          canvas.width = viewport.width
          canvas.height = viewport.height
          await page.render({ //把pdf页渲染到canvas上
            canvasContext: context,
            viewport: viewport
          }).promise
          return canvas
        } catch (error) {
          console.error('生成高质量页面失败:', error)
          return null
        }
      }
      //更新缩略图显示
      const updateThumbnailDisplay = (pageNum) => {
        const thumbnailIndex = thumbnailList.value.findIndex(t => t.pageNum === pageNum)
        if (thumbnailIndex !== -1) {
          //触发重新渲染
          thumbnailList.value = [...thumbnailList.value]
        }
      }
      /* ==================== 6、监听与生命周期 ==================== */
      // 修改watch,监听URL变化!!!
      watch(() => props.url, (newUrl) => { 
        if (newUrl) {
          loadPdfDocument()
        }
      })
      // 监听水印设置变化,实时应用到当前预览图片
      watch(usedWatermarkSettings, () => {
        // 只有当水印弹窗打开时才实时应用水印
        if (watermarkDialogVisible.value && watermarkSelectedIndex.value >= 0) {
          applyWatermarkToCurrentPage()
        }
      }, { deep: true })
      //组件挂载时加载PDF
      onMounted(() => {
        if (props.url) {
          loadPdfDocument()
        }
      })
      /* ==================== 7、暴露给父组件且仅在父组件使用 ==================== */
      const selectedPagesCount = computed(() => selectedPagesNum.value.length)
      //更新缩略图显示
      const getWatermarkedSelectedImages = () => {
        //找出既被选中又有水印的页面
        const targetPages = selectedPagesNum.value.filter(page => watermarkedPagesNum.value.has(page))
        const images = []
        for (const pageNum of targetPages) {
          const canvas = watermarkedCanvases.value.get(pageNum)
          if (canvas) {
            images.push({
              pageNum,
              dataUrl: canvas.toDataURL('image/png'),
              width: canvas.width,
              height: canvas.height,
              fileName: `已加水印_第${pageNum}页.png`
            })
          }
        }
        return images
      }
      //生成并下载PDF
      const getWatermarkedPDF = async () => {
        try {
          let images = []
          let fileName = ''
          // 判断逻辑:如果usedWatermarkSettings.text存在,下载所有图片,否则走原来的逻辑
          if (usedWatermarkSettings.text && usedWatermarkSettings.text.trim() !== '') {
            // text属性存在:下载所有图片
            ElMessage.info(`正在为所有${thumbnailList.value.length}页生成PDF...`)
            // 按页码排序
            const sortedPages = thumbnailList.value
              .map(t => t.pageNum)
              .sort((a, b) => a - b)
            // 获取所有页面的高质量图片
            const imagePromises = sortedPages.map(async (pageNum) => {
              // 尝试获取已存在的水印图片
              const watermarkedCanvas = watermarkedCanvases.value.get(pageNum)
              if (watermarkedCanvas) {
                return {
                  pageNum,
                  dataUrl: watermarkedCanvas.toDataURL('image/png'),
                  width: watermarkedCanvas.width,
                  height: watermarkedCanvas.height,
                  fileName: `第${pageNum}页_${usedWatermarkSettings.text}.png`
                }
              }
              // 如果没有水印图片,生成高质量图片并添加水印
              const canvas = await generateHighQualityPage(pageNum)
              if (canvas) {
                // 添加水印
                const context = canvas.getContext('2d')
                addWatermarkToCanvas(context, canvas.width, canvas.height)
                // 存储水印图片
                watermarkedCanvases.value.set(pageNum, canvas)
                watermarkedPagesNum.value.add(pageNum)
                return {
                  pageNum,
                  dataUrl: canvas.toDataURL('image/png'),
                  width: canvas.width,
                  height: canvas.height,
                  fileName: `第${pageNum}页_${usedWatermarkSettings.text}.png`
                }
              }
              return null
            })
            // 等待所有图片准备完成
            const imageResults = await Promise.all(imagePromises)
            images = imageResults.filter(img => img !== null)
            if (images.length === 0) {
              ElMessage.warning('没有找到可用的图片数据')
              return
            }
            fileName = `${props.name||'pdf文件'}.pdf`
          } else {
            // text属性不存在:走原来的逻辑(只下载已勾选且有水印的图片)
            const watermarkedSelectedImages = getWatermarkedSelectedImages()
            if (watermarkedSelectedImages.length === 0) {
              ElMessage.warning('没有找到已勾选且已添加水印的图片')
              return
            }
            ElMessage.info('正在为已选中的水印图片生成PDF...')
            images = watermarkedSelectedImages
            images.sort((a, b) => a.pageNum - b.pageNum)
            fileName = `${props.name||'pdf文件'}.pdf`
          }
          // 创建新的PDF文档
          const pdf = new jsPDF()
          // 遍历所有图片,添加到PDF
          for (let i = 0; i < images.length; i++) {
            const image = images[i]
            // 显示进度提示(特别是处理大量页面时)
            if (images.length > 5 && (i === 0 || i === images.length - 1 || i % 5 === 0)) {
              ElMessage.info(`正在处理第${i + 1}/${images.length}页...`)
            }
            // 将dataURL转换为Image对象
            const img = new Image()
            img.src = image.dataUrl
            // 等待图片加载
            await new Promise((resolve) => {
              img.onload = resolve //当图片成功加载时,调用resolve()函数,即await结束,继续执行后面代码
              img.onerror = () => {
                console.error(`第${image.pageNum}页图片加载失败`)
                ElMessage.warning(`第${image.pageNum}页图片加载失败,已跳过`)
                resolve()
              }
            })
            // 获取图片尺寸
            const imgWidth = img.width
            const imgHeight = img.height
            // PDF页面尺寸 (A4)
            const pdfWidth = pdf.internal.pageSize.getWidth()
            const pdfHeight = pdf.internal.pageSize.getHeight()
            // 计算缩放比例,保持图片比例
            const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight)
            const width = imgWidth * ratio * 0.95 // 稍微缩小一点,留出边距
            const height = imgHeight * ratio * 0.95
            // 计算居中位置
            const x = (pdfWidth - width) / 2
            const y = (pdfHeight - height) / 2
            // 添加图片到PDF。!!!非常重要1/3
            pdf.addImage(img, 'PNG', x, y, width, height)
            // 如果不是最后一页,添加新页面。非常重要2/3
            if (i < images.length - 1) {
              pdf.addPage()
            }
          }
          // 下载PDF。非常重要3/3
          pdf.save(fileName)
          // 显示成功信息
          if (usedWatermarkSettings.text && usedWatermarkSettings.text.trim() !== '') {
            ElMessage.success(`PDF生成成功!共${images.length}页,已全部添加水印`)
          } else {
            ElMessage.success(`PDF生成成功!共${images.length}张水印图片`)
          }
        } catch (error) {
          console.error('生成PDF失败:', error)
          // 更详细的错误提示
          if (error.message && error.message.includes('超过最大尺寸')) {
            ElMessage.error('图片尺寸过大,请尝试减少页面数量')
          } else if (error.message && error.message.includes('内存')) {
            ElMessage.error('处理图片时内存不足,请尝试分批处理')
          } else {
            ElMessage.error('PDF生成失败:' + (error.message || '未知错误'))
          }
        }
      }
      //生成并发送PDF的dataURI!!!
      const emit = defineEmits(['watermarkUpdate']);
      const loadingUp = ref(false) 
      const handleSave = async () => {
        if (selectedPagesNum.value.length === 0) {
          ElMessage.warning('请先选择图片')
          return
        }
        try {
          loadingUp.value = true
          // 为所有选中的页面应用当前水印设置
          for (const pageNum of selectedPagesNum.value) {
            // 清除旧水印(如果存在)
            if (hasWatermark(pageNum)) {
              watermarkedPagesNum.value.delete(pageNum)
              watermarkedCanvases.value.delete(pageNum)
              allPreviewCanvases.value.delete(pageNum)
            }
            // 重新生成高质量原始图片
            const currCanvas = await generateHighQualityPage(pageNum)
            if (!currCanvas) continue
            // 如果没有水印文字,直接保存原始图片,不添加水印
            if (!usedWatermarkSettings.text || usedWatermarkSettings.text.trim() === '') {
              allWatermarkImg.value.set(pageNum, currCanvas)
              // 更新缩略图显示
              updateThumbnailDisplay(pageNum)
              continue
            }
            // 创建新canvas用于添加水印
            const watermarkedCanvas = document.createElement('canvas')
            watermarkedCanvas.width = currCanvas.width
            watermarkedCanvas.height = currCanvas.height
            const context = watermarkedCanvas.getContext('2d')
            // 先绘制原始内容
            context.drawImage(currCanvas, 0, 0)
            // 给当前canvas添加水印
            addWatermarkToCanvas(context, watermarkedCanvas.width, watermarkedCanvas.height)
            // 存储已添加水印的canvas对象
            watermarkedCanvases.value.set(pageNum, watermarkedCanvas)
            watermarkedPagesNum.value.add(pageNum)
            // 更新高质量图片副本
            allWatermarkImg.value.set(pageNum, watermarkedCanvas)
            // 更新缩略图显示
            updateThumbnailDisplay(pageNum)
          }
          // 更新当前预览图片
          const currentPageNum = selectedPagesNum.value[watermarkSelectedIndex.value]
          if (watermarkedCanvases.value.has(currentPageNum)) {
            // 触发重新渲染
            await nextTick()
          }
        } catch (error) {
          console.error('批量添加水印失败:', error)
          ElMessage.error('批量添加水印失败')
        }
        try {
          // 1. 获取所有已勾选且已添加水印的页面
          // 找出既被选中又有水印的页面
          const watermarkedSelectedPages = selectedPagesNum.value.filter(pageNum => 
            watermarkedPagesNum.value.has(pageNum)
          )
          if (watermarkedSelectedPages.length === 0) {
            throw new Error('没有找到已勾选且已添加水印的图片')
          }
          watermarkedSelectedPages.sort((a, b) => a - b)
          // 2. 获取所有水印图片的高质量数据
          const images = []
          for (const pageNum of watermarkedSelectedPages) {
            // 尝试获取已存在的水印图片
            const canvas = watermarkedCanvases.value.get(pageNum)
            if (canvas) {
              images.push({
                pageNum,
                dataUrl: canvas.toDataURL('image/png'),
                width: canvas.width,
                height: canvas.height,
                fileName: `已加水印_第${pageNum}页.png`
              })
              continue
            }
          }
          if (images.length === 0) {
            throw new Error('无法获取水印图片数据')
          }
          // 3. 创建PDF文档
          const pdf = new jsPDF()
          for (let i = 0; i < images.length; i++) {
            const image = images[i]
            // 创建图片对象
            const img = new Image()
            img.src = image.dataUrl
            // 等待图片加载
            await new Promise((resolve) => {
              img.onload = resolve //当图片成功加载时,调用resolve()函数,即await结束,代码继续执行
              img.onerror = () => {
                console.error(`第${image.pageNum}页图片加载失败`)
                ElMessage.warning(`第${image.pageNum}页图片加载失败,已跳过`)
                resolve()
              }
            })
            // PDF页面尺寸 (A4)
            const pdfWidth = pdf.internal.pageSize.getWidth()
            const pdfHeight = pdf.internal.pageSize.getHeight()
            // 计算缩放比例,保持图片比例
            const ratio = Math.min(pdfWidth / img.width, pdfHeight / img.height)
            const width = img.width * ratio * 0.95 // 稍微缩小一点,留出边距
            const height = img.height * ratio * 0.95
            // 计算居中位置
            const x = (pdfWidth - width) / 2
            const y = (pdfHeight - height) / 2
            // 添加图片到PDF。!!!非常重要1/3
            pdf.addImage(img, 'PNG', x, y, width, height)
            // 如果不是最后一页,添加新页面。非常重要2/3
            if (i < images.length - 1) {
              pdf.addPage()
            }
          }
          // 4. 获取PDF数据。非常重要3/3
          const pdfDataUri = pdf.output('datauristring') // data URI
          // 5. 发送事件给父组件
          const sizeEstimate = Math.ceil(pdfDataUri.split(',')[1].length * 0.75);
          console.log(`pdf.vue页面,第1095行,估算上传pdf文件大小为: ${sizeEstimate/1000/1000} M`);
          emit('watermarkUpdate', pdfDataUri)
        } catch (error) {
          // 发送错误信息给父组件
          const errorResult = {
            success: false,
            error: error.message,
            selectedPages: [...selectedPagesNum.value],
            watermarkSettings: { ...usedWatermarkSettings }
          }
          emit('watermarkUpdate', errorResult)
        } finally {
          loadingUp.value = false
          watermarkDialogVisible.value = false
        }
      }
      //暴露给父组件的方法和属性
      defineExpose({
        openWatermarkDialog,
        selectedPagesCount,
        getSelectedPages: () => selectedPagesNum.value,
        getWatermarkedPages: () => Array.from(watermarkedPagesNum.value),
        getWatermarkedSelectedImages,
        getWatermarkedPDF,
        getCurrentSelectionStatus,
      })
    </script>
    <style scoped>
      .position-fixed {
          position: fixed;
          top: 0;
          left: 0 ;
          display: flex;
          justify-content: space-between;
          width: 100%;
          background-color: #ffffff;
          z-index: 1000;
          padding: 10px;
          .padding{
              padding-top: 6px;
          }
      }
      /* 样式保持不变 */
      .pdf-thumbnail-container {
        margin: 0 auto;
      }
      .file-section {
        margin-bottom: 30px;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
      }
      .file-info {
        margin-top: 15px;
        padding: 15px;
        background: #e7f3ff;
        border-radius: 6px;
      }
      .file-info p {
        margin: 5px 0;
        color: #606266;
      }
      .page-notice {
        margin-top: 15px;
      }
      .thumbnails-section {
        margin-top: -10px;
      }
      .empty-state {
        text-align: center;
        padding: 40px 0;
      }
      .thumbnails-flex {
        display: flex;
        flex-direction: flex-start;
        flex-wrap: wrap;
      }
      .thumbnail-item {
        cursor: pointer;
        transition: all 0.3s ease;
        width: 130px;
        /* height: 210px;  */
        margin: 20px;
      }
      .thumbnail-item:hover {
        border-color: #409eff;
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      }
      .thumbnail-item.selected { 
        border-color: #409eff;
        background: #f0f7ff;
      }
      .thumbnail-item.watermarked {
        border-color: #67c23a;
      }
      .thumbnail-image {
        padding: 24px 12px 12px 12px;
        text-align: center;
        position: relative;
        background: white;
      }
      .thumbnail-image img {
        max-width: 100%;
        height: auto;
      }
      .watermark-badge.bottom-left {
        position: absolute;
        bottom: 5px;
        left: 5px;
      }
      .preview-btn.top-right {
        position: absolute;
        top: 2px;
        right: 2px;
      }
      .preview-btn.top-right button{
        color: #5321FF !important;
      }
      .thumbnail-info {
        text-align: center;
        margin-top: 8px;
      }
      .watermark-dialog-content {
        display: flex;
        gap: 30px;
        min-height: 600px;
      }
      .preview-panel {
        flex: 2;
        display: flex;
        flex-direction: column;
      }
      .preview-container {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
        border: 1px solid #e4e7ed;
        border-radius: 8px;
        margin-bottom: 20px;
        background: #f8f9fa;
        max-height: 600px;
        position: relative;
      }
      .preview-image {
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        object-fit: contain;
      }
      .preview-watermark-badge {
        position: absolute;
        top: 10px;
        left: 10px;
      }
      .pagination {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
      }
      .arrow-btn {
        width: 40px;
        height: 40px;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #606266;
      }
      .page-counter {
        font-size: 16px;
        font-weight: 600;
        color: #606266;
        min-width: 60px;
        text-align: center;
      }
      .settings-panel {
        width: 420px;
        display: flex;
        flex-direction: column;
      }
      .apply-btn {
        margin-top: auto;
        display: flex;
        justify-content: flex-start;
        gap: 10px;
      }
      /* 预览大图弹窗样式 */
      .preview-dialog-content {
        display: flex;
        flex-direction: column;
        gap: 20px;
      }
      /* 新增:预览导航样式 */
      .preview-navigation {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 30px;
        margin-bottom: 20px;
        padding: 10px;
        background: #f8f9fa;
        border-radius: 8px;
      }
      .nav-btn {
        display: flex;
        align-items: center;
        gap: 8px;
        color: #606266;
        cursor: pointer;
        user-select: none;
      }
      .nav-btn:disabled {
        color: #c0c4cc;
        cursor: not-allowed;
      }
      .nav-btn:not(:disabled):hover {
        color: #409eff;
      }
      .page-indicator {
        font-size: 16px;
        font-weight: 600;
        color: #606266;
        min-width: 150px;
        text-align: center;
      }
      .large-preview-container {
        display: flex;
        justify-content: center;
        align-items: center;
        background: #f8f9fa;
        border-radius: 8px;
        padding: 20px;
        min-height: 500px;
        position: relative;
      }
      .large-preview-image {
        max-width: 100%;
        /* max-height: 70vh; */
        width: auto;
        height: 70vh;/* 原为auto */
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        object-fit: contain;
        cursor: pointer; /* 添加点击手势 */
        transition: transform 0.3s ease;
      }
      .large-preview-image:hover {
        transform: scale(1.01);
      }
      .large-preview-watermark-badge {
        position: absolute;
        top: 20px;
        left: 20px;
      }
      .preview-info {
        padding: 15px;
        background: #e7f3ff;
        border-radius: 6px;
      }
      .preview-info p {
        margin: 5px 0;
        color: #606266;
      }
      .preview-dialog-footer {
        display: flex;
        justify-content: center;
        gap: 15px;
      }
      :deep(.el-dialog__body) {
        padding: 20px;
      }
      /* 优化弹窗样式 */
      :deep(.el-dialog) {
        max-width: 90%;
        margin-top: 5vh !important;
      }
      :deep(.el-dialog__header) {
        padding: 20px 20px 10px;
      }
      :deep(.el-dialog__title) {
        font-size: 18px;
        font-weight: bold;
      }
      /* 添加loading样式 */
      .loading-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 70vh;
        color: #909399;
      }
      .loading-icon {
        font-size: 40px;
        margin-bottom: 16px;
        animation: rotating 2s linear infinite;
      }
      .loading-text {
        font-size: 14px;
      }
      @keyframes rotating {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(360deg);
        }
      }
      /* 确保预览容器有合适的高度 */
      .preview-container,
      .large-preview-container {
        min-height: 400px;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    </style>
  (2)使用组件
    A、使用方式1
      <PdfThumbnail 
        :url="uploadForm.base64Image" 
        :isCheckbox="false"
      />
      const isDev = process.env.NODE_ENV === 'development';
      const http2 = isDev ? 'http://10.71.33.88/'(也可以是'abcd') : '/'; 
      uploadForm.base64Image = http2 + res.data.fullUrl;
    B、使用方式2
      <div class="describe" v-show="!item.load">
        <PdfThumbnail 
          :url="item.base64Image" 
          :ref="(el) => setPdfRef(item.sid, el)"
          :name="'第'+(dataInfo.info.length - index)+'次频道换证证照'" 
          :title=" '广播电视频道许可证-' + item.channelName " 
          :isCheckbox="false"
        />
        <div style="padding-top: 4px">
          <el-button type="primary" link @click="getPDF(item.sid)">下载证照</el-button>
        </div> 
      </div>
      import PdfThumbnail from '../base/pdf.vue'
      //存储所有PDF组件实例的Map
      const pdfRefs = ref(new Map())
      //设置ref的函数
      const setPdfRef = (sid, el) => {
        if (el) {
          pdfRefs.value.set(sid, el)
        } else {
          pdfRefs.value.delete(sid)
        }
      }
      //下载PDF
      const getPDF = (sid) => {
        const pdfComponent = pdfRefs.value.get(sid)
        if (pdfComponent && typeof pdfComponent.getWatermarkedPDF === 'function') {
          pdfComponent.getWatermarkedPDF()
        } else {
          console.error('PDF组件未找到或getWatermarkedPDF方法不存在')
        }
      }
    C、使用方式3
      <!-- 查看-待审核 -->
      <PdfThumbnail
        v-if="isDetail && appInfo.status==0"  
        ref="pdfThumbnailRef"
        :url="appInfo.licenses[0].base64Image"
        :isCheckbox="false"
      />
      <!-- 查看-已批准 -->
      <PdfThumbnail
        v-else-if="isDetail && appInfo.status==1"  
        ref="pdfThumbnailRef"
        :url="appInfo.licenses[0].base64Image"
        :isText="false"
        :isCheckbox="false"
      />
      <!-- 查看-已拒绝 -->
      <PdfThumbnail
        v-else-if="isDetail && appInfo.status==2"  
        ref="pdfThumbnailRef"
        :url="appInfo.licenses[0].base64Image"
        :isCheckbox="false"
      />
      <!-- 审核 -->
      <PdfThumbnail
        v-else-if="!isDetail && appInfo.status==0"  
        ref="pdfThumbnailRef"
        :url="appInfo.licenses[0].base64Image"
        @watermarkUpdate="updateWatermark"
        :isText="false"
      />//pdf组件分为页面展示和弹窗展示两部分组成,隐现弹窗时弹窗没有销毁,以前的弹窗数据仍然保留
      //以下pdf相关!!!
      import PdfThumbnail from '../base/pdf.vue'
      //获取子组件实例
      const pdfThumbnailRef = ref(null)
      //计算属性
      const hasSelectedPages = computed(() => {
        return pdfThumbnailRef.value?.selectedPagesCount > 0
      })
      //打开水印弹窗
      const openAddWatermark = () => {
        if (pdfThumbnailRef.value) {
          pdfThumbnailRef.value.openWatermarkDialog()
        }
      }
      const hasWatermarkedSelected = computed(() => {
        return watermarkedSelectedCount.value > 0
      })
      const watermarkedSelectedCount = computed(() => {
        if (!pdfThumbnailRef.value) return 0
        const selectedPages = pdfThumbnailRef.value.getSelectedPages?.() || []
        const watermarkedPages = pdfThumbnailRef.value.getWatermarkedPages?.() || []
        // 找出既被选中又有水印的页面
        return selectedPages.filter(page => watermarkedPages.includes(page)).length
      })
      const updateWatermark = (imageURL) => {//更新水印
        showEditWatermark.value = false;
        let file = { raw: dataURLtoFile(imageURL, '已添加水印.pdf') }
        uploadFileTool(file,'license').then(res => { //上传水印图片
          checkAppForm.value.licenses[0].fileId = res.data.fileInfoId;
        })
      }
      function dataURLtoFile(dataUrl, filename) { 
        var arr = dataUrl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n);
        }
        return new File([u8arr], filename, { type: mime });
      }
  
  

  

posted @ 2020-10-26 23:40  WEB前端工程师_钱成  阅读(9163)  评论(0)    收藏  举报