Element Plus 开发记录

目录

Dialog 对话框

点击弹窗以外的区域不自动关闭弹窗

1
2
3
4
5
6
7
<el-dialog
  :close-on-click-modal="false"
  v-model="dialogVisible"
  title="Tips"
  width="500"
  :before-close="handleClose"
>

点击 x 执行的方法、点击 空白处 执行的方法

1
2
3
4
5
6
7
<el-dialog
  :close-on-click-modal="false"
  v-model="dialogVisible"
  title="Tips"
  width="500"
  :before-close="handleClose"
>

el-dialog 存在 el-table,每次打开弹窗,el-table都会变一下

这个问题可能是由于每次打开弹窗时,你都在重新渲染el-table组件,导致它的状态(如展开的行、选中的行等)发生变化。为了避免这种情况,可以通过以下方法来确保 el-table 的状态稳定:
使用 v-if 而不是 v-show 来控制 el-dialog 的显示,这样在对话框显示时才会渲染表格。
使用 v-if 来控制 el-table 的渲染,这样每次对话框打开时才会重新渲染表格。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
  <el-dialog v-model="dialogVisible">
    <el-table
      v-if="dialogVisible"
      :data="tableData"
    >
      <!-- 你的表格内容 -->
    </el-table>
  </el-dialog>
</template>

Popconfirm 气泡确认框

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<el-table-column fixed="right" label="操作">
  <template #default="scope">
    <el-button type="primary" link @click="getPipeline(scope.row)">编辑</el-button>
    
    <el-popconfirm title="确定删除吗?" confirm-button-text="确定" cancel-button-text="取消" @confirm="delPipeline(scope.row)">
      <template #reference>
        <el-button type="danger" link>删除</el-button>
      </template>
    </el-popconfirm>

  </template>
</el-table-column>

Table 表格

设置列是否固定在表格的左侧或右侧

组件的 fixed 属性用于设置列是否固定在表格的左侧或右侧。当你设置 fixed="right" 时,该列会被固定在表格的最右侧,即使用户滚动表格的水平滚动条,这一列也会保持可见。

fixed 属性的选项:

  • 不设置:列不固定,随着滚动而移动。
  • fixed="left":列固定在表格的左侧。
  • fixed="right":列固定在表格的右侧。
1
2
3
4
5
6
<el-table-column fixed="right" label="操作">
  <template #default="scope">
    <el-button type="primary" link @click="getPipeline(scope.row)">编辑</el-button>
    <el-button type="danger" link @click="delPipeline(scope.row)">删除</el-button>
  </template>
</el-table-column>

展开行

该行数据没有子数据,不展示下拉图标

1.在 table 表格属性新增 "row-class-name"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<el-table
  :data="tableData"
  style="width: 100%"
  :row-class-name="getRowClass"
>
  <el-table-column type="expand">
    <template #default="props">
      <div>
        <el-table :data="props.row.family">
          <el-table-column label="Name" prop="name" />
          <el-table-column label="State" prop="state" />
          <el-table-column label="City" prop="city" />
          <el-table-column label="Address" prop="address" />
          <el-table-column label="Zip" prop="zip" />
        </el-table>
      </div>
    </template>
  </el-table-column>
  <el-table-column label="Date" prop="date" />
  <el-table-column label="Name" prop="name" />
</el-table>

2.js 代码新增方法 "getRowClass"

1
2
3
4
5
6
7
8
<script>
  //隐藏展开箭头
  const getRowClass = (row) => {
    if (row.row.family.length === 0) {
      return 'row-expand-cover';
    }
  },
</script>

3.增加 css 样式,如果不生效,前面增加 vue 样式穿透 /deep/ ::v-deep
vue3 中使用 ::v-deep

1
2
3
4
5
<style>
  ::v-deep .row-expand-cover td .el-table__expand-icon {
    visibility: hidden;
  }
</style>

标题和数据居中

1
2
3
4
5
6
7
<el-table :data="documentData">
  <el-table-column prop="id" label="文档id" header-align="center" align="center">
    <template #default="scope">
      {{ scope.row.id }}
    </template>
  </el-table-column>
</el-table>

Element UIel-table-column 组件提供了 header-alignalign 属性,可以直接用于设置表头和单元格文本的对齐方式。将这两个属性都设置为 center 即可实现居中对齐。

Upload 上传

可以使用 :http-request="" 来覆盖组件默认的 action="" 方法

拖拽上传

uploadUrl:上传的地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<template>
  <!-- 上传文件弹窗 -->
  <el-dialog
    title="上传文档"
    v-model="fileDialogVisible"
    width="30%"
    :before-close="fileHandleClose"
    :close-on-click-modal="false"
  >
    <div>
      <el-upload
        class="upload-demo"
        drag
        multiple
        :action="uploadUrl"
        :show-file-list="true"
        accept=".txt, .md, .pdf, .html, .xlsx, .xls, .docx, .doc, .csv"
        :data="uploadParams"
        :before-upload="beforeUpload"
        ref="myUpload"
      >
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">
          将文件拖到此处,或<em>点击上传</em>
        </div>
        <div class="el-upload__tip">
          已支持 TXT、 MARKDOWN、 PDF、 HTML、 XLSX、 XLS、 DOCX、
          CSV,每个文件不超过 15MB。
        </div>
      </el-upload>
    </div>
  </el-dialog>
</template>

<script>
  export default {
    setup() {
      const myUpload = ref({})

      //上传时携带的参数
      const uploadParams = ref({
        id: "",
      });

      //上传之前进行文件验证
      const beforeUpload = (file) => {
        var testmsg = file.name.substring(file.name.lastIndexOf(".") + 1);
        let arr = ["txt", "md", "pdf", "html", "xlsx", "xls", "docx", "doc", "csv"];
        if (arr.indexOf(testmsg) === -1) {
          ElMessage.error(file.name + "的文件格式不符")
          return false;  //return false 不符后,不调用接口
        }
        const isLt15M = file.size / 1024 / 1024 < 15;
        if (!isLt15M) {
          ElMessage.error("上传文件大小不能超过 15MB")
          return false;
        }
        return true;
      };

      //上传文件弹窗-"×"号
      const fileHandleClose = () => {
        fileDialogVisible.value = false;
        //清空上传文件列表
        myUpload.value.clearFiles()
      };
    }
  }
</script>

照片墙

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
  <el-dialog
      v-model="imageDialog"
      title="图片管理"
      width="600px"
      :before-close="handleCloseImage"
      :close-on-click-modal="false"
    >
      <el-upload
        v-model:file-list="fileList"
        :action="uploadImage"
        :headers="data.hearders"
        list-type="picture-card"
        :on-preview="handlePictureCardPreview"
        :on-remove="handleRemove"
        :on-success="handleSuccess"
        :data="upParam"
        accept=".jpg,.png"
      >
        <!-- "+" 号图标 -->
        <i class="iconfont icon-jiahao"></i>
      </el-upload>
    
      <el-dialog v-model="imageDialogVisible">
        <img w-full class="preview-img" :src="dialogImageUrl" alt="图片预览" />
      </el-dialog>
  
  </el-dialog>
</template>
<script>
export default {
  setup() {
    const data = reactive({
      //请求头,请求时携带token
      hearders: {
        "jwttoken":window.localStorage.getItem("token")
      },
    });

    //图片管理弹窗
    const imageDialog = ref(false);
    //图片管理弹窗中图片集合
    const fileList = ref([])
    //预览图片的弹窗
    const imageDialogVisible = ref(false)
    //预览图片的值
    const dialogImageUrl = ref('')
    //图片管理弹窗-上传图片 携带的参数
    const upParam = ref({
      documentId: "",
    });

    //图片管理-删除图片
    const handleRemove = (uploadFile, uploadFiles) => {
      console.log(uploadFile, uploadFiles)
    }

    //图片管理-预览图片
    const handlePictureCardPreview = (uploadFile) => {
      dialogImageUrl.value = uploadFile.url
      imageDialogVisible.value = true
    }

    //图片管理
    const imageManage = (row) => {
      imageDialog.value = true;

      //回显图片
      let pictureArr = row.picture
      const newFile = {
        name: xxx,
        url: xxx,
      };
      fileList.value.push(newFile);
    }

    //图片管理上传图片成功是调用的方法
    const handleSuccess = () => { 

    }

    return {
      data,
      imageDialog,
      fileList,
      imageDialogVisible,
      dialogImageUrl,
      upParam,
      handleRemove,
      handlePictureCardPreview,
      imageManage,
      handleSuccess
    }
  }
}
</script>

在预览图片后,会出现 press delete to remove 的字样,使用下面的样式隐藏

/images/posts/ElementPlus开发记录/1.jpg
(图1)
1
2
3
4
5
<style lang="scss">
.el-icon--close-tip{
  display: none !important;
}
</style>

组件的失败回调函数(on-error)里拿不到返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
  setup() {
    const uploadFileError = (error) => {
      let myError=error.toString();//转字符串
      myError=myError.replace("UploadAjaxError: ","")//去掉前面的
      myError=JSON.parse(myError);//转对象

      ElMessage({
        message: myError.message,
        type: "error",
      });
    }

    return {
      uploadFileError,
    }
  }
}
</script>
/images/posts/ElementPlus开发记录/2.jpg
(图2)

手动调用上传文件的接口

场景:聊天页面发送图片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<template>
    <el-dialog
    v-model="imageDialog"
    :title="'发送给'+accountInfo.name"
    width="600px"
    :before-close="handleCloseImage"
    :close-on-click-modal="false"
  >
    <el-upload
      v-model:file-list="fileList"
      list-type="picture-card"
      :on-preview="handlePictureCardPreview"
      :auto-upload="false" 
      :action="''"
    >
      <i class="iconfont icon-jiahao"></i>
    </el-upload>
    <!-- 预览弹窗 -->
    <el-dialog v-model="imagePreviewDialog">
      <img w-full class="preview-img" :src="dialogPreviewImage" alt="图片预览" style="width: 100%;height: 100%"/>
    </el-dialog>

      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCloseImage()">取消</el-button>
          <el-button type="primary" @click="sendImage()">发送</el-button>
        </div>
      </template>
  </el-dialog>
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    const imageDialog = ref(false)  //图片弹窗
    const fileList = ref([])  //图片弹窗中图片集合
    const imagePreviewDialog = ref(false) //预览图片弹窗
    const dialogPreviewImage = ref('')  //预览图片

    //图片弹窗-预览
    const handlePictureCardPreview = (uploadFile) => { 
      dialogPreviewImage.value = uploadFile.url
      imagePreviewDialog.value = true
    }

    //图片弹窗-点叉号
    const handleCloseImage = () => {
      imageDialog.value = false
      fileList.value = []
    }

    //图片弹窗-发送
    const sendImage = () => {
      let formData = new FormData();
      
      fileList.value.forEach(file => {
        
        console.log("遍历文件:", file);
        console.log("文件类型:", typeof file.raw);

        if (file.raw instanceof File) {
          formData.append('files', file.raw);
        } else {
          console.error("文件对象不是 File 类型:", file.raw);
        }
      });
      
      // 调试 FormData 内容
      for (let pair of formData.entries()) {
        console.log(pair[0] + ', ' + pair[1]);
      }
    }
  }
}
</script>

照片墙上传视频

下面的案列没有写回显的相关代码,适用的场景是聊天界面发送 图片/视频,如果需要回显,请结合代码进行修改

/images/posts/ElementPlus开发记录/3.jpg
(图3)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<template>
  <p @click="imageDialog = true">图片/视频</p>

   <el-dialog
    v-model="imageDialog"
    :title="'发送给'+accountInfo.name"
    width="600px"
    :before-close="handleCloseImage"
    :close-on-click-modal="false"
  >
    <el-upload
      v-model:file-list="fileList"
      list-type="picture-card"
      :on-preview="handlePictureCardPreview"
      :auto-upload="false" 
      :action="''"
      :on-change="handleChange"
    >
      <i class="iconfont icon-jiahao"></i>
    </el-upload>
    <!-- 预览弹窗 -->
    <el-dialog v-model="imagePreviewDialog">
      <img v-if="isImage" w-full class="preview-img" :src="dialogPreviewImage" alt="图片预览" style="width: 100%;height: 100%"/>
      <video v-else :src="dialogPreviewVideo" style="width: 100%; height: auto;" controls="controls"></video>
    </el-dialog>

      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCloseImage()">取消</el-button>
          <el-button type="primary" @click="sendImage()">发送</el-button>
        </div>
      </template>
  </el-dialog> 
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    const imageDialog = ref(false)  //弹窗
    const fileList = ref([])  //图片、视频文件的集合
    const imagePreviewDialog = ref(false) //预览弹窗
    const dialogPreviewImage = ref('')  //预览图片
    const dialogPreviewVideo = ref('')  //预览视频
    const isImage = ref()

    //图片/视频 变动时调用的方法,手动上传 before-upload 无效
    const handleChange = (file) => {
      if (file.raw.type.startsWith('video/')) {
        getVideoThumbnail(file.raw).then(thumbnail => {
          console.log('视频封面 URL:', thumbnail);  //是一个base64编码,需要转换形式
          let blob = dataURLtoBlob(thumbnail);
          file.url = URL.createObjectURL(blob); //视频封面的 URL
          file.videoUrl = URL.createObjectURL(file.raw); //视频的 URL
          // 手动更新 fileList (这个 fileList 是我定义的 fileList,不是 handleChange 方法传过来的 handleChange)
          fileList.value = [...fileList.value];  // 重新触发 Vue 响应式更新
        });
      }
      
    };
    

  function dataURLtoBlob(dataurl) {
    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 Blob([u8arr], {type:mime});
  }

  // 获取视频封面
  const getVideoThumbnail = (file) => {
    return new Promise((resolve, reject) => {
      const video = document.createElement('video');
      video.preload = 'metadata';
      video.src = URL.createObjectURL(file);
      video.onloadedmetadata = () => {
        // 将视频时间轴移动到一个非零位置,例如 0.1 秒
        video.currentTime = 0.1;

        // 使用 requestAnimationFrame 确保视频帧已准备好
        function drawFrame() {
          if (video.readyState === video.HAVE_ENOUGH_DATA) {
            const canvas = document.createElement('canvas');
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            const context = canvas.getContext('2d');
            context.drawImage(video, 0, 0, canvas.width, canvas.height);
            resolve(canvas.toDataURL('image/jpeg'));
          } else {
            requestAnimationFrame(drawFrame);
          }
        }

        requestAnimationFrame(drawFrame);
      };
      video.onerror = (error) => {
        reject(error);
      };
    });
  };

    //图片弹窗-预览
    const handlePictureCardPreview = (uploadFile) => {
      //预览时,根据文件的type,获取 图片/视频 的url
      if (uploadFile.raw.type.startsWith('video/')) {
        dialogPreviewVideo.value = uploadFile.videoUrl;
        isImage.value = false;
      } else {
        dialogPreviewImage.value = uploadFile.url;
        isImage.value = true;
      }
      imagePreviewDialog.value = true
    }

    //图片弹窗-点叉号
    const handleCloseImage = () => {
      imageDialog.value = false
      fileList.value = []
    }

    //图片弹窗-发送
    const sendImage = () => { 
      console.log("fileList:",fileList.value)
      let formData = new FormData();
      fileList.value.forEach(file => {
        console.log("遍历:",file.raw)
        formData.append('files', file.raw);
      });
      console.log("图片:",formData)

        for (let pair of formData.entries()) {
          console.log(pair[0] + ', ' + pair[1]);
        }
    }
  }
}
</script>

组件之间传值

子组件传父组件

子组件 BotForm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script>
import { onMounted, watch } from "vue";
export default {
  name: "botForm",
  props: {
    data: {
      name: String,
      botSetting: String,
      welcome: String,
      reply: String,
      knowBases: Array,
      default: () => {},
    },
  },
  //子组件通过 { emit } 向父组件传数据
  setup(props, { emit }) {
    onMounted(() => {});

    //监听父组件传递的数据
    watch(
      () => props.data,
      (n) => {
        if (n) {
          console.log("父组件中的信息:", n);
        }
      }
    );

    //子组件向父组件发送数据
    const sonToFather = () => {
      let obj = {};
      emit("updateReply", obj);
    };

    return {
      sonToFather,
    };
  },
};
</script>

父组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<template>
  <div class="container">
    <div class="header">
      <div class="main-top"></div>
    </div>
    <div class="main">
      <div class="main-left">
        <!-- 子组件 BotForm、BotChat -->
        <!-- 父组件通过在 <bot-form>、<bot-chat> 中添加 :data="botInfo" 将数据传递给子组件 -->
        <!-- 父组件通过 @updateReply="getResults" 接收子组件传递的数据 -->
        <bot-form :data="botInfo" @updateReply="getResults"></bot-form>
      </div>
      <div class="main-right">
        <bot-chat :data="botInfo"></bot-chat>
      </div>
    </div>
  </div>
</template>
<script>
import { onMounted, ref } from "vue";
//引入子组件
import BotForm from "@/components/BotForm.vue";
import BotChat from "@/components/BotChat.vue";
export default {
  //引入子组件
  components: {
    BotForm,
    BotChat,
  },
  setup() {
    const botInfo = ref({});

    onMounted(() => {
      initializationMethod();
    });

    //初始化
    const initializationMethod = () => {
      //调用接口,获取数据,将数据赋值给 botInfo
      botInfo.value = res.data;
      //父组件通过在 <bot-form>、<bot-chat> 中添加 :data="botInfo" 将数据传递给子组件
      //为了防止父组件数据发生改变,而子组件没有监听到,添加 .timer
      botInfo.value.timer = new Date().getTime();
    };

    //获取子组件 BotForm 传递的数据
    const getResults = (e) => {
      console.log("子组件 BotForm 传递的数据:", e);
      //使用子组件的数据更新 botInfo
      botInfo.value = e;
      //为了防止父组件数据发生改变,而子组件没有监听到,添加 .timer
      botInfo.value.timer = new Date().getTime();
    };

    return {
      initializationMethod,
      getResults,
    };
  },
};
</script>

父组件传子组件

父组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
  <div class="container">
    <div class="header">
      <div class="main-top"></div>
    </div>
    <div class="main">
      <div class="main-left">
        <!-- 子组件 BotForm、BotChat -->
        <bot-form :data="botInfo" @updateReply="getResults"></bot-form>
      </div>
      <div class="main-right">
        <bot-chat :data="botInfo"></bot-chat>
      </div>
    </div>
  </div>
</template>
<script>
import { onMounted, ref } from "vue";
import BotForm from "@/components/BotForm.vue";
import BotChat from "@/components/BotChat.vue";
export default {
  components: {
    BotForm,
    BotChat,
  },
  setup() {
    const botInfo = ref({});

    onMounted(() => {
      initializationMethod();
    });

    //初始化
    const initializationMethod = () => {
      //调用接口,获取数据,将数据赋值给 botInfo
      botInfo.value = res.data;
      //父组件通过在 <bot-form>、<bot-chat> 中添加 :data="botInfo" 将数据传递给子组件
      //为了防止父组件数据发生改变,而子组件没有监听到,添加 .timer
      botInfo.value.timer = new Date().getTime();
    };

    //获取子组件 BotForm 传递的数据
    const getResults = (e) => {
      console.log("子组件 BotForm 传递的数据:", e);
      //使用子组件的数据更新 botInfo
      botInfo.value = e;
      //为了防止父组件数据发生改变,而子组件没有监听到,添加 .timer
      botInfo.value.timer = new Date().getTime();
    };

    return {
      initializationMethod,
      getResults,
    };
  },
};
</script>

子组件 BotChat

如果父组件添加了 new Date().getTime() 后,子组件的检测不到内部数据,这是因为默认情况下,watch 对于对象和数组是进行浅层监听的。这意味着它只会监听对象的直接属性变化,而不是嵌套对象或数组内部的变化,如果需要监听到嵌套对象或数组内部的变化,可以在定义 watch 时指定 { deep: true } 选项,以启用深层监听。这样,任何嵌套对象的变化都会被监听到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
  watch(() => props.data, (newVal) => {
    if (newVal) {
      nextTick(() => {
        form.id = newVal.id;
        form.userType = String(newVal.userType);
        form.useCounts = newVal.useCounts;
        // 根据需要添加其他字段
      })
    }
  }, { deep: true })  // 启用深层监听
<script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
import { ref, onMounted, watch } from "vue";
export default {
  props: {
    data: {
      name: String,
      botSetting: String,
      welcome: String,
      knowBases: Array,
      default: () => {},
    },
  },
  //子组件通过 props 接收父组件的数据
  setup(props) {
    const setData = ref("");

    onMounted(() => {});

    //监听父组件的数据
    watch(
      //( ) => props.data.timer:这是一个箭头函数,它返回 props.data.timer 的值。这意味着我们要观察 props.data.timer 的变化
      () => props.data.timer,
      //(n) => { ... }:这是一个回调函数,它接受一个参数 n,这个参数代表了 props.data.timer 的新值(也就是变化后的值)
      (n) => {
        console.log("时间:", n);
        //父组件的数据
        setData.value = props.data;
      }
    );

    return {};
  },
};
</script>

页面之间传值

A 页面跳转到 B 页面时,将 A 页面中的参数携带到 B 页面

但是这种方式存在一个问题,参数是在地址栏中通过 ? 在后面拼接,不安全

A.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
import { useRouter } from 'vue-router';
export default {
  setup() {
    const router =useRouter()

    const getKBDocument = (row) => { 
        router.push({path: "/userKBDocument", query: {kbId: row.id}})
    }
  }
}
</script>
B.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
import { useRoute } from 'vue-router';
export default {
  setup() {
    const route = useRoute()

    onMounted(() => {
      kbId.value = route.query.kbId
    });
  }
}
</script>

Switch 开关

el-table 表格里使用 el-switch 开关按钮

active-valueinactive-value 属性,接受 Boolean、StringNumber 类型的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<el-table-column prop="active" label="状态">
  <template #default="scope">
    <!--当scope.row.active=1时switch开启,当scope.row.active=0时switch关闭-->
    <el-switch
      inline-prompt
      active-text="启用"
      :active-value=1
      style="--el-switch-on-color: #13ce66; --el-switch-off-color: #9c9c9c"
      inactive-text="停用"
      :inactive-value=0
      v-model="scope.row.active"
      @change="handleStatusChange(scope.row)"
    />
  </template>
</el-table-column>

<script>
  function handleStatusChange(row) {
    console.log(row);
  }
</script>

Form 表单

样例

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
<template>
  <div class="container">
    <div class="list-box">
      <div class="title">流水线管理</div>

      <div class="table-div">
        <div><el-button type="primary" @click="addPopup()">新增</el-button></div>

        <el-table :data="tableData" style="width: 100%" v-loading="loading">
          <el-table-column prop="id" label="ID">
            <template #default="scope">
              {{ scope.row.id }}
            </template>
          </el-table-column>
          <el-table-column prop="name" label="名称">
            <template #default="scope">
              {{ scope.row.name }}
            </template>
          </el-table-column>
          <el-table-column prop="params" label="内容">
            <template #default="scope">
              {{ scope.row.params }}
            </template>
          </el-table-column>
          <el-table-column prop="components" label="组件">
            <template #default="scope">
              {{ scope.row.components }}
            </template>
          </el-table-column>
          <el-table-column prop="process" label="流程">
            <template #default="scope">
              {{ scope.row.process }}
            </template>
          </el-table-column>
          <el-table-column prop="created_at" label="创建时间">
            <template #default="scope">
              {{ getDate(scope.row.created_at) }}
            </template>
          </el-table-column>
          <el-table-column fixed="right" prop="address" label="操作">
            <template #default="scope">
              <el-button type="primary" link @click="getPipeline(scope.row)">编辑</el-button>
              
              <el-popconfirm title="确定删除吗?" confirm-button-text="确定" cancel-button-text="取消" @confirm="delPipeline(scope.row)">
                <template #reference>
                  <el-button type="danger" link>删除</el-button>
                </template>
              </el-popconfirm>
          
            </template>
          </el-table-column>
        </el-table>

      </div>
      <div class="page-box">
        <el-pagination
          background 
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="currentPage"
          :page-sizes="pageSizes"
          :page-size="pageSize"
          layout="prev, pager, next" 
          :total="total"
        />
      </div>
    </div>
  </div>

  <!--流水线弹窗-->
  <el-dialog
        :title="isEdit?'编辑流水线':'添加流水线'"
        v-model="dialogVisible"
        width="600px"
      >
        <el-form
          label-width="auto"
          :model="form"
          :rules="rules"
          ref="myForm"
          v-loading="loading"
          label-position='top'
          width="100%"
        >
          <el-form-item label="流水线名称:" prop="name">
            <el-input v-model="form.name" placeholder="请输入流水线名称" />
          </el-form-item>
          
          <div class="form-item-json">
            <el-form-item label="流水线内容" required>
              <div class="form-item-list" v-for="(item,index) in form.param" :key="index">
                  <div class="form-item-list-left">
                    参数名:
                    <el-input v-model="item[0]" @input="updateInputKey(index,$event)" />
                  </div>
                  <div>
                    参数值:
                    <el-input v-model="item[1]" @input="updateInputValue(index,$event)"/>
                  </div>
                  <div class="item-delete">
                    <el-button link type="danger" @click="deleteOneParam(item)">删除</el-button>
                  </div>
              </div>

              <div>
                <el-button @click="addParam()">添加参数</el-button>
              </div>
            </el-form-item>

            <el-form-item label="流水线组件:" prop="components">
              <el-input
                v-model="form.components"
                placeholder="请输流水线组件,例如:text_embedder,retriever"
                maxlength="2000"
                type="textarea"
                show-word-limit
              />
            </el-form-item>
            <el-form-item label="流水线流程:" prop="process">
              <el-input
                v-model="form.process"
                placeholder="请输入流水线流程"
                maxlength="2000"
                type="textarea"
                show-word-limit
              />
            </el-form-item>

          </div>
        </el-form>

        <template #footer>
          <div class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary" @click="isEdit ? submitUpdateForm(myForm) : submitAddForm(myForm)">
              保存
            </el-button>
          </div>
        </template>
      </el-dialog>

</template>
<script>
import { ref, onMounted, reactive } from "vue";
import { landRelevant } from "@/api/api.js";
import { ElMessage } from "element-plus";
import { getDate } from '@/utils/index';
export default {
  name: "pipLineMange",
  
  setup() {
    const form = reactive({
      id: "",
      name: "",
      param: [],
      process: "",
      components: "",
    });
    const isEdit = ref(false)
    const myForm = ref(null);
    const rules = reactive({
      name: [{ required: true, message: "请填写流水线名称", trigger: "blur" }],
      components: [{ required: true, message: "请填写流水线名称", trigger: "blur" }],
      process: [{ required: true, message: "请填写流水线流程", trigger: "blur" }],
    });
    const loading = ref(false);
    const listData = ref([]);
    const dialogVisible = ref(false);
    //分页
    const currentPage = ref(1);
    const pageSizes = [5, 10, 20, 30, 50];
    const pageSize = ref(10);
    const total = ref(0);
    const tableData = ref([]);
    const disStart1=ref(false);
    const isStartParams=ref([])

    onMounted(() => {
      pipelineList()
    });

    //创建流水线
    const submitAddForm = async (myForm) => {
      await myForm.validate((valid, fields) => {

        loading.value = true;
        if (valid) {
          let obj
          if(form.param.length>0){
            //数组转对象
            obj = form.param.reduce((accumulator, [key, value]) => {
              //key和value都有值,才进行存储
              if (key && value) { 
                accumulator[key] = value; // 保持 value 的原始类型
              }
              return accumulator;
            }, {});
            obj = JSON.stringify(obj)
          }

          let params = {
            name: form.name,
            param: obj,
            components: form.components,
            process: JSON.parse(form.process),
          };
          landRelevant.createPipeline(params).then((res) => {
            if (res.statusCode == 200) {
              dialogVisible.value = false
              //流水线列表
              pipelineList()
              ElMessage({
                message: '创建成功',
                type: 'success',
              })
            } else {
              ElMessage({
                message: res.message,
                type: "error",
              });
            }
            loading.value = false;
          }).catch((e) => console.log("request accounts ERR", e));

          form.name = ''
          form.param = []
          form.components = ''
          form.process = ''

        } else {
          console.log(fields, "--fields-");
          loading.value = false;
        }
      });
    }

    //打开流水线弹窗
    const addPopup = () => { 
      isEdit.value = false

      form.id = ''
      form.name = ''
      form.param = []
      form.components = ''
      form.process = ''

      // 重置表单验证状态
      if (myForm.value) {
        myForm.value.resetFields();
      }
      
      dialogVisible.value = true
    }

    //流水线列表
    const pipelineList = () => { 
      let params = {
        pageNo: currentPage.value,
        pageSize: pageSize.value,
        data: {},
      };
      landRelevant.pipelineList(params).then((res) => {
        if (res.statusCode == 200) {
          tableData.value = res.data
          total.value = res.page.total
        } else {
          if (res.message == "Checked in today") {
            ElMessage({
              message: "Checked in today!!",
              type: "warning",
            });
          }
        }
      }).catch((e) => console.log("request accounts ERR", e));
    }

    //每页条数改变
    const handleSizeChange = (val) => {
      currentPage.value = 1
      pageSize.value = val
      pipelineList()
    }

    //页码变化
    const handleCurrentChange = (val) => {
      currentPage.value = val
      pipelineList()
    }

    //删除流水线
    const delPipeline = (row) => { 
      landRelevant.delPipeline(row.id).then((res) => {
        if (res.statusCode == 200) {
          ElMessage({
            message: '删除成功',
            type: 'success',
          })
          //流水线列表
          pipelineList()
        } else {
          if (res.message == "Checked in today") {
            ElMessage({
              message: "Checked in today!!",
              type: "warning",
            });
          }
        }
      }).catch((e) => console.log("request accounts ERR", e));
    }

    //编辑流水线(回显数据)
    const getPipeline = (row) => {
      isEdit.value = true
      //重置表单验证状态
      if (myForm.value) { 
        myForm.value.resetFields();
      }

      landRelevant.getPipeline(row.id).then((res) => {
        if (res.statusCode == 200) {
          form.id=res.data?.id
          form.name=res.data?.name
          form.components=res.data?.components
          form.process=JSON.stringify(res.data?.process)

          //form.param
          const params= res.data?.params
          if( params !==undefined && params !==null){
              const arrFromObj = Object.entries(params);
              form.param=arrFromObj
          }else{
              form.param=[]
          }

          dialogVisible.value = true
        } else {
          if (res.message == "Checked in today") {
            ElMessage({
              message: "Checked in today!!",
              type: "warning",
            });
          }
        }
      }).catch((e) => console.log("request accounts ERR", e));
    }


    //更新流水线
    const submitUpdateForm = async (myForm) => {
      await myForm.validate((valid, fields) => {
        loading.value = true;
        if (valid) {
          let obj
          if(form.param.length>0){
            //数组转对象
            obj = form.param.reduce((accumulator, [key, value]) => {
              //key和value都有值,才进行存储
              if (key && value) { 
                accumulator[key] = value; // 保持 value 的原始类型
              }
              return accumulator;
            }, {});
            obj = JSON.stringify(obj)
          }

          let params = {
            id: form.id,
            name: form.name,
            param: obj,
            components: form.components,
            process: JSON.parse(form.process),
          };
          landRelevant.updatePipeline(params).then((res) => {
            if (res.statusCode == 200) {
              dialogVisible.value = false

              //流水线列表
              pipelineList()
              ElMessage({
                message: '更新成功',
                type: 'success',
              })
            } else {
              ElMessage({
                message: res.message,
                type: "error",
              });
            }
            loading.value = false;
          }).catch((e) => console.log("request accounts ERR", e));
        } else {
          console.log(fields, "--fields-");
          loading.value = false;
        }
      });
    }

    //更新输入
    const updateInputKey =(index,newValue)=>{
      form.param[index][0]=newValue;
    }
      
    const updateInputValue=(index,newValue)=>{
      form.param[index][1]=newValue;
    }

    //删除一个参数
    const deleteOneParam=(item1)=>{
      let params=form.param
  
      form.param=params.filter(item=>{
          if(item[0]!==item1[0] && item[1]!==item1[1]){
              return item
          }
      })
    }
    
    //添加一个参数
    const addParam=()=>{
      form.param.push(['',''])
    }   


    return {
      form,
      myForm,
      disStart1,
      rules,
      loading,
      listData,
      dialogVisible,
      currentPage,
      pageSizes,
      pageSize,
      total,
      tableData,   
      isStartParams,
      getPipeline,
      submitUpdateForm,
      submitAddForm,
      handleSizeChange,
      handleCurrentChange,
      getDate,
      delPipeline,
      pipelineList,
      updateInputKey,
      updateInputValue,
      deleteOneParam,
      addParam,
      addPopup,
      isEdit,
    };
  },
};
</script>
<style lang="scss" scoped>
.container {
  .list-box {
    background: white;
    padding: 20px;

    .table-div{
      margin-top: 30px;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }

    ::v-deep .form-item-json{
      textarea{
        min-height: 80px !important;
      }
    }

    .title{
      font-size: 20px;
      font-weight: 800;
    }
  }
  .page-box {
    padding: 10px;
    display: flex;
    justify-content: flex-end;
  }
 
}

.form-item-list{
    display: flex;
    .form-item-list-left{
      margin-right: 5px;
    }
    .item-delete{
      display: flex;
      justify-content: center;
    }

  }

</style>

分页

滑动分页

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<template>
  <div class="list-box" @scroll="handleScroll">
    <!-- 会话列表 -->
    <div class="conversation-item-box" v-for="(item, index) in sessionListDate" :key="index" @click="getChatMessages(index, item)" :class="{ selected: selectedIndex === index }">
      <span class="el-avatar el-avatar--icon el-avatar--square item-avatar app-flex-c-c">
        <i class="el-icon">
          <img src="../assets/images/default-avatar.jpg" alt="" style="height: 40px; width: 40px" />
        </i>
      </span>
      <div class="item-right-box">
        <div class="item-info app-flex-c-b">
          <div class="item-nickName app-line-ellipsis" style="width: calc(100% - 70px)">{{ item.name }}</div>
          <div class="item-type app-flex-c">
            <div class="item-type-text"></div>
            <div class="item-type-time">{{ convertRTime(item.record.update_at) }}</div>
          </div>
        </div>
        <div class="item-con app-line-ellipsis">{{ item.record.message }}</div>
      </div>
    </div>

    <div v-if="loading === true && allDataLoaded === false" class="loadingMore-wrap">
      <div>Loading...</div>
    </div>
    <div v-if="allDataLoaded" class="loadingMore-wrap">
      <div>-- 我是有底线的 --</div>
    </div>
  </div>
</template>
<script>
import _ from 'lodash';

export default {
  name: "inviteView",
  setup() {
    const sessionListDate = ref();
    const pageNo = ref(1); // 当前页码
    const pageSize = ref(10); // 每页数据量
    const loading = ref(false); //是否正在进行数据加载
    const allDataLoaded = ref(false); //数据是否都已经加载完毕
    const totalCount = ref(0) //总条数
    const condition = ref('') //查询条件

    //列表滑动事件
    const handleScroll = _.debounce(async (event) => {
      const bottomReached = event.target.scrollHeight - event.target.scrollTop === event.target.clientHeight;
      if (bottomReached) { 
        getWeExternal();
      }
    }, 200);

    // 获取当前客服聊天的客户
    const getWeExternal = () => {
      // 判断是否要加载数据
      // loading:如果当前正在进行数据加载(即 loading.value 为 true),则直接返回
      // allDataLoaded:如果所有数据都已经加载完毕(即 allDataLoaded.value 为 true),则直接返回
      if (loading.value || allDataLoaded.value) return;

      loading.value = true;

      let params = {
        pageNo: pageNo.value,
        pageSize: pageSize.value,
        data: {
          open_kfid: route.query.open_kfid,
          agentsId: route.query.agentsId,
          condition: condition.value,
        }
      };
      landRelevant.getWeExternal(params).then((res) => {
          if (res.statusCode == 200) {
            if (pageNo.value === 1) {
              sessionListDate.value = res.data;
            } else {
              sessionListDate.value = [...sessionListDate.value, ...res.data];
            }
            // 数据的总条数
            totalCount.value = res.page.total
            
            // 默认展示第一个客户的数据
            if (sessionListDate.value.length > 0) {
              selectedIndex.value = 0;
              accountInfo.name = sessionListDate.value[0].name;
              getChatRecord(sessionListDate.value[0].external_userid)
            }

            pageNo.value += 1;  // 页码+1

            // 检查是否已经加载了所有数据
            if (sessionListDate.value.length >= totalCount.value) {
              allDataLoaded.value = true;
            }
          } else {
            ElMessage({
              message: res.message,
              type: "error",
            });
          }
      }).catch((e) => console.log("request accounts ERR", e)).finally(() => { 
        loading.value = false;
      });
    };

    //搜索框回车事件
    const selectByCondition = () => {
      //重置
      pageNo.value = 1
      loading.value = false
      allDataLoaded.value = false
      sessionListDate.value = []

      //再次查询
      getWeExternal()
    }

    //刷新按钮
    const flush = () => { 
      //重置
      pageNo.value = 1
      loading.value = false
      allDataLoaded.value = false
      condition.value = ""
      sessionListDate.value = []

      getWeExternal()
    }

  }
}
</script>

Input 输入框

手机号校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script>
  const checkMobile = (rule, value, callback) => {
    const regMobile = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/;
    if (regMobile.test(value)) {
      // 合法的手机号码
      return callback();
    }
    callback(new Error("手机号码格式不正确"));
  };

  // 表单校验
  const rules = reactive({
   phoneCode: [{ required: true, message: "手机号不能为空", trigger: "blur" },{ validator: checkMobile, trigger: ["blur", "change"] }],
  })
</script>

邮箱校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script>
  const checkEmail = (rule, value, callback) => {
    const mailReg = /([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/
    if (!value) {
      return callback(new Error('邮箱不能为空'))
    }
    if (mailReg.test(value)) {
      callback()
    } else {
      callback(new Error('请输入正确的邮箱格式'))
    }
  };
</script>

密码验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
  // 自定义验证函数 (函数定义放在函数调用之前)
  const validatePassword = (rule, value, callback) => {
    if (value === '') {
      callback(new Error('请填写密码'));
    } else {
      // 密码字段验证通过
      callback();
    }
  };

  const validateConfirmPassword = (rule, value, callback) => {
    if (value === '') {
      callback(new Error('请再次输入密码'));
    } else if (value !== form.password) {
      callback(new Error('两次输入的密码不一致'));
    } else {
      // 确认密码验证通过
      callback();
    }
  };

  const rules = reactive({
    password: [
      { required: true, message: "请填写密码", trigger: "blur" },
      { validator: validatePassword, trigger: "blur" }
    ],
    confirmPassword: [
      { required: true, message: "请再次输入密码", trigger: "blur" },
      { validator: validateConfirmPassword, trigger: "blur" }
    ],
  });
</script>

常规必填项校验

1
2
3
4
5
6
7
<script>
  // 表单校验
  const rules = reactive({
    coopPartnerName: [{ required: true, message: '此项是必填项', trigger: 'blur' }],
    partnerType: [{ required: true, message: '此项是必填项', trigger: 'blur' }],
  })
</script>

四舍五入校验(重写了 toFixed 方法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
  // 验证函数
  const checkRecommendDividendPercent = (rule, value, callback) => {
    let reg = /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g;
    if (value === '') {
      callback(new Error('请输入内容'));
    } else if (!reg.test(value)) {
      callback(new Error('请输入数字'));
    } else if (value > 100) {
      callback(new Error('您输入的数字超过了100!'));
    } else {
      value = toFixed(value, 1)//自定义的四舍五入函数
      infoFormData.recommendDividendPercent = value
      callback();
    }
  }
  
  // 表单校验
  const rules = reactive({
    recommendDividendPercent: [{ type: Number, validator: checkRecommendDividendPercent, trigger: ['blur'] }],
  })
</script>
toFixed
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
  //四舍五入方法,num为处理的数值,decimal为保留位数
  export let toFixed = function(num, decimal) {
  	if (isNaN(num)) {
  		return 0;
  	}
  	const p1 = Math.pow(10, decimal + 1);
  	const p2 = Math.pow(10, decimal);
  	return Math.round(num * p1 / 10) / p2;
  }
</script>

禁止输入中文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <el-form-item label="Agent Id" prop="id">
    <el-input v-model="form.id" placeholder="请输入Agent Id" @input="filterInput" />
  </el-form-item>
</template>
<script>
  //禁止输入中文
  //在调用filterInput方法时,会自动把当前行的数据传递过来,event就是当前行的数据
  const filterInput = (event) => {
    const value = event;
    const newValue = value.replace(/[\u4e00-\u9fff]+/g, '');
    form.id = newValue;
  }
</script>

validate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
  <el-form
    label-width="auto"
    :model="form"
    :rules="rules"
    ref="myForm"
    width="100%"
    class="accountForm"
  >
    <p class="step-account-info">请填写您需要找回的账号</p>
    <el-form-item prop="account" class="account-item">
      <el-input
        v-model="form.account"
        placeholder="请输入用户名"
      />
    </el-form-item>
    <input type="button" value="下一步" class="pass-button-submit" @click="getPhone(myForm)">
  </el-form>
</template>
<script>
export default {
  setup() {
    const getPhone = async (myForm) => {
      await myForm.validate((valid, fields) => { 
        if (valid) {
          let params = {
            account: form.account,
          }
          landRelevant.getPhone(params).then((res) => {
              if (res.statusCode == 200) {
                verifyForm.way = res.data
                verifyDialogVisible.value = true;
              } else {
                ElMessage({
                  message: res.message,
                  type: "error",
                });
              }
          }).catch((e) => console.log("request accounts ERR", e));
        } else { 
          console.log(fields, "--fields-");
        }
      })
    }
  }
}
<script>

validate 方法:

  • myForm.validate 方法用于验证整个表单。
  • 如果所有表单项都符合验证规则,validate 方法会返回 true
  • 如果有任意一项不符合验证规则,validate 方法会返回 false,并将未通过验证的字段信息存储在 fields 对象中。

错误信息显示:

  • Element Plus 自动处理验证错误信息的显示。
  • 如果某个表单项未通过验证,Element Plus 会在该表单项下方显示错误信息。

立即触发校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
   <el-form-item prop="imageCode" v-if="editTitle === 2">
     <el-input
       type="code"
       size="large"
       placeholder="请输入图形验证码"
       v-model="form.imageCode"
       style="flex: 1;"
       @input="validateImageCodeNumber"
     ></el-input>
     <img :src="imageUrl" alt="验证码" @click="getImageCode()" title="点击刷新验证码"/>
   </el-form-item>
</template>
<script>
export default {
  setup() {
    const rules = reactive({
      imageCode: [{ required: true, message: "请填写图形验证码", trigger: "blur" }],
    });

    // 监听输入框的 input 事件,并立即触发校验
    const validateImageCodeNumber = () => {
      if (myForm.value) {
          myForm.value.validateField('imageCode').then(() => {
          // 校验成功
            
          }).catch(() => {
          // 校验失败

          });
      }
    };

  }
}
</script>

限制输入只能是数字

尽管 type="number" 会阻止非数字输入,但它允许用户粘贴非数字文本。使用 JavaScript 可以更精细地控制输入内容。

1.只允许输入数字 (整数:小数点不能输入)

1
<input type="text" onkeyup="value=value.replace(/[^\d]/g,'')" >

2.允许输入小数 (两位小数)

1
<input type="text" onkeyup="value=value.replace(/^\D (\d (?:.\d{0,2})?). 1')" >

3.允许输入小数 (一位小数)

1
<input type="text" onkeyup="value=value.replace(/^\D (\d (?:.\d{0,1})?). 1')" >

4.开头不能为 0,且不能输入小数

1
<input type="text" onkeyup="value=value.replace(/[ \d]/g,'').replace(/ 0{1,}/g,'')" >

左下拉、右按钮

/images/posts/ElementPlus开发记录/9.png
(图9)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<template>
  <el-dialog v-model="kbDialogVisible" width="500px" :before-close="kbDialogClose">
    <div class="content-div">
      <!-- 创建 知识库/Agents -->
      <div class="add-div">
        <el-input v-model="inputName" :placeholder="isKB ? '请输入知识库名称' : '请输入Agent名称'" style="width: 400px;">
        
          <template #prepend>
              <!-- 左侧下拉菜单(知识库) -->
              <el-select v-if="isKB" v-model="kbCategory" placeholder="知识库类型" style="width: 110px;">
                <el-option
                  v-for="item in kbCategoryOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
              <!-- 左侧下拉菜单(agent) -->
              <el-select v-else v-model="agentCategory" placeholder="Agent类型" style="width: 110px;">
                <el-option
                  v-for="item in agentCategoryOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
          </template>

          <!-- 右侧按钮 -->
          <template #append>
            <el-button type="primary" @click="isKB ? addKb() : addAgent() ">{{ isKB ? "创建知识库" : "创建Agent" }}</el-button>
          </template>
        </el-input>
      </div>
      
      <div class="list-div">
        <el-table :data="kbTableData" stripe style="width: 100%">
          <el-table-column prop="name" :label="isKB ? '知识库名称' : 'Agent名称'" header-align="center" align="center">
            <template #default="scope">
              {{ scope.row.name }}
            </template>
          </el-table-column>
          <el-table-column prop="knowledgeBaseName" label="知识库名称" v-if="!isKB" header-align="center" align="center">
            <template #default="scope">
              {{ scope.row.knowledgeBaseName }}
            </template>
          </el-table-column>
          <el-table-column prop="created_at" label="创建时间" header-align="center" align="center">
            <template #default="scope">
              {{ getDate(scope.row.created_at) }}
            </template>
          </el-table-column>
          <el-table-column label="操作" width="auto" header-align="center" align="center">
            <template #default="scope">
              <el-button type="primary" @click="isKB ? docList(scope.row) : agentConfig(scope.row)">查看</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
     
    </div>
    <div class="page-box">
      <el-pagination background @size-change="kbHandleSizeChange" @current-change="kbHandleCurrentChange" :current-page="kbCurrentPage" :page-sizes="kbPageSizes" :page-size="kbPageSize" layout="total, prev, pager, next" :total="kbTotal" />
    </div>
  </el-dialog>
</template>
<script>
export default {
  setup() {
    const inputName = ref("") //输入框中的值
    const kbCategory = ref("1")  //下拉框默认选中知识库
    const kbCategoryOptions = [
      {
        value: '1',
        label: '知识库',
      },
      {
        value: '2',
        label: '知识图谱',
      },
    ]
    const agentCategoryOptions = [
      {
        value: '1',
        label: 'AAA',
      },
      {
        value: '2',
        label: 'BBB',
      },
    ]
    const agentCategory = ref("1")  //下拉框默认选中
  }
}
</script>
<style>
.content-div {
  display: flex;
  flex-direction: column;
  justify-content:center;

  .add-div {
    display: flex;
    justify-content:center;
    margin-bottom: 20px;
  }

  .list-div {
    display: flex;
    justify-content:center; 
  }
}
</style>

边框线、背景色和选中时边框颜色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<style>
:deep(.el-input__wrapper) {
  box-shadow: none; // 去除边框线
  background-color:rgba(0,0,0,0); // 背景色透明
  border-color: rgba(0,0,0,0);
}

:deep(.el-input__wrapper.is-focus) {
  box-shadow: none!important; // 去除选中时的蓝色边框
}
</style>

MessageBox 消息弹框

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script>
  ElMessageBox.confirm('这是一个提示信息,您确定要继续吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      // 用户点击了“确定”按钮
      handleConfirm();
    })
    .catch(() => {
      // 用户点击了“取消”按钮
      console.log('取消了操作');
    });
</script>

其他

快速给对象赋空值

1
2
3
4
5
6
<script>
  // 将每个属性值设为空字符串
  Object.keys(form).forEach((key) => {
    form[key] = "";
  });
</script>

替换三元运算符

有时判断太多,用三元运算符会很复杂,以下是对三元运算符的替换

映射对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>
  <el-table :data="tableData">
    <el-table-column prop="userType" label="用户类型">
      <template #default="scope">
        {{ userTypeMap[scope.row.userType] || '未知' }}
      </template>
    </el-table-column>
  </el-table>
</template>
<script setup>
  import { ref } from 'vue';
  // 定义映射对象
  const userTypeMap = ref({
    '0': '会员',
    '1': '员工',
    '2': '店长收银',
    '9': '系统管理员'
  });
</script>
switch 语句
1
2
3
4
5
6
7
8
<el-table-column prop="userType" label="用户类型">
  <template #default="scope">
    <span v-if="scope.row.userType === '0'">会员</span>
    <span v-else-if="scope.row.userType === '1'">员工</span>
    <span v-else-if="scope.row.userType === '2'">店长收银</span>
    <span v-else>系统管理员</span>
  </template>
</el-table-column>

在 el-dialog 中使用 el-form 数据无法清空

日常开发中,常利用 el-dialog + el-form 去进行表格项的新增和修改,在开发过程中,发现先点击编辑,后点击新增,位于 dialog 中的 form 表单无法清空。

错误样例
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
<template>
  <el-dialog :title="isEdit ? '编辑用户' : '添加用户'" v-model="dialogVisible" width="600px">
    <el-form label-width="auto" :model="form" :rules="rules" ref="myForm" v-loading="loading" label-position="right" width="100%">
      <el-form-item label="姓名:" prop="userName">
        <el-input v-model="form.userName" placeholder="请输入姓名" />
      </el-form-item>

      <el-form-item label="账号:" prop="account">
        <el-input v-model="form.account" placeholder="请输入账号" @input="filterInput"/>
      </el-form-item>
      <el-form-item label="密码:" prop="password">
        <el-input v-model="form.password" type="password" placeholder="请输入密码" />
      </el-form-item>
      <el-form-item label="确认密码:" prop="confirmPassword">
        <el-input v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" />
      </el-form-item>
      <el-form-item label="性别:" prop="sex">
        <el-radio-group v-model="form.sex">
          <el-radio value="1"></el-radio>
          <el-radio value="0"></el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="手机:" prop="mobile">
        <el-input
            v-model="form.mobile"
            placeholder="请输入手机号码"
            type="number"
        />
      </el-form-item>
      <el-form-item label="邮箱:" prop="email">
        <el-input v-model="form.email" placeholder="请输入邮箱" />
      </el-form-item>
      <el-form-item label="角色:" prop="userType">
        <el-radio-group v-model="form.userType">
          <el-radio value="0">普通用户</el-radio>
          <el-radio value="9">系统管理员</el-radio>
        </el-radio-group>
      </el-form-item>
    </el-form>

    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="isEdit ? submitUpdateForm(myForm) : submitAddForm(myForm)"> 保存 </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script>
export default {
  setup() {
    const form = reactive({
      id: "",
      userName: "",
      account: "",
      password: "",
      sex: "",
      mobile: "",
      email: "",
      userType: "",
      active: "",
    });
  
    // 添加按钮
    const addPopup = () => {
      isEdit.value = false;
  
      // 将每个属性值设为空字符串
      Object.keys(form).forEach((key) => {
        form[key] = "";
      });
  
      //重置表单验证状态
      if (myForm.value) {
        myForm.value.resetFields();
      }
  
      dialogVisible.value = true;
    };
  
    //修改按钮
    const updatePopup = (row) => {
      isEdit.value = true;
  
      // 将每个属性值设为空字符串
      Object.keys(form).forEach((key) => {
        form[key] = "";
      });
  
      //重置表单验证状态
      if (myForm.value) {
        myForm.value.resetFields();
      }
  
      form.id = row.id;
      form.userName = row.userName;
      form.account = row.account;
      form.password = row.password;
      form.confirmPassword = row.password;
      form.sex = (row.sex && row.sex != null) ? (row.sex === "男" ? "1" : "0"): "";
      form.mobile = row.mobile;
      form.email = row.email;
      form.userType = String(row.userType);
  
      dialogVisible.value = true;
    };
  }
}
</script>

resetFields: 这是 Element PlusElForm 组件提供的一个方法,用于重置表单的所有字段。这包括:

  • 清空表单的所有字段值。
  • 清除所有字段上的验证错误。

resetFields 函数我理解错了,这个函数的本质并不是清空表单,而是用初始值覆盖表单。 如果初始值是空,那么就能起到清空表单的效果。而这个初始值指的是对话框第一次渲染出来时的表单数据。

updatePopup 方法中,将 dialogVisible 的值设为 true 之后,el-dialog 正处于渲染中,在 Mounted() 生命周期之前,就将表单值赋值了,此时表单 Mounted 之后,就认为赋值后的为表单的初始值,而不是 const form 声明处为初始值,而 resetFields 方法是将表单重置为初始值,就无法清空第一次编辑的数据。

修改后的样例
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
<template>
  <el-dialog :title="isEdit ? '编辑用户' : '添加用户'" v-model="dialogVisible" width="600px">
    <el-form label-width="auto" :model="form" :rules="rules" ref="myForm" v-loading="loading" label-position="right" width="100%">
      <el-form-item label="姓名:" prop="userName">
        <el-input v-model="form.userName" placeholder="请输入姓名" />
      </el-form-item>

      <el-form-item label="账号:" prop="account">
        <el-input v-model="form.account" placeholder="请输入账号" @input="filterInput"/>
      </el-form-item>
      <el-form-item label="密码:" prop="password">
        <el-input v-model="form.password" type="password" placeholder="请输入密码" />
      </el-form-item>
      <el-form-item label="确认密码:" prop="confirmPassword">
        <el-input v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" />
      </el-form-item>
      <el-form-item label="性别:" prop="sex">
        <el-radio-group v-model="form.sex">
          <el-radio value="1"></el-radio>
          <el-radio value="0"></el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="手机:" prop="mobile">
        <el-input
            v-model="form.mobile"
            placeholder="请输入手机号码"
            type="number"
        />
      </el-form-item>
      <el-form-item label="邮箱:" prop="email">
        <el-input v-model="form.email" placeholder="请输入邮箱" />
      </el-form-item>
      <el-form-item label="角色:" prop="userType">
        <el-radio-group v-model="form.userType">
          <el-radio value="0">普通用户</el-radio>
          <el-radio value="9">系统管理员</el-radio>
        </el-radio-group>
      </el-form-item>
    </el-form>

    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="isEdit ? submitUpdateForm(myForm) : submitAddForm(myForm)"> 保存 </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script>
import { ref, onMounted, reactive, nextTick } from "vue";
export default {
  setup() {
    const form = reactive({
      id: "",
      userName: "",
      account: "",
      password: "",
      sex: "",
      mobile: "",
      email: "",
      userType: "",
      active: "",
    });

    onMounted(() => {
      userList();
    });

    // 添加按钮
    const addPopup = () => {
      isEdit.value = false;

      //重置表单验证状态
      if (myForm.value) {
        myForm.value.resetFields();
      }

      dialogVisible.value = true;
    };

    //修改按钮
    const updatePopup = (row) => {
      isEdit.value = true;

      //重置表单验证状态
      if (myForm.value) {
        myForm.value.resetFields();
      }

      //在编辑赋值时使用 nextTick 使表单 Mounted 之后再去执行赋值
      nextTick(() => {
        form.id = row.id;
        form.userName = row.userName;
        form.account = row.account;
        form.password = row.password;
        form.confirmPassword = row.password;
        form.sex = (row.sex && row.sex != null) ? (row.sex === "男" ? "1" : "0"): "";
        form.mobile = row.mobile;
        form.email = row.email;
        form.userType = String(row.userType);
        form.useCounts = row.useCounts;
      });

      dialogVisible.value = true;
    };
  }
}
</script>

解决在 input 框中按回车键页面刷新

VUE 页面键盘输入 Enter 键就刷新是由于浏览器的默认行为所导致的。当在 VUE 页面中按下 Enter 键时,浏览器会默认执行提交表单的操作,这会导致页面刷新。

要避免这种情况,可以通过阻止默认的提交行为来禁止页面刷新。在 VUE 中,可以使用 @submit.prevent 修饰符来阻止表单的默认提交行为,或者可以在 <form> 标签中添加 @submit.prevent 来达到同样的效果。

通过 @keydown.enter.prevent 指令,可以阻止浏览器默认的 Enter 键提交行为,从而避免页面刷新。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<el-form-item label="新密码" prop="password" label-position="right">
  <el-input
    v-model="form.password"
    size="large"
    type="password"
    placeholder="请输入密码"
    @keyup.enter="submitNewPassword(myForm)"
    @keydown.enter.prevent
  />
</el-form-item>

父元素点击事件覆盖子元素的点击事件

父元素的点击事件覆盖了子元素的点击事件。在这种情况下,可以使用事件冒泡阻止机制来解决这个问题。

Vue 3 中,可以使用 @click.stop 修饰符来阻止事件冒泡。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<div class="channelItemBox" v-for="(item, index) in wxScenes" :key="index">
  <div class="box" style="height: 140px;" @click="jumpMessage(item)">
    <div class="account-avatar-box app-flex-c-b">
      <span class="el-avatar el-avatar--icon el-avatar--square avatar app-flex-c-c" style="--el-avatar-size: 40px">
        <img :src="item.avatar" style="object-fit: cover" />
      </span>
      <img class="more-icon" src="../assets/images/more-icon.png" alt="" />
    </div>
    <div class="account-info-item app-flex-c" style="margin-top: 16px">
      授权账号:<span class="app-line-ellipsis">{{ item.name }}</span>
    </div>
    <div class="account-info-item app-flex-c">
      绑定员工:
      <div class="app-flex-c" style="font-weight: 400; font-size: 12px; color: #226cff; margin-left: 10px" @click.stop="bindEmp(item)">
        <div class="app-line-ellipsis" style="cursor: pointer">{{ item.agentsName === "" || item.agentsName === null ? "绑定员工" : item.agentsName }}</div>
        <img class="right-arrow-blue" src="../assets/images/youjian.png" alt="" />
      </div>
    </div>
  </div>
</div>

Vue 2 中的解决方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
  <div class="box" style="height: 140px;" @click="jumpMessage">
    <div class="account-avatar-box app-flex-c-b">
      <span class="el-avatar el-avatar--icon el-avatar--square avatar app-flex-c-c" style="--el-avatar-size: 40px">
        <img :src="item.avatar" style="object-fit: cover" />
      </span>
      <img class="more-icon" src="https://cdn.wiseai.chat/APP/sys_img/digitalEmployee/huiyanWeb/more-icon.png" alt="" />
    </div>
    <div class="account-info-item app-flex-c" style="margin-top: 16px">
      授权账号:<span class="app-line-ellipsis">{{ item.open_kfid }}</span>
    </div>
    <div class="account-info-item app-flex-c">
      绑定员工:
      <div class="app-flex-c" style="font-weight: 400; font-size: 12px; color: #226cff; margin-left: 10px">
        <div class="app-line-ellipsis" style="cursor: pointer" @click="handleBindEmpClick($event, item)">
          {{ item.agentsId === "" ? "绑定员工" : item.agentsId }}
        </div>
        <img class="right-arrow-blue" src="../assets/images/youjian.png" alt="" />
      </div>
    </div>
  </div>
</template>
<script>
methods: {
  jumpMessage() {
    // 跳转到新的页面
    console.log('跳转到新的页面');
    // 你可以在这里编写跳转逻辑,例如使用 router.push
    this.$router.push('/new-page');
  },
  handleBindEmpClick(event, item) {
    // 阻止事件冒泡
    event.stopPropagation();
    // 调用 bindEmp 方法
    this.bindEmp(item);
  },
  bindEmp(item) {
    // 显示弹窗或其他逻辑
    console.log('显示绑定员工弹窗', item);
    // 你可以在这里编写显示弹窗的逻辑
  }
}
</script>
帮助理解
  1. @click="handleBindEmpClick($event, item)":在子元素上添加了一个新的点击事件处理器 handleBindEmpClick,并传递了 $eventitem 作为参数。
  2. handleBindEmpClick(event, item):在 handleBindEmpClick 方法中,调用了 event.stopPropagation() 来阻止事件冒泡到父元素,然后调用了 bindEmp 方法。
  3. jumpMessage:父元素的点击事件处理器,用于跳转到新的页面。

会话列表的时间

场景:会话列表,显示该消息的时间,例如:5秒前、5小时前、2024-09-25(超过一天显示时间)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
import { data-open=false, ref, onMounted, reactive, nextTick } from "vue";
export default {
  setup() {
    // 将转换后的时间在页面显示
    const convertRTime = (time) => {
      let formatTime;
      let updateTimeObj = new Date(time);
      let currentTime = new Date();
      let diff = currentTime - updateTimeObj; // 计算这条消息的时间和当前时间的差值
      const diffInDays = diff / (1000 * 60 * 60 * 24);  // 将毫秒值转换为天数
      if (diffInDays > 1) { // 判断时间差是否超过一天
        // 如果超过一天,直接显示原始时间 (例如:2020-09-25)
        formatTime = `${updateTimeObj.getFullYear()}-${String(updateTimeObj.getMonth() + 1).padStart(2, '0')}-${String(updateTimeObj.getDate()).padStart(2, '0')}`;
        return formatTime;
      } else {  // 时间差小于一天
        let diffInHours = Math.floor(diff / (1000 * 60 * 60));  // 计算小时数
        if (diffInHours <= 1) { //判断是否大于1小时
          let minute = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); // 计算分钟数
          if (minute > 0) {
            formatTime = minute + "分钟前";
          } else { 
            let second = Math.floor((diff % (1000 * 60 * 60)) / 1000);  // 计算秒数
            formatTime = second + "秒前";
          }
        } else { 
          formatTime = diffInHours + "小时前";
        }
        return formatTime;
      }
    }
  }
}
</script>
帮助理解
  1. getFullYear(): 获取年份,例如 2024
  2. getMonth() + 1: 获取月份,注意月份是从 0 开始的,所以需要加 1。例如,9 表示 10 月。
  3. getDate(): 获取日期,例如 19
  4. padStart(2, '0'): 确保月份和日期始终是两位数,不足两位时前面补 0

diff % (1000 * 60 * 60)

  • 这个表达式计算的是 diff 除以1小时的毫秒数后的余数。换句话说,它取出了 diff 中不足1小时的部分。
  • 例如,如果 diff4500000 毫秒(即 1 小时 15 分钟),那么 4500000 % 3600000 的结果是 900000 毫秒。

(diff % (1000 * 60 * 60)) / (1000 * 60)

  • 这个表达式将上一步得到的余数(不足1小时的部分)转换成分钟数。因为:1分钟 = 60秒 = 60 * 1000 毫秒
  • 因此,900000 / (1000 * 60) 的结果是 15 分钟。

Math.floor(...)

  • Math.floor 函数用于向下取整,确保结果是一个整数。
  • 例如,如果 diff4500000 毫秒,那么 (4500000 % 3600000) / (1000 * 60) 的结果是 15,Math.floor(15) 仍然是 15

聊天记录自动下滑至最新

原因:DOM 渲染时机问题:在 Vue 中,当你更新数据时(如 chatMessages.value = res.data),Vue 需要一些时间来重新渲染 DOM。如果 scrollToBottom()DOM 更新完成之前被调用,那么它可能不会滚动到最新的位置。

使用 nextTick 确保 DOM 更新完成后,再调用 scrollToBottom()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
  <!-- 聊天信息框 -->
  <div class="msg-box" style="height: calc(100% - 75px)" ref="chatContainer">
  
  </div>
</template>
<script>
import { nextTick } from "vue";

export default {
  name: "inviteView",
  setup() {
    const chatContainer = ref(null)
    const recordLoading = ref(false)  //聊天记录的loading

    const getChatRecord = (external_userid) => {
      recordLoading.value = true;
      let params = {

      };
      landRelevant.getChatRecord(params).then((res) => {
          if (res.statusCode == 200) {
            chatMessages.value = res.data
            nextTick(() => {  
              scrollToBottom();  
            });   
          } else {
            ElMessage({
              message: res.message,
              type: "error",
            });
          }
        })
        .catch((e) => console.log("request accounts ERR", e));
        recordLoading.value = false
    }

    //自动划到聊天记录的底部
    const scrollToBottom = () => {
      chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
    };
  }
}
</script>

上滑加载聊天记录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<template>
  <div class="msg-box" style="height: calc(100% - 75px)" ref="chatContainer" @scroll="handleScrollTop">

  </div>
</template>
<script>
import { nextTick } from "vue";

export default {
  name: "inviteView",
  setup() {
    const mrAllDataLoaded = ref(false); //聊天记录数据是否都已经加载完毕
    const scrollTopBeforeLoad = ref(0); //聊天记录之前的滚动条位置
    const recordLoading = ref(false)  //聊天记录的loading
    const mrPageNo = ref(1); // 聊天记录的当前页码
    const mrPageSize = ref(10); // 聊天记录的每页数据量
    const mrTotalCount = ref(0) //聊天记录的总条数
    const chatMessages = ref([]); //聊天记录数据

    //监听聊天记录是否滑倒了顶部,从而触发加载更多聊天记录
    const handleScrollTop = () => {
      if (chatContainer.value.scrollTop === 0) {
        if (!mrAllDataLoaded.value) { //聊天记录没有全部加载完毕的情况下,才会触发加载更多聊天记录
          scrollTopBeforeLoad.value = chatContainer.value.scrollTop + chatContainer.value.offsetHeight; //在触发加载更多聊天记录之前,先记录当前的滚动条位置  
          getChatRecords() //获取聊天记录
        }
      }
    };

    //获取聊天记录
    const getChatRecords = (item) => {
      recordLoading.value = true;
      let params = {

      }
      landRelevant.getChatRecords(params).then((res) => {
          if (res.statusCode == 200) {
            if (mrPageNo.value === 1) {
              chatMessages.value = res.data
              nextTick(() => {  //第一页聊天记录滑倒底部
                scrollToBottom();  
              });
            } else {
              let oldLength = chatMessages.value.length;
              chatMessages.value = [...res.data, ...chatMessages.value];  //将数据添加到之前的数据前面
              // 计算新的滚动条位置
              let newScrollTop = scrollTopBeforeLoad.value + (chatMessages.value.length - oldLength) * 50; // 假设每条消息的高度为50px
              chatContainer.value.scrollTop = newScrollTop;
            }

            mrTotalCount.value = res.page.total //数据的总条数
            recordLoading.value = false
            mrPageNo.value += 1;  // 聊天记录页码+1

            // 检查是否已经加载了所有的聊天记录数据
            if (chatMessages.value.length >= mrTotalCount.value) { //如果当前数据的长度 >= 总条数
              mrAllDataLoaded.value = true;
            }

          } else {
            ElMessage({
              message: res.message,
              type: "error",
            });
            recordLoading.value = false
          }
        })
        .catch((e) => {
          console.log("request accounts ERR", e)
          recordLoading.value = false
        });
    }
  }
}
</script>

引入项目中的图片

方式一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <div class="bot-set-div">
    <div class="title"><span class="require-span">*</span>角色类型</div>
  </div>
  <div class="el-form-item" role="group">
    <div class="el-form-item__content">
      <div class="user-list" v-for="(item, index) in customerTypeList" :key="index">
        <div class="user-item">
          <img :src="item.avatar" alt="" class="user-head" @click="selectItem(index, item)" :class="{ selected: selectedIndex === index }" />
          <p class="user-name">{{ item.name }}</p>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
export default {
  name: "inviteView",
  setup() {
    const customerTypeList = ref([
      {"name": "通用", "type": "1", "avatar": new URL('../assets/images/tongyong.png', import.meta.url).href, "botSet": ""},
      // 其他项...
    ]);
  }
}
</script>

方式二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
  <div class="support-scenarios">
    <el-row :gutter="20" justify="center" class="scenarios-row">
      <el-col v-for="(item, index) in scenarios" :key="index" :span="4" class="scenario-col">
        <div class="scenario-item">
          <img :src="item.icon" alt="场景图标" class="scenario-icon" />
          <p class="scenario-text">{{ item.text }}</p>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
export default {
  setup() {
    const scenarios = ref([
      { text: "微信客服", icon: require("@/assets/images/qiwei.png") },
      ......
    ]);
  }
}
</script>

索引

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
export default {
  setup() {
    const bindEmp = (item) => {
      // 找到索引
      selectedIndex.value = employees.value.findIndex((emp) => emp.id === item.agentsId);
      // 通过索引获取数据
      const selectedEmployee = employees.value[selectedIndex.value];
    };
  }
}
</script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
  <el-table-column prop="userType" label="角色">
    <template #default="scope">
      {{ userTypeMap[scope.row.userType] }}
    </template>
  </el-table-column>
</template>
<script>
export default {
  setup() {
    const userTypeMap = reactive({
        '0': '普通用户',
        '9': '系统管理员'
      })
  }
}
</script>

去除二维码图片周围的空白包边

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<script>
export default {
  name: "inviteView",
  setup() {
    
    // 二维码图片
    const getCustomerQrCode = (item) => { 
      kefuImgUrlLoading.value = true
      let params = {
        open_kfid: item.open_kfid,
        enId: item.enId,
      }

      landRelevant.getCustomerQrCode(params).then((res) => {
        if (res.statusCode == 200) {
          // kefuImgUrl.value = 'data:image/png;base64,' + res.data;
          // kefuImgUrlLoading.value = false
          
          const base64Data = res.data;
          trimWhiteSpaces(base64Data).then(trimmedImage => {
            kefuImgUrl.value = trimmedImage;
            kefuImgUrlLoading.value = false;
          }).catch(error => {
            console.error('Error trimming image:', error);
            kefuImgUrlLoading.value = false;
          });

        } else {
          if (res.message == "Checked in today") {
            ElMessage({
              message: "Checked in today!!",
              type: "warning",
            });
            kefuImgUrlLoading.value = false
          }
        }
      })
    }


    //去除二维码图片周围的空白
    const trimWhiteSpaces = (base64Data) => {
        return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = `data:image/png;base64,${base64Data}`;
        img.onload = () => {
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
        
          // Draw the image on the canvas
          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img, 0, 0);
        
          // Get the image data
          const imageData = ctx.getImageData(0, 0, img.width, img.height);
          const data = imageData.data;
        
          // Find the bounding box of the non-white pixels
          let top = img.height, bottom = 0, left = img.width, right = 0;
          for (let y = 0; y < img.height; y++) {
            for (let x = 0; x < img.width; x++) {
              const index = (y * img.width + x) * 4;
              if (data[index] !== 255 || data[index + 1] !== 255 || data[index + 2] !== 255) {
                if (y < top) top = y;
                if (y > bottom) bottom = y;
                if (x < left) left = x;
                if (x > right) right = x;
              }
            }
          }
        
          // Create a new canvas with the cropped dimensions
          const croppedCanvas = document.createElement('canvas');
          const croppedCtx = croppedCanvas.getContext('2d');
          croppedCanvas.width = right - left;
          croppedCanvas.height = bottom - top;
          croppedCtx.drawImage(img, left, top, croppedCanvas.width, croppedCanvas.height, 0, 0, croppedCanvas.width, croppedCanvas.height);
        
          // Convert the cropped canvas to a data URL
          resolve(croppedCanvas.toDataURL());
        };
        img.onerror = () => {
          reject(new Error('Failed to load image'));
        };
      });
    };

  }
}

点击文字跳转

1
2
3
<a v-if="scope.row.type == 2" :href="scope.row.path" target="_blank" style="color: blue; text-decoration: none;">
  {{ scope.row.fileName }}
</a>

style="color: blue; text-decoration: none;"

  • color: blue;:将链接文字的颜色设置为蓝色。
  • text-decoration: none;:移除链接的下划线。

target 属性用于指定链接在何处打开。_blank 表示链接将在新的浏览器标签页中打开。其他常见的 target 值包括:

  • _self:默认值,链接在同一框架或窗口中打开。
  • _parent:链接在父框架集中打开。
  • _top:链接在整个窗口中打开,忽略任何框架。

Cannot read properties of null (reading ‘getBoundingClientRect’)

/images/posts/ElementPlus开发记录/7.png
(图7)

不知道怎么解决,后续解决后再补充。

报错案例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
  <el-form-item prop="agentCategory" v-show="userType == '9'">
    <el-select v-model="form.agentCategory">
      <el-option
        v-for="item in agentCategoryOptions"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      />
    </el-select>
  </el-form-item>
</template>
<script>
  export default {
    setup() {
      const agentCategoryOptions = [
        {
          value: '1',
          label: '测试1',
        },
        {
          value: '2',
          label: '测试2',
        },
      ]

      const userType = ref('0');
      
      onMounted(() => {
        let userInfo = localStorage.getItem('userInfo');
        if (userInfo) { 
          getInfoById(JSON.parse(userInfo).userId)
        }
      });

      const getInfoById = (id) => {
        let params = {
          userId: id,
        }
        landRelevant.getInfoById(params).then((res) => {
          if (res.statusCode == 200 ) {
            userType.value = res.data.userType;
          }
        }).catch((e) => console.log("request accounts ERR", e));
      }
      
    }
  }
</script>

Extraneous non-emits event listeners (updateReply) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the “emits” option.

/images/posts/ElementPlus开发记录/8.png
(图8)
agentsInfo.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <div class="main">
    <div class="main-left">
      <bot-form :data="botInfo" @updateReply="getResults"></bot-form>
    </div>
    <div class="main-right">
      <bot-chat :data="botInfo" :chatStyle="`width:100%`"></bot-chat>
    </div>
  </div>
</template>
<script>
  export default {
    setup() {
      const getResults = (e) => {
        // 子组件传递过来的数据
        console.log(e)
      }
    }
  }
</script>
BotForm.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
  export default {
    emits: ['updateReply'],
    setup(props,{emit}) {

      //向父组件发送数据
      const sendMessage = () => {
        emit('updateReply', data)
      }
    }
  }
</script>

watch

监听一个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
export default {
  setup() {
    watch(phoneIsTrue, () => {
      if (phoneIsTrue.value) {
          spanRef.value.style.cursor = "pointer"
          isClickable.value = true
      } else {
          spanRef.value.style.cursor = "not-allowed"
      }
    }
  }
}
</script>

监听两个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script>
export default {
  setup() {
    watch([phoneIsTrue, imageCodeIsTrue], ([newPhoneIsTrue, newImageCodeIsTrue]) => {
      if (newPhoneIsTrue && newImageCodeIsTrue) {
        spanRef.value.style.cursor = "pointer";
        isClickable.value = true;
      } else {
        spanRef.value.style.cursor = "not-allowed";
        isClickable.value = false;
      }
    });
  }
}
</script>
补充

如果 spanRef.value.style.cursor = "not-allowed"; 报错的话,用下面的写法

1
2
3
4
5
<script>
  if (spanRef.value) {
      spanRef.value.style.cursor = "not-allowed";
  }
</script>

html

Padding / Margin

Padding(内边距)

  • 定义:padding 是指元素内容与其边框之间的空间。
  • 作用:增加元素内容与边框之间的距离,使内容不会紧贴边框。
  • 影响:padding 会增加元素的总尺寸。例如,如果一个元素的宽度为 100px,加上 20pxpadding,那么元素的总宽度将变为 120px
  • 语法:可以分别设置上、右、下、左四个方向的 padding,也可以使用简写形式。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<style>
  /* 分别设置 */
  padding-top: 10px;
  padding-right: 20px;
  padding-bottom: 10px;
  padding-left: 20px;

  /* 简写形式 */
  padding: 10px 20px 10px 20px; /* 上 右 下 左 */
  padding: 10px 20px 10px; /* 上 左右 下 */
  padding: 10px 20px; /* 上下 左右 */
  padding: 10px; /* 四个方向相同 */
<style>

Margin(外边距)

  • 定义:margin 是指元素边框与其相邻元素之间的空间。
  • 作用:控制元素与其他元素之间的距离。
  • 影响:margin 不会增加元素的总尺寸,但会影响元素在页面中的位置。
  • 语法:与 padding 类似,可以分别设置上、右、下、左四个方向的 margin,也可以使用简写形式。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<style>
  /* 分别设置 */
  margin-top: 10px;
  margin-right: 20px;
  margin-bottom: 10px;
  margin-left: 20px;

  /* 简写形式 */
  margin: 10px 20px 10px 20px; /* 上 右 下 左 */
  margin: 10px 20px; /* 上下 左右 */
  margin: 10px; /* 四个方向相同 */
</style>

阴影

实现在鼠标悬停时为 .block, .todo 添加阴影效果

1
2
3
4
5
6
7
8
<style>
  .block, .todo {
      transition: box-shadow 0.3s ease; /* 添加过渡效果 */
  }
  .block:hover, .todo:hover {
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 
  }
</style>
说明
  • 过渡效果 (transition):
    为了使阴影效果平滑过渡,我们添加了 transition: box-shadow 0.3s ease;。这会让阴影效果在鼠标悬停时平滑地显示出来。
  • 阴影效果 (box-shadow):
    使用 :hover 伪类来指定鼠标悬停时的效果。这里我们设置了 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);,表示阴影的水平偏移量为 0,垂直偏移量为 4px,模糊半径为 8px,颜色为黑色(rgba(0, 0, 0, 0.2) 表示透明度为 0.2 的黑色)。

v-xxx

v-if/v-else
1
2
3
4
5
6
<template>
    <div>
      <span v-if="!password">未设置</span>
      <span v-else>{{ password }}</span>
  </div>
</template>
v-for
1
2
3
4
5
6
<div class="user-list" v-for="(item, index) in customerTypeList" :key="index">
  <div class="user-item">
    <img :src="item.avatar" alt="" class="user-head" />
    <p class="user-name">{{ item.name }}</p>
  </div>
</div>

列表变色

鼠标悬停变色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
  <div 
    class="conversation-item-box"
    v-for="(item, index) in sessionListDate"
    :key="index"
    @mouseenter="hoveredIndex = index" 
    @mouseleave="hoveredIndex = null"
    :class="{ hovered: hoveredIndex === index }"
  >
  </div>
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    const hoveredIndex = ref(null);

    return {
      hoveredIndex,
    };
  }
}
</script>
<style>
.hovered {
  background-color: #f3f6fd; /* 设置悬停时的背景颜色 */
}
</style>

鼠标点击变色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
  <div 
    class="conversation-item-box"
    v-for="(item, index) in sessionListDate"
    :key="index"
    @click="selectItem(index)"
    :class="{ selected: selectedIndex === index }"
  >
  </div>
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    const selectedIndex = ref(null); // 用于跟踪当前选中的列表项索引
    
    const selectItem = (index) => {
      selectedIndex.value = index; // 更新选中的列表项索引
    };
    
    return {
      selectedIndex,
      selectItem,
    };
  }
}
</script>
<style>
.selected {
  background-color: #f3f6fd; /* 设置选中时的背景颜色 */
}
</style>

disabled / aria-disabled

disabled

用途:disabled 属性是一个布尔属性,用于禁用表单元素(如按钮、输入框等)。当一个元素被禁用时,它不仅不可点击,而且其值不会被提交到服务器。

效果:

  • 用户无法与该元素进行交互(例如,点击按钮或输入文本)。
  • 元素的样式通常会变得灰暗或不明显,以指示其不可用状态。
  • 表单提交时,被禁用的元素的值不会被包含在提交的数据中。

仅使用 disabled 属性

1
<button disabled type="button">点击我</button>

效果:按钮不可点击,用户无法与之交互,屏幕阅读器也会告知用户该按钮是不可用的。

aria-disabled

用途:aria-disabled 是一个 ARIA(Accessible Rich Internet Applications) 属性,用于增强无障碍功能。它告诉屏幕阅读器和其他辅助技术该元素是否被禁用。

效果:

  • aria-disabled 属性设置为 true 时,屏幕阅读器会告知用户该元素是不可用的。
  • disabled 属性不同,aria-disabled 不会影响元素的交互行为,只是提供额外的无障碍信息。

仅使用 aria-disabled 属性

1
<button aria-disabled="true" type="button">点击我</button>

效果:按钮仍然可以点击,用户可以与之交互,但屏幕阅读器会告知用户该按钮是不可用的。

为什么同时使用 disabled 和 aria-disabled?

  • 最佳实践:虽然 disabled 属性已经提供了基本的无障碍支持,但有时为了确保兼容性和更好的无障碍体验,开发者会选择同时使用 disabledaria-disabled 属性。
  • 冗余保护:这样做可以确保即使某些浏览器或辅助技术没有完全支持 disabled 属性,aria-disabled 也能提供额外的无障碍信息

四舍五入

Math.round()

Math.round() 方法用于将一个数字四舍五入为最接近的整数。

1
2
3
let number = 4.5;
let rounded = Math.round(number);
console.log(rounded); // 输出: 5

四舍五入到小数点后几位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
export default {
  name: "inviteView",
  setup() {
    function roundToDecimalPlaces(num, places) {
      const factor = Math.pow(10, places);
      return Math.round(num * factor) / factor;
    }
    
    let number = 4.5678;
    let rounded = roundToDecimalPlaces(number, 2);
    console.log(rounded); // 输出: 4.57
  }
}

参数

  • num:需要四舍五入的数字。
  • places:需要保留的小数位数。

详细步骤

1.计算因子 (factor)

1
const factor = Math.pow(10, places);
  • Math.pow(10, places) 计算 10places 次方。
  • 例如,如果 places2factor 将是 100;如果 places3factor 将是 1000

2.乘以因子

1
num * factor
  • num 乘以 factor,这样可以将小数点移动到需要四舍五入的位置。
  • 例如,如果 num4.5678places2num * factor 将是 456.78

3.四舍五入

1
num * factor
  • 使用 Math.round 对乘积进行四舍五入。
  • 例如,456.78 四舍五入后是 457

4.除以因子

1
return Math.round(num * factor) / factor;
  • 将四舍五入后的结果除以 factor,将小数点移回原来的位置。
  • 例如,457 除以 1004.57

display / position / cursor 属性

display 属性

display 属性用于定义元素如何在文档流中显示。它控制元素的类型,决定了元素如何参与布局以及其子元素如何被渲染。

block

  • 将元素设置为块级元素。块级元素默认独占一行,并且可以设置宽度和高度。
  • 适用场景:用于创建需要占据整个容器宽度的元素,如段落、标题等。

inline

  • 将元素设置为内联元素。内联元素与其他内联元素在同一行显示,并且不会独占一行。
  • 适用场景:用于创建不需要独占一行的小型元素,如链接、文本等。

inline-block

  • 结合了 blockinline 的特点,使元素作为一个内联块显示。内联块元素与其他内联元素在同一行显示,并且可以设置宽度和高度。
  • 适用场景:用于创建需要在同一行排列且可设置尺寸的元素。

none

  • 隐藏元素,使其不显示在页面上,并且不占用任何空间。
  • 适用场景:用于暂时隐藏某个元素。

flex

  • 将元素设置为 Flex 容器,允许其子元素按照 Flexbox 规则进行布局。
  • 适用场景:用于创建灵活的布局,特别是当需要水平或垂直对齐元素时。

grid

  • 将元素设置为 Grid 容器,允许其子元素按照 Grid 规则进行布局。
  • 适用场景:用于创建复杂的网格布局。

justify-content 属性

justify-content 属性用于在 Flex 容器的主轴(main axis)上对齐子元素。Flex 容器的主轴默认是水平方向(从左到右),但如果设置了 flex-direction 属性,则主轴的方向会改变。

flex-start

  • 子元素靠左对齐(如果是水平方向)。
  • 适用场景:默认情况下,子元素从容器的起始位置开始排列。

flex-end

  • 子元素靠右对齐(如果是水平方向)。
  • 适用场景:子元素从容器的结束位置开始排列。

center

  • 子元素在主轴上居中对齐。
  • 适用场景:子元素在容器中心对齐。

space-between

  • 子元素在主轴上均匀分布,两端不留空白。
  • 适用场景:子元素之间均匀分布,首尾不留空白。

space-around

  • 子元素在主轴上均匀分布,每个子元素两侧留有相同间距。
  • 适用场景:子元素之间以及与容器边缘都有相同间距。

space-evenly

  • 子元素在主轴上均匀分布,包括首尾。
  • 适用场景:子元素之间以及与容器边缘都有相同间距。

position 属性

position 属性用于定义元素的定位类型。它控制元素相对于文档流的位置,以及如何使用 top, right, bottom, left 属性来精确控制元素的位置。

static

  • 默认值,元素按照正常文档流定位。
  • 适用场景:大多数元素默认情况下都是静态定位。

relative

  • 元素相对于其正常位置进行定位。可以使用 top, right, bottom, left 属性来调整元素的位置。
  • 适用场景:用于微调元素的位置,但仍然保留其在文档流中的位置。

absolute

  • 元素相对于最近的已定位祖先元素进行定位。如果没有已定位的祖先,则相对于初始包含块(通常是 <body>)进行定位。
  • 适用场景:用于完全脱离文档流,精确控制元素的位置。

fixed

  • 元素相对于浏览器窗口进行定位。无论页面如何滚动,元素的位置始终不变。
  • 适用场景:用于创建固定定位的元素,如导航栏、页脚等。

sticky

  • 元素在跨越特定阈值前为相对定位,越过阈值后变为固定定位。
  • 适用场景:用于创建粘性定位的元素,如导航栏在滚动到一定位置后固定在顶部。

cursor 属性

cursor 属性用于定义鼠标指针在元素上时的样式。它控制鼠标指针的形状,以提供用户交互的视觉反馈。

default

  • 显示默认的箭头指针。
  • 适用场景:大多数情况下,默认指针即可。

pointer

  • 显示手形指针,通常用于表示链接或其他可点击的元素。
  • 适用场景:用于创建看起来像可点击的元素,如按钮、链接等。

text

  • 显示 I 形光标,通常用于表示可编辑的文本区域。
  • 适用场景:用于文本输入框或其他需要编辑文本的地方。

help

  • 显示问号指针,通常用于表示帮助或信息提示。
  • 适用场景:用于提示用户有帮助信息可用。

not-allowed

  • 显示斜杠圆圈指针,表示操作不允许。
  • 适用场景:用于指示用户当前操作不可用。

点击展示 / 隐藏

点击展示,再次点击隐藏

/images/posts/ElementPlus开发记录/4.png
(图4)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
  <div class="account-type-item" v-for="(item, index) in accountTypeList" :key="index" @click="showAccountList(index)">
    <div class="account-type-title-box app-flex-c-b account-type-title-box-line">
      <div class="account-type-title app-flex-c">
        <img :src="item.url"/>
        <div>{{ item.name }}</div>
      </div>
      <i class="el-icon-arrow-down"></i>
    </div>
    <div class="account-list-box" :style="{ display: isAccountListVisible[index] ? 'block' : 'none' }">
      <!-- 无数据 -->
      <div class="app-nodata app-flex-c-c" style="height: 60px" v-if="item.subset.length <= 0">暂无数据</div>
      <!-- 有数据 -->
      <div v-if="item.subset.length > 0">
        <div
          class="account-item app-flex-c"
          v-for="(subItem, subIndex) in item.subset"
          :key="subIndex"
        >
          <span class="el-avatar el-avatar--icon el-avatar--square item-avatar app-flex-c-c"><img :src="subItem.avatar" style="object-fit: cover" /><span>
          <div class="account-item-nickName app-line-ellipsis">{{ subItem.name }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    const list1 = ref([
      { avatar: "", name: "客服一号" },
      { avatar: "", name: "客服二号" },
    ]);

    const list2 = ref([
      { avatar: "", name: "客服一号" },
      { avatar: "", name: "客服二号" },
    ]);

    const list3 = ref([
      { avatar: "", name: "客服一号" },
      { avatar: "", name: "客服二号" },
    ]);
    //map 方法遍历 accountTypeList 数组中的每一个元素,并返回一个新的数组。在这个新的数组中,每个元素都被设置为 false。
    const isAccountListVisible = ref(accountTypeList.value.map(() => false));
    const accountTypeList = ref([
      { url: "", name: "测试1", subset: list1},
      { url: "", name: "测试2", subset: list2},
      { url: "", name: "测试3", subset: list3},
    ]);

    const showAccountList = (index) => { 
      isAccountListVisible.value[index] = !isAccountListVisible.value[index];
    }
  }
}
</script>
解释

数据定义:

  • accountTypeList:存储账号列表的数据。
  • isAccountListVisible:一个布尔数组,用于记录每个 account-type-item 的显示状态,默认全部为 false(隐藏)。

模板:

  • 使用 v-for 循环渲染每个 account-type-item
  • 使用 @click 绑定 showAccountList 方法,并传递当前项的索引 index
  • 使用 :style 动态绑定 display 样式,根据 isAccountListVisible 数组中的对应值来决定显示或隐藏。

方法:

  • showAccountList(index):切换 isAccountListVisible 数组中对应索引的值,从而控制 account-list-box 的显示和隐藏。 通过这种方式,你可以实现点击时切换显示和隐藏的功能。每次点击 account-type-item 时,对应的 account-list-box 会显示或隐藏。

进阶

之前:点击 “测试1”,“测试1” 展开子列表,点击 “测试2”,“测试2” 展开子列表,此时,“测试1” 和 “测试2” 的子列表都处于展开状态
现在:点击 “测试1”,“测试1” 展开子列表,点击 “测试2”,“测试2” 展开子列表,同时,“测试1” 的子列表收回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
export default {
  name: "inviteView",
  setup() {
    const showAccountList = (index) => {
      //将 isAccountListVisible 数组中的所有元素都设置为 false,除了点击的父菜单的索引位置,该位置设置为 true。
      isAccountListVisible.value = isAccountListVisible.value.map((_, i) => i === index);
    }
  }
}
</script>

进阶

点击 “测试4” 不需要展开子列表,同时,其他展开的子列表隐藏

/images/posts/ElementPlus开发记录/5.png
(图5)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script>
export default {
  name: "inviteView",
  setup() {
    const showAccountList = (index) => {
      if (index === 3) {
        // 点击第四个菜单时,隐藏所有已展开的菜单
        isAccountListVisible.value = isAccountListVisible.value.map(() => false);
        return;
      }
      isAccountListVisible.value = isAccountListVisible.value.map((_, i) => i === index);
    };
  }
}
</script>

验证部分输入框的规则

前四个输入框都有值且验证规则都通过后,修改 “获取验证码” 字体为蓝色

/images/posts/ElementPlus开发记录/6.png
(图6)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<template>
<div class="form-div">
  <el-form
    :model="form"
    :rules="rules"
    ref="formRef"
    class="login-form"
  >
    <el-form-item class="login-form-username" prop="account">
      <el-input
        placeholder="手机号"
        size="large"
        @input="handleInput('account')"
        v-model="form.account"
        name="account"
      ></el-input>
    </el-form-item>
    <el-form-item class="login-form-pwd"  prop="password">
      <el-input
        type="password"
        size="large"
        placeholder="密码"
        show-password
        v-model="form.password"
        @input="handleInput('password')"
      ></el-input>
    </el-form-item>
    <el-form-item class="login-form-pwd"  prop="fiPassword">
      <el-input
        type="password"
        size="large"
        placeholder="确认密码"
        show-password
        v-model="form.fiPassword"
        @input="handleInput('fiPassword')"
      ></el-input>
    </el-form-item>
    <el-form-item class="login-form-pwd"  prop="verCode">
      <el-input
        type="code"
        size="large"
        placeholder="请输入验证码"
        v-model="form.verCode"
        style="flex: 1;padding-right: 20px;"
        @input="handleInput('verCode')"
      ></el-input>
      <img :src="imageUrl" alt="验证码" @click="getCode()" title="点击刷新验证码"/>
    </el-form-item>
    <el-form-item class="login-form-pwd"  prop="code">
      <el-input
        size="large"
        placeholder="请输入短信验证码"
        v-model="form.code"
      >
        <template #append >
          <div class="code-div">
            <span v-loading="loading" ref="spanRef" style="cursor: not-allowed; color: #909399;" @click="getCode()">获取验证码<span>
          </div>
        </template>
    </el-input>
    </el-form-item>
    <div class="not-code">
      <div class="toLogin-div" @click="linkToLogin">已有账号?返回登录</div>
      <div style="cursor: pointer;"  @click="codeVis=true">收不到验证码?</div>
    </div>
    
    <el-form-item class="login-form-btn">
      <el-button
        type="primary"
        size="large"
        :loading="subloading"
        style="width: 100%"
        @click="registAccount(formRef)"
        :disabled="!isAgree"
      >注 册</el-button>
    </el-form-item>
  </el-form>
  <div class="is-ser-div">
      <el-checkbox style="margin-top:20px" v-model="isAgree" size="large" />
      <span class="cont-gev">我同意 <router-link target="_blank" to="/serviceTerm" style="cursor: pointer;">《服务条款》</router-link><router-link target="_blank"  to="/privacyPolicy" style="cursor: pointer;"  >《用户隐私政策》</router-link></span>
  </div>
</div>
</template>
<script>
export default {
  name: "inviteView",
  setup() {
    // 手机号验证规则
    const checkMobile = (rule, value, callback) => {
      //手机号校验的正则
      const regMobile = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;    //11位手机号码正则
      if (regMobile.test(value)) {
        // 合法的手机号码
        return callback();
      }
      callback(new Error("手机号码格式不正确"));
    };

    // 确认密码
    const validateConfirmPassword = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('请输入确认密码'));
      } else if (value !== form.password) {
        callback(new Error('两次输入的密码不一致'));
      } else {
        // 确认密码验证通过
        callback();
      }
    };
    //密码校验
    const validPassword=(rule,value,callback)=>{
      if(value==='' || value ==undefined || value ==null){
        callback(new Error('请输入密码'))
      }
      if(value.length <5){
        callback(new Error('密码长度不小于6'))
      }else{
        callback() 
      }
    }

    const rules = reactive({
      account: [
        { required: true, message: '请输入手机号', trigger: 'blur' },
        { validator: checkMobile, trigger: "blur" },
      ],
      password: [{ required: true, message: '请输入密码', trigger: 'blur' },
      { validator: validPassword, trigger: "blur" }],
      fiPassword: [
        { required: true, message: '请输入确认密码', trigger: 'blur' },
        { validator: validateConfirmPassword, trigger: "blur" },
      ],
      code: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }],
      verCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
    })

    //判断account、password、fiPassword、verCode四个输入框是否都值且通过验证
    const validateFields = () => {
      if (!formRef.value) {
          console.log('formRef is not initialized yet');
          return;
      }

      const fields = ['account', 'password', 'fiPassword', 'verCode'];
      const isValid = fields.every(field => {
          const fieldEl = formRef.value.fields.find(f => f.prop === field);
          if (!fieldEl) {
              console.log(`Field ${field} not found`);
              return false;
          }
          console.log(`Field ${field} value:`, form[field]);
          console.log(`Field ${field} validateState:`, fieldEl.validateState);
          return fieldEl.validateState === 'success' && form[field];
      });

      if (isValid) {
          console.log('所有字段都已输入且验证通过');
          spanRef.value.style.color = "#1677ff"
      } else {
          console.log('一些字段有问题');
          spanRef.value.style.color = "#909399"
      }
    };

    //用于验证account、password、fiPassword、verCode输入框有值且通过验证后,改变"获取验证码"字体颜色
    const handleInput = (field) => {
      validateField(field).then(() => {
        // 验证完成后重新评估整体验证状态
        validateFields();
      });
    };

    //判断某个个输入框是否通过验证
    const validateField = async (field) => {
      try {
        await nextTick(); // 确保 DOM 更新后再进行验证
        await formRef.value.validateField(field);
        console.log(`Field ${field} validated successfully`);
      } catch (error) {
        console.error(`Validation failed for field ${field}:`, error);
      }
    };

  }
}
</script>
validateField
  • await nextTick(); 使用 nextTick 确保输入框的值已经同步到 DOM 中,所有依赖于 form.accountDOM 元素都已经更新
  • await formRef.value.validateField(field); 调用 formRef.value.validateField(field) 方法对指定字段进行验证,当一个输入框的规则验证通过后,其 validateState 的值会被设置为 success,以下是一些常见的验证状态及其含义:
    success:表示该字段的验证已经通过,没有错误。
    error:表示该字段的验证失败,存在错误。
    validating:表示该字段正在被验证,验证过程尚未完成。
    ''(空字符串):表示该字段还没有进行验证。
validateFields

fields.every(field => { … })

  • fields 是一个包含需要验证的字段名称的数组,例如 ['account', 'password', 'fiPassword', 'verCode']
  • every 方法会遍历数组中的每一个元素,并对每个元素调用提供的回调函数。如果回调函数对所有元素都返回 true,则 every 方法返回 true;否则返回 false

const fieldEl = formRef.value.fields.find(f => f.prop === field);

  • formRef.value 是表单组件的引用,formRef.value.fields 是表单组件中所有字段的集合。
  • find 方法用于查找数组中第一个满足条件的元素。这里查找的是 prop 属性等于当前字段名称 field 的字段对象。
  • fieldEl 是找到的字段对象,包含了该字段的各种属性和方法。

if (!fieldEl) { … }

  • 检查是否找到了对应的字段对象。如果没有找到,说明字段不存在。
  • 如果字段不存在,打印一条错误消息并返回 false,这会导致 every 方法立即返回 false,终止后续的验证。

console.log 语句

  • console.log(Field ${field} value:, form[field]);:打印当前字段的值。
  • console.log(Field ${field} validateState:, fieldEl.validateState);:打印当前字段的验证状态。

return fieldEl.validateState === ‘success’ && form[field];

  • 检查字段的验证状态是否为 'success',并且字段的值不为空。
  • fieldEl.validateState === 'success':确保字段的验证状态为 'success'
  • form[field]:确保字段的值不为空(即字段已经被填写)。
  • 如果这两个条件都满足,返回 true;否则返回 false

超出div宽度范围的文字进行省略号省略,鼠标移上去以后显示完整的内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
  <div class="app-line-ellipsis" title="这是很长的一段话">这是很长的一段话</div>
</template>
<style>
  .app-line-ellipsis {
    white-space: nowrap;  /* 不换行 */
    overflow: hidden; /* 超出容器内容的部分隐藏 */
    text-overflow: ellipsis;  /* 用省略号(...)表示被截断的文本 */
  }
  .app-line-ellipsis:hover {
    cursor: pointer;
  }
</style>

li

1
2
3
4
5
6
7
8
9
ul {
  list-style-type: disc;
  list-style-image: none;
  list-style-position: outside;
}
ul li {
  list-style-type: disc;
  list-style-position: inside;
}

list-style-type
这个属性定义了列表项标记(如圆点、数字等)的类型。

  • disc: 默认值,显示为实心圆点。
  • circle: 显示为空心圆圈。
  • square: 显示为实心方块。
  • none: 不显示任何标记。
  • decimal: 显示阿拉伯数字(1, 2, 3…)。
  • lower-alpha 或 lower-latin: 显示小写字母(a, b, c…)。
  • upper-alpha 或 upper-latin: 显示大写字母(A, B, C…)。
  • lower-roman: 显示小写罗马数字(i, ii, iii…)。
  • upper-roman: 显示大写罗马数字(I, II, III…)。

list-style-image
这个属性允许你使用自定义图像作为列表项标记。

  • url(): 指定一个图像文件的 URL。
  • none: 不使用图像作为标记,默认回退到 list-style-type 定义的标记。
1
2
3
ul {
  list-style-image: url('images/custom-bullet.png');
}

list-style-position
这个属性决定了列表项标记相对于文本的位置。

  • inside: 标记出现在文本内部,即与文本在同一行内对齐,可能会导致文本换行时被截断。
  • outside: 标记出现在文本外部,即在文本之前,不会影响文本的换行。这是默认值。

动态改变按钮状态

场景描述: 列表中每条数据都有一个添加按钮,点击后,会将该数据添加到一个数组A中,这个数组A会在其他位置渲染,渲染后数组A中每条数据后有一个移除按钮,列表中已经添加过的不能再添加,所以按钮禁用,除非在数组A渲染的位置点击移除按钮,移除后,添加按钮可用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<el-table :data="tableData" style="width: 100%" v-if="dialogVisible">
  <el-table-column prop="id" width="auto" v-if="false"/>
  <el-table-column prop="name" width="auto"/>
  <el-table-column width="auto">
    <template #default="{ row }">
      <div style="text-align: right;">
        <el-button
          type="primary"
          @click="addAssociate(row)"
          :disabled="isAssociated(row)"
        >添加</el-button>
      </div>
    </template>
  </el-table-column>
</el-table>

<script>
  export default {
    emits: ['updateReply'],
    setup(props,{emit}) {

      const addAssociate = (row) => {
        associateList.value.push(row)
      }
        
      // 动态改变添加按钮的状态
      const isAssociated = (row) => { 
        // 检查当前行的数据是否已经在associateList中
        return associateList.value.some(item => item.id === row.id);
      }
    }
  }
</script>

Vue

命令

打包:npm run build,打包后的文件夹为 dist
运行:npm run serve
进入 package-lock.json 所在的目录,执行 npm install,会安装 package-lock.json 中没有安装的包

场景

上传文件后,文件列表显示解析状态

之前:上传文件时解析,用户上传一个文件要等待很长的时间。
现在:上传时不解析,上传后在后台解析,轮询时查询列表接口 selectDocumentList
轮询在什么时候调用:刚进入页面时,在 onMounted 中会调用列表接口,在列表接口中,如果返回的有数据,就调用 startPolling 开始轮询。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
import { onMounted, reactive, ref, onBeforeUnmount, } from "vue";
export default {
    setup() {
      const pollingInterval = ref(10000); // 初始轮询间隔
      const maxInterval = 120000; // 最大轮询间隔
      const maxRetries = 5; // 最大重试次数
      let retries = 0; // 当前重试次数
      let pollingTimer = null; // 存储定时器的引用

      //启动轮询
      const startPolling = () => {
        //防止多个定时器并发
        if (pollingTimer) {
            clearInterval(pollingTimer);
        }
        //定时执行请求
        pollingTimer = setInterval(selectDocumentList(kbId.value), pollingInterval.value);
      };

      //停止轮询
      const stopPolling = () => {
        if (pollingTimer) {
            clearInterval(pollingTimer); // 清除定时器
            pollingTimer = null;
        }
      };

      //组件销毁时停止轮询
      onBeforeUnmount(() => {
        stopPolling();
      });
      
    }
}

预览上传的 xlsx 和 csv 文件

/images/posts/ElementPlus开发记录/10.png
(图10)

前提安装依赖包:npm install -S xlsx

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
<template>
    <div class="container">
        <div class="upload-div">
            <el-upload
                class="upload-demo"
                drag
                :action="uploadXlsxOrCsv"
                :on-remove="removeFile"
                :before-upload="beforeUploadFile"
                :on-success="successFile"
                :headers="requestData.hearders"
                list-type="text"
                :on-preview="previewFile"
                multiple
            >
                <el-icon class="el-icon--upload">
                    <svg viewBox="0 0 1024 1024"><path fill="currentColor" d="M544 864V672h128L512 480 352 672h128v192H320v-1.6c-5.376.32-10.496 1.6-16 1.6A240 240 0 0 1 64 624c0-123.136 93.12-223.488 212.608-237.248A239.808 239.808 0 0 1 512 192a239.872 239.872 0 0 1 235.456 194.752c119.488 13.76 212.48 114.112 212.48 237.248a240 240 0 0 1-240 240c-5.376 0-10.56-1.28-16-1.6v1.6z"></path></svg>
                </el-icon>
                <div class="el-upload__text">
                    Drop file here or <em>click to upload</em>
                </div>
                <!-- <template #tip>
                    <div class="el-upload__tip">
                        jpg/png files with a size less than 500kb
                    </div>
                </template> -->
            </el-upload>
        </div>

        <div class="preview-div">
            <el-table :data="tableData" border height="500" style="width: 1412px;">
                <el-table-column width="50px" fixed v-if="hasTableColumns">
                    <template v-slot="scope">
                        {{ scope.$index+1 }}
                    </template>
                </el-table-column>
                <el-table-column v-for="(column, index) in tableColumns" :key="index" :label="column.label" :prop="column.prop">
                </el-table-column>
            </el-table>
        </div>

        <div>
            <span>问题</span>
            <el-input v-model="questionVal" style="width: 100%" placeholder="Please input" @keyup.enter="sendQ()" @keydown.enter.prevent/>
        </div>

        <div v-loading="loading">
            <span>答案</span>
            <el-input v-model="answerVal" style="width: 100%" disabled/>
        </div>
    </div>
</template>
<script>
import { onMounted, ref, computed, reactive, } from "vue";
import * as xlsx from 'xlsx' 
import { uploadXlsxOrCsv } from '@/utils/config.js'
import { landRelevant } from "@/api/api.js";
import { ElMessage } from "element-plus";
export default {
    setup() {
        const tableData = ref([])
        const excelList = ref([])
        const tableColumns = ref([])
        const hasTableColumns = computed(() => tableColumns.value.length > 0);
        const questionVal = ref('')
        const answerVal = ref('')
        const uploadedFiles = ref([]);  //存储上传的文件
        const fileListArr = ref([])    //文件列表
        const loading = ref(false)
        const requestData = reactive({
            hearders: {
                "jwttoken":window.localStorage.getItem("token")
            },
        });

        onMounted(() => {
            clearArr()
         });

        const httpRequest = async (op) => {
            // 获取上传的excel  并解析数据
            // let file = op.file
            // console.log("op.file->",op.file)
            let dataBinary = await readFile(op);
            let workBook = xlsx.read(dataBinary, { type: "array", cellDates: true })
            let workSheet = workBook.Sheets[workBook.SheetNames[0]]
            const excelData = xlsx.utils.sheet_to_json(workSheet,{ header: 1 })
            excelList.value = excelData
            getTableData()
        }

        const readFile = (file) => {
    return new Promise((resolve, reject) => {
        let reader = new FileReader();
        reader.readAsArrayBuffer(file); // 使用 readAsArrayBuffer 替代 readAsBinaryString
        reader.onload = (ev) => {
            resolve(ev.target.result);
        };
        reader.onerror = reject;
    });
        }


        const getTableData = () => {
            // 根据每行的长度,获取表格的列数
            let numColumns = Math.max(...excelList.value.map(row => row.length));


            tableColumns.value = [];
            // for (let i = 1; i <= numColumns; i++) { // 注意从1开始,因为第一列是行号
            //     tableColumns.value.push({
                //         label: `列${i}`,
                //         prop: `col${i}`
                //     });
                // }
                
            let title = excelList.value[0]
            for (let i = 1; i <= numColumns; i++) { // 注意从1开始,因为第一列是行号
                tableColumns.value.push({
                    label: title[i-1],
                    prop: `col${i}`
                });
            }

            // excelList.value.forEach(row => {
            //     let obj = {};
            //     for (let i = 0; i < numColumns; i++) {
            //         let columnName = `col${i + 1}`;
            //         console.log("columnName->", columnName)
            //         console.log("row->", row)
            //         obj[columnName] = row[i];
            //     }
            //     console.log("obj->", obj)
            //     tableData.value.push(obj);
            // });

            for (let rowIndex = 1; rowIndex < excelList.value.length; rowIndex++) { 
                let obj = {};
                for (let i = 0; i < numColumns; i++) {
                    let columnName = `col${i + 1}`;
                    obj[columnName] = excelList.value[rowIndex][i];
                }
                tableData.value.push(obj);
            }
            
        }

        // 从文件列表中移除文件
        const removeFile = (file) => {
            // 更新 uploadedFiles 数组(如果需要)
            uploadedFiles.value = uploadedFiles.value.filter(f => f.uid !== file.uid);
            // 清空表格中的数据
            tableColumns.value = []
        }

        // 在文件上传前保存文件信息
        const beforeUploadFile = (file) => {
            // 清空表格中的数据
            tableColumns.value = []
            tableData.value = []

            uploadedFiles.value.push(file);
            return true;
        }

        // 文件上传成功后的处理函数
        const successFile = (response, file, fileList) => {

            // if (fileList.length > 1) {
            //     fileListArr.value = [file]; // 保留最新的文件
            // }
            console.log(response)
            console.log(fileList)

            // 从 uploadedFiles 中找到对应的文件
            const uploadedFile = uploadedFiles.value.find(f => f.uid === file.uid);

            if (uploadedFile) {
                // 在这里可以对上传的文件进行其他操作
                httpRequest(uploadedFile)
            }
        }

        const previewFile = (file) => { 
            // 清空表格中的数据
            tableColumns.value = []
            tableData.value = []

            // 从 uploadedFiles 中找到对应的文件
            const uploadedFile = uploadedFiles.value.find(f => f.uid === file.uid);

            if (uploadedFile) {
                // 在这里可以对上传的文件进行其他操作
                httpRequest(uploadedFile)
            }
        }

        const sendQ = () => {
            loading.value = true;

            // let params = {
            //     question: questionVal.value,
            // };
            landRelevant.askQuestion(questionVal.value).then((res) => {
                if (res.statusCode == 200) {
                    answerVal.value = res.data
                    loading.value = false
                } else {
                    if (res.message == "Checked in today") {
                        ElMessage({
                            message: "Checked in today!!",
                            type: "warning",
                        });
                    }
                }
            }).catch((e) => console.log("request accounts ERR", e));
        }

        // 清空文件数组
        const clearArr = () => {
            landRelevant.clearArr().then((res) => {
                if (res.statusCode == 200) {
                    console.log(res)
                } else {
                    if (res.message == "Checked in today") {
                        ElMessage({
                            message: "Checked in today!!",
                            type: "warning",
                        });
                    }
                }
            }).catch((e) => console.log("request accounts ERR", e));
        }


        return {
            httpRequest,
            excelList,
            readFile,
            tableData,
            getTableData,
            tableColumns,
            hasTableColumns,
            questionVal,
            sendQ,
            removeFile,
            uploadedFiles,
            uploadXlsxOrCsv,
            beforeUploadFile,
            successFile,
            requestData,
            previewFile,
            answerVal,
            fileListArr,
            loading,
            clearArr,
        };
    },
};
</script>
<style lang="scss" scoped>
.container {
    width: 100%;
    min-height: 100vh;
    overflow: auto;

    padding-left: 150px;
    padding-right: 150px;
    padding-top: 10px;

    display: flex;
    flex-direction: column;
    gap: 20px; /* 控制每对 div 之间的间距 */
}

</style>

实现文件列表只保留最新的一个文件
el-upload 中添加 :file-list="fileListArr"
以及取消注释

if (fileList.length > 1) {
    fileListArr.value = [file]; // 保留最新的文件
}

0%