vue实现文件预览和文件上传、下载、预览——多图、模型、dwg图纸、文档(word、excel、ppt、pdf)

整体思路(模型特殊不考虑,别人封装不具备参考性)

  1. 图片上传采用单独的组件,其他三种类型采用一个上传组件(仅仅文件格式不同)
  2. 文件上传采用前端直接上传阿里云的方式
  3. 图片预览使用elementUI自带的image预览
  4. dwg预览采用 kkfileview 方式,需要后台部署,前端使用 npm install js-base64 ,应该支持所有文件的预览,我们这里仅用作CAD文件的预览
  5. 文档类型的直接在浏览器打开预览,如果不支持需要拼接前缀 "https://view.officeapps.live.com/op/view.aspx?src="
  6. 图片下载为压缩包
  7. 文档类的相同方法下载

上传

js 复制代码
        <el-form
          style="width: 650px"
          :model="form"
          :rules="rules"
          ref="form"
          label-width="180px"
          class="demo-form"
          :disabled="type == 3"
        >
          <el-form-item label="样板格式" prop="modelType">
            <el-radio-group v-model="form.modelType">
              <el-radio label="1">图片样板</el-radio>
              <el-radio label="2">三维样板</el-radio>
              <el-radio label="3">图纸样板</el-radio>
              <el-radio label="4">文档样板</el-radio>
            </el-radio-group>
          </el-form-item>
         <!-- 1 图片样板 2 三维样板 3 图纸样板 4 文档样板 -->
         <el-form-item
            v-if="form.modelType == 1"
            label="样板文件上传"
            prop="url"
         >
            <ImageUpload
              v-model="form.url"
              tip="请上传图片:"
              :limit="10"
              :showIcon="type == 3 ? true : false"
            ></ImageUpload>
         </el-form-item>
         <el-form-item
            v-if="form.modelType != 1"
            label="样板文件上传"
            prop="url"
         >
            <ModelUpload
              v-if="type != 3"
              v-model="form.url"
              :limit="1"
              :fileSize="
                form.modelType == 3 ? 50 : form.modelType == 4 ? 10 : 5120
              "
              :isShowUploadModel="true"
              ref="videoUploadRef"
              @input="uploadTemplate"
              :fileType="
                form.modelType == 3
                  ? ['.dwg']
                  : form.modelType == 4
                  ? ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf']
                  : ['.rvt', '.ifc', '.obj', '.stl', '.fbx', '.3DS']
              "
            ></ModelUpload>
            <div v-else class="blue point" @click="preview(form)">
              {{ form.url?.split("/")[form.url?.split("/").length - 1] }}
            </div>
        </el-form-item>

ModelUpload.vue

js 复制代码
<template>
  <div class="component-upload-image">
    <el-upload
      action=""
      :http-request="beforeUpload"
      class="avatar-uploader"
      :limit="limit"
      :on-remove="handleDelete"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      name="file"
      :show-file-list="true"
      :file-list="fileList"
      ref="uploadRef"
      :on-preview="handlePreview"
      :data="otherQuery"
    >
      <el-button
        size="small"
        type="primary"
        v-if="!modelFlag"
        :disabled="disabled"
        >点击上传</el-button
      >
      <!-- 上传提示 -->
      <div class="el-upload__tip" slot="tip" v-if="showTip">
        <template v-if="fileSize">
          大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
        </template>
        <template v-if="fileType">
          文件类型支持
          <b style="color: #f56c6c">{{ fileType.join(" / ") }}</b> 格式
        </template>
      </div>
      <el-progress
        v-if="modelFlag == true"
        type="circle"
        :percentage="modelUploadPercent"
        style="margin-top: 7px"
      ></el-progress>
    </el-upload>
  </div>
</template>

<script>
export default {
  props: {
    value: [String, Object, Array],
    // 图片数量限制
    limit: {
      type: Number,
      default: 1,
    },
    // 大小限制(MB)
    fileSize: {
      type: Number,
      default: 5120,
    },
    fileType: {
      type: Array,
      default: () => [".rvt", ".ifc", ".dwg", ".obj", ".stl", ".fbx", ".3DS"],
    },
    // 是否显示提示
    isShowTip: {
      type: Boolean,
      default: true,
    },
    // 是否显示进度条
    isShowUploadModel: {
      type: Boolean,
      default: false,
    },
    // 是否显示重新上传按钮
    isShowBtn: {
      type: Boolean,
      default: true,
    },
    // 是否禁用上传按钮
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      number: 0,
      hideUpload: false,
      fileList: [],
      otherQuery: {}, //上传文件传递额外参数
      uploadList: [],
      modelFlag: false,
      modelUploadPercent: 0,
      isCancel: false,
    };
  },
  watch: {
    value: {
      handler(val) {
        if (val) {
          console.log(val, "val");
          if (typeof val == "string") {
            // 首先将值转为数组
            const list = Array.isArray(val) ? val : this.value.split(",");
            // 然后将数组转为对象数组
            this.fileList = list.map((item) => {
              if (typeof item === "string") {
                item = {
                  name: item.substring(item.lastIndexOf("/") + 14),
                  url: item,
                };
              }
              return item;
            });
          } else {
            val.forEach((el) => {
              el.name = el.fileName;
            });
            this.fileList = val;
          }
        } else {
          this.fileList = [];
          return [];
        }
      },
      deep: true,
      immediate: true,
    },
  },
  computed: {
    // 是否显示提示
    showTip() {
      return this.isShowTip && (this.fileType || this.fileSize);
    },
  },
  methods: {
    //自定义上传方法..
    Upload(file, data) {
      console.log(file);
      let OSS = require("ali-oss");
      let client = new OSS({
        region: data.region,
        accessKeyId: data.accessKeyId,
        accessKeySecret: data.accessKeySecret,
        // accessKeyId: "",
        // accessKeySecret: "",
        bucket: "cscec83-openfile",
      });
      // let cdnUrl = data.cdnUrl;
      let cdnUrl = "https://cscec83-openfile.oss-cn-shanghai.aliyuncs.com/";
      this.number++;
      this.isCancel = false;
      const progress = (p, _checkpoint) => {
        console.log(p);
        // console.log(_checkpoint);
        this.modelFlag = true;
        this.modelUploadPercent = Number((Number(p) * 100).toFixed(1));
        console.log(this.isCancel);
        if (this.isCancel) {
          client.cancel();
        }
      };
      let fileName = "model/" + new Date().getTime() + file.file.name;
      client
        .multipartUpload(fileName, file.file, {
          progress,
          // 设置并发上传的分片数量。
          // parallel: 4,
          // 设置分片大小。默认值为1 MB,最小值为100 KB。
          partSize: 5 * 1024 * 1024,
        })
        .then((res) => {
          // console.log(res, "res");
          this.modelFlag = false;
          if (res.name) {
            let obj = {
              fileName: res.name,
              name: res.name,
              size: this.otherQuery.size,
              url: cdnUrl + res.name,
            };
            console.log(cdnUrl + res.name);
            this.uploadList.push(obj);
            if (this.uploadList.length === this.number) {
              this.fileList = this.fileList.concat(this.uploadList);
              this.uploadList = [];
              this.number = 0;
              // let list = this.fileList.map((item) => {
              //   return {
              //     ...item,
              //     modelUrl: item.url,
              //   };
              // });
              this.$emit("input", this.fileList);
            }
          } else {
            this.$modal.msgError("上传失败,请重试");
            this.cancel();
          }
        })
        .catch((err) => {
          console.log(err);
          if (err.name == "cancel") {
            this.$message("上传取消");
          } else {
            this.$modal.msgError(err);
          }
          this.cancel();
        });
    },
    handleDelete(file) {
      const findex = this.fileList
        .map((f) => f.fileName)
        .indexOf(file.fileName);
      if (findex > -1) {
        this.fileList.splice(findex, 1);
        this.$emit("input", this.fileList);
      }
      this.cancel();
    },
    // 上传前loading加载
    beforeUpload(file) {
      console.log(this.fileType, file.file.name);
      this.otherQuery = {};
      this.isCancel = false;
      var fileSize = file.file.size / 1024 / 1024 < this.fileSize; //控制大小  修改50的值即可
      let fileType = file.file.name.substring(file.file.name.lastIndexOf("."));
      let isContinue = true;
      if (
        this.fileType.indexOf(fileType) == -1 //控制格式
      ) {
        this.$modal.msgError(
          `文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`
        );
        this.fileList = [];
        isContinue = false;
      }
      if (!fileSize) {
        this.$modal.msgError(`上传视频大小不能超过 ${this.fileSize} MB!`);
        this.fileList = [];
        isContinue = false;
      }
      this.otherQuery.fileName = file.file.name;
      this.otherQuery.size = (file.file.size / 1024 / 1024).toFixed(2);
      if (!isContinue) return;
      this.$axios.get("/file/ossFile/getOssParameter").then((res) => {
        if (res.code == 200) {
          this.Upload(file, res.data);
        }
      });
    },
    // 预览
    handlePreview(file) {
      window.open(file.url, "_blank");
    },
    // 文件个数超出
    handleExceed() {
      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
    },
    // 上传失败
    handleUploadError() {
      this.$modal.msgError("上传失败,请重试");
      // this.loading.close();
    },
    cancel(type) {
      this.isCancel = true;
      this.modelFlag = false;
    },
  },
};
</script>
<style scoped lang="css">
::v-deep.hideUpload .el-upload--picture-card {
  display: none;
}

::v-deep .el-upload--picture-card {
  width: 104px;
  height: 104px;
  line-height: 104px;
}

::v-deep .el-upload-list--picture-card .el-upload-list__item {
  width: 104px;
  height: 104px;
}

.avatar-uploader-icon {
  border: 1px dashed #d9d9d9 !important;
}

.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9 !important;
  border-radius: 6px !important;
  position: relative !important;
  overflow: hidden !important;
}

.avatar-uploader .el-upload:hover {
  border: 1px dashed #d9d9d9 !important;
  border-color: #409eff;
}

.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 300px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}

.avatar {
  width: 300px;
  height: 178px;
  display: block;
}
</style>

ImageUpload.vue

js 复制代码
<template>
  <div class="component-upload-image">
    <el-upload
      multiple
      :action="uploadImgUrl"
      list-type="picture-card"
      :on-success="handleUploadSuccess"
      :before-upload="handleBeforeUpload"
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      name="file"
      :disabled="showIcon"
      :on-remove="handleRemove"
      :show-file-list="true"
      :headers="headers"
      :file-list="fileList"
      :on-preview="handlePictureCardPreview"
      :class="{
        hideUpload: this.fileList.length >= this.limit || this.showIcon,
      }"
    >
      <i class="el-icon-plus"></i>
    </el-upload>

    <!-- 上传提示 -->
    <div class="el-upload__tip" slot="tip" v-if="showTip">
      {{ tip }}
      <template v-if="fileSize">
        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
      </template>
    </div>

    <el-dialog
      :visible.sync="dialogVisible"
      title="预览"
      width="800"
      append-to-body
    >
      <img
        :src="dialogImageUrl"
        style="display: block; max-width: 100%; margin: 0 auto"
      />
    </el-dialog>
  </div>
</template>

<script>
export default {
  props: {
    value: [String, Object, Array],
    // 图片数量限制
    limit: {
      type: Number,
      default: 5,
    },
    // 大小限制(MB)
    fileSize: {
      type: Number,
      default: 10,
    },
    // 文件类型, 例如['png', 'jpg', 'jpeg']
    fileType: {
      type: Array,
      default: () => ["png", "jpg", "jpeg"],
    },
    tip: {
      type: String,
    },
    // 是否显示提示
    isShowTip: {
      type: Boolean,
      default: true,
    },
    // 是否禁用添加图片按钮
    showIcon: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      number: 0,
      uploadList: [],
      dialogImageUrl: "",
      dialogVisible: false,
      hideUpload: false,
      uploadImgUrl: "/prod-api" + "/file/upload", // 上传的图片服务器地址
      headers: {
        Authorization: this.$store.state.token,
      },
      fileList: [],
    };
  },
  watch: {
    value: {
      handler(val) {
        if (val) {
          // 首先将值转为数组
          const list = Array.isArray(val) ? val : this.value.split(",");
          // 然后将数组转为对象数组
          this.fileList = list.map((item) => {
            if (typeof item === "string") {
              item = { name: item, url: item };
            }
            return item;
          });
        } else {
          this.fileList = [];
          return [];
        }
      },
      deep: true,
      immediate: true,
    },
  },
  computed: {
    // 是否显示提示
    showTip() {
      return this.isShowTip && (this.fileType || this.fileSize);
    },
  },
  methods: {
    // 删除图片
    handleRemove(file, fileList) {
      const findex = this.fileList.map((f) => f.name).indexOf(file.name);
      if (findex > -1) {
        this.fileList.splice(findex, 1);
        this.$emit("input", this.listToString(this.fileList));
      }
    },
    // 上传成功回调
    handleUploadSuccess(res) {
      this.uploadList.push({ name: res.data.url, url: res.data.url });
      if (this.uploadList.length === this.number) {
        this.fileList = this.fileList.concat(this.uploadList);
        this.uploadList = [];
        this.number = 0;
        this.$emit("input", this.listToString(this.fileList));
        this.$modal.closeLoading();
      }
    },
    // 上传前loading加载
    handleBeforeUpload(file) {
      let isImg = false;
      if (this.fileType.length) {
        let fileExtension = "";
        if (file.name.lastIndexOf(".") > -1) {
          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
        }
        isImg = this.fileType.some((type) => {
          if (file.type.indexOf(type) > -1) return true;
          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
          return false;
        });
      } else {
        isImg = file.type.indexOf("image") > -1;
      }

      if (!isImg) {
        this.$modal.msgError(
          `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
        );
        return false;
      }
      if (this.fileSize) {
        const isLt = file.size / 1024 / 1024 < this.fileSize;
        if (!isLt) {
          this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
          return false;
        }
      }
      this.$modal.loading("正在上传图片,请稍候...");
      this.number++;
    },
    // 文件个数超出
    handleExceed() {
      this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
    },
    // 上传失败
    handleUploadError() {
      this.$modal.msgError("上传图片失败,请重试");
      this.$modal.closeLoading();
    },
    // 预览
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    // 对象转成指定字符串分隔
    listToString(list, separator) {
      let strs = "";
      separator = separator || ",";
      for (let i in list) {
        strs += list[i].url + separator;
      }
      return strs != "" ? strs.substr(0, strs.length - 1) : "";
    },
  },
};
</script>
<style scoped >
::v-deep.hideUpload .el-upload--picture-card {
  display: none;
}

::v-deep .el-upload--picture-card {
  width: 86px;
  height: 86px;
  line-height: 86px;
}

::v-deep .el-upload-list--picture-card .el-upload-list__item {
  width: 86px;
  height: 86px;
}

::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {
  transition: all 0s;
}

::v-deep .el-list-enter,
.el-list-leave-active {
  opacity: 0;
  transform: translateY(0);
}
</style>

预览和下载

js 复制代码
<template>
  <div id="index">
      <el-card shadow="always" style="margin-top: 20px">
        <div class="model pd-16">
          <div class="flex mb-20">
            <div class="title-left bold size-22">工程样板</div>
            <div
              class="flex_r point border-blue blue plr-10 ptb-4 bg-white radius-4 size-14"
              @click="toTemplate"
            >
              查看更多 <i class="el-icon-right"></i>
            </div>
          </div>
          <div class="flex_l">
            <div
              class="center mr-20 plr-20 ptb-10"
              @click="changeTab(1)"
              :style="
                tab == 1
                  ? 'background: linear-gradient(210.65deg, #5FA4FE 0%, #367EF8 100%);border-radius: 25px;color:#fff'
                  : ''
              "
            >
              <img
                :src="`${
                  tab == 1
                    ? require('../../assets/img/zhi-tab-1-1.png')
                    : require('../../assets/img/zhi-tab-1.png')
                }`"
                alt=""
                style="width: 27px"
              />
              <div class="ml-16 size-18">图片区</div>
            </div>
            <div
              class="center mr-20 plr-20 ptb-10"
              @click="changeTab(2)"
              :style="
                tab == 2
                  ? 'background: linear-gradient(210.65deg, #5FA4FE 0%, #367EF8 100%);border-radius: 25px;color:#fff'
                  : ''
              "
            >
              <img
                :src="`${
                  tab == 2
                    ? require('../../assets/img/zhi-tab-4-1.png')
                    : require('../../assets/img/zhi-tab-4.png')
                }`"
                alt=""
                style="width: 27px"
              />
              <div class="ml-16 size-18">三维模型区</div>
            </div>
            <div
              class="center mr-20 plr-20 ptb-10"
              @click="changeTab(3)"
              :style="
                tab == 3
                  ? 'background: linear-gradient(210.65deg, #5FA4FE 0%, #367EF8 100%);border-radius: 25px;color:#fff'
                  : ''
              "
            >
              <img
                :src="`${
                  tab == 3
                    ? require('../../assets/img/zhi-tab-3-1.png')
                    : require('../../assets/img/zhi-tab-3.png')
                }`"
                alt=""
                style="width: 27px"
              />
              <div class="ml-16 size-18">图纸区</div>
            </div>

            <div
              class="center mr-20 plr-20 ptb-10"
              @click="changeTab(4)"
              :style="
                tab == 4
                  ? 'background: linear-gradient(210.65deg, #5FA4FE 0%, #367EF8 100%);border-radius: 25px;color:#fff'
                  : ''
              "
            >
              <img
                :src="`${
                  tab == 4
                    ? require('../../assets/img/zhi-tab-2-1.png')
                    : require('../../assets/img/zhi-tab-2.png')
                }`"
                alt=""
                style="width: 27px"
              />
              <div class="ml-16 size-18">文档区</div>
            </div>
          </div>
          <el-row :gutter="20" class="mt-30">
            <div v-if="templateList.length == 0" class="text-center">
              <img src="../../assets/img/null.png" alt="" style="width: 4rem" />
              <div>暂无数据</div>
            </div>
            <el-col :span="6" v-for="(item, i) in templateList" :key="i + 'c'">
              <el-card shadow="hover" class="point course mb-12">
                <el-image
                  v-if="tab == 1"
                  style="width: 100%; height: 170px; border-radius: 5px"
                  mode="aspectFill"
                  :src="item.pictureUrl"
                  :preview-src-list="[item.url.split(',')]"
                >
                </el-image>
                <img
                  @click="preview(item)"
                  v-if="tab == 2 || tab == 3 || tab == 4"
                  :src="
                    tab == 4
                      ? require(`../../assets/img/doc-${
                          item.docType || 'word'
                        }.png`)
                      : item.pictureUrl
                  "
                  style="width: 100%; height: 170px; border-radius: 5px"
                  mode="aspectFill"
                />
                <div class="flex mtb-10 mlr-12">
                  <div class="line-1 mr-10">{{ item.modelName }}</div>
                  <img
                    style="width: 22px"
                    src="../../assets/img/download.png"
                    alt=""
                    @click.stop="download(item)"
                  />
                </div>
              </el-card>
            </el-col>
          </el-row>
        </div>
      </el-card>

      <modelPop ref="modelPopRef"></modelPop>
    </div>
  </div>
</template>

<script>
// npm install js-base64
import { Base64 } from "js-base64";
export default {
  // layout: "default-all",
  data() {
    return {
      tab: 1,
      templateList: [],
    };
  },
  mounted() { 
    this.getTemplateList();
  },
  methods: {
    changeTab(tab) {
      this.tab = tab;
      this.getTemplateList();
    },
    // 下载
    download(item) {
      // 1 图片样板 2 三维样板 3 图纸样板 4 文档样板
      if (this.tab == 1) {
        this.$downloadZip.zip(
          "/qualityTrain/sample/downloadAndZip?sampleId=" + item.sampleId,
          item.modelName + "样板文件.zip"
        );
      } else if (this.tab == 2 || this.tab == 3) {
        window.open(item.url);
      } else if (this.tab == 4) {
        fetch(item.url)
          .then((res) => res.blob())
          .then((blob) => {
            const a = document.createElement("a");
            const objectUrl = window.URL.createObjectURL(blob);
            a.download =
              item.modelName +
              (item.docType == "ppt"
                ? ".ppt"
                : item.docType == "excel"
                ? ".xls"
                : item.docType == "word"
                ? ".doc"
                : ".pdf");
            a.href = objectUrl;
            a.click();
          });
      }
    },
     
    getTemplateList() {
      this.$axios
        .get("/qualityTrain/sample/homePage", {
          params: {
            modelType: this.tab,
          },
        })
        .then((res) => {
          this.templateList = res.data;
        });
    },
    // 预览
    preview(item) {
      // 1 图片样板 2 三维样板 3 图纸样板 4 文档样板
      if (this.tab == 2) {
        this.$refs.modelPopRef.init(item.sampleId, item.modelName, false, true);
      } else if (this.tab == 3) {
        let base = location.protocol + "//" + location.hostname + ":8012";
        let url =
          (base.includes("192.168.2.89") ? "http://192.168.0.19:8012" : base) +
          "/onlinePreview?url=" +
          encodeURIComponent(Base64.encode(item.url));
        window.open(url, "_blank");
      } else {
        console.log(item.docType);
        window.open(
          (item.docType != "pdf"
            ? "https://view.officeapps.live.com/op/view.aspx?src="
            : "") + item.url
        );
      }
    },
  },
};
</script> 

downloadZip.js

js 复制代码
import axios from 'axios'
import { saveAs } from 'file-saver'
import { Loading, Message } from 'element-ui';
export default ({ store }, inject) => {
  const zip = (url, name) => {
    let loadingInstance = Loading.service({
      lock: true,
      text: '正在下载...',
      spinner: 'el-icon-loading',
      background: 'rgba(0, 0, 0, 0.7)'
    });
    const fullUrl = '/prod-api' + url;
    axios({
      method: 'get',
      url: fullUrl,
      responseType: 'blob',
      timeout: 600000,
      headers: { 'Authorization': store.state.token }
    }).then(async (res) => {
      const isLogin = await blobValidate(res.data);
      loadingInstance.close();
      if (isLogin) {
        const blob = new Blob([res.data], { type: 'application/zip' })
        saveAs(blob, name)
      } else {
        printErrMsg(res.data);
      }
    }).catch(() => {
      loadingInstance.close();
    });
  };
  // 验证是否为blob格式
  async function blobValidate(data) {
    try {
      const text = await data.text();
      JSON.parse(text);
      return false;
    } catch (error) {
      return true;
    }
  }
  let errorCode = {
    '401': '认证失败,无法访问系统资源',
    '403': '当前操作没有权限',
    '404': '访问资源不存在',
    'default': '系统未知错误,请反馈给管理员'
  }

  async function printErrMsg(data) {
    const resText = await data.text();
    const rspObj = JSON.parse(resText);
    const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
    Message.error(errMsg);
  }
  // 注入到全局上下文中
  inject('downloadZip', {
    zip,
  });
}
相关推荐
码不停T40 分钟前
乾坤项目学习总结
vue.js·qiankun·乾坤
customer081 小时前
【开源免费】基于SpringBoot+Vue.J影城管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源
jason_yang1 小时前
vue3复习-源码-迷你版vite
vue.js·vite
jason_yang2 小时前
vue3复习-源码-编译原理-自定义vite插件
vue.js·vite
热爱前端的小君同学2 小时前
实现插入公式的富文本框(tinymce+kityformula-editor)
vue.js
计算机学姐3 小时前
基于协同过滤算法的旅游网站推荐系统
vue.js·mysql·算法·mybatis·springboot·旅游·1024程序员节
✎﹏ℳ๓₯㎕3 小时前
el-table实现固定列相同合并切重排序号
javascript·vue.js·elementui
海绵宝宝不喜欢侬3 小时前
vue + elementui 全局Loading效果
前端·vue.js·elementui
会发光的猪。4 小时前
uniapp+华为HBuilder X 4.29跑鸿蒙模拟器报错没有签名授权
javascript·vue.js·华为·uni-app·bug·harmonyos·1024程序员节
神奇夜光杯4 小时前
Python酷库之旅-第三方库Pandas(181)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长