阿里云上传组件+jQuery实现翻书效果

52 阅读6分钟

image.png

image.png

image.png

image.png

上传图片组件:

<template>
  <div>
    <el-upload
        v-model:file-list="fileList"
        :action="uploadUrl"
        list-type="picture-card"
        :on-preview="handlePictureCardPreview"
        :on-remove="handleRemove"
        :limit="1"
        :http-request="uploadFormSuccess"
        :before-upload="beforeUpload"
        :class="[
          fileList.length > 0 ? 'showSelect el-dialog__body' : 'hideSelect el-dialog__body',
          props.disableRemove ? 'disable-remove' : 'allow-remove'
        ]"
    >
      <el-icon>
        <Plus/>
      </el-icon>
    </el-upload>



    <el-dialog
        v-model="dialogVisible"
        center
        append-to-body
        align-center
        width="'100%'"
    >
      <img :src="dialogImageUrl"   />

    </el-dialog>
  </div>
</template>

<script setup>
import {ref} from 'vue'
import {ElMessage, dayjs, ElLoading} from "element-plus";
import request from "../../utils/request";
import {globalConfigs} from "../../utils/globalConfigs";
import {imageTypeList, pdfList} from "../../utils/fileTypeList";
import {v4 as uuidv4} from "uuid";
import {uploadWithToken} from "../../api/login";
import {bucketUpload} from "../../utils/fileUpload";

const props = defineProps({
  //编辑时传入的图片链接
  oldUrl: {
    type: String,
    default: ''
  },
  //拼接新文件名的前缀
  newfileTitle: {
    type: String,
    default: ''
  },
  disableRemove: {
    type: Boolean,
    default: false
  }
})

//预览用的图片链接
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const progressPercent = ref(0)

//上传的文件
const fileList = ref([])
//文件原名称
const oldFileName = ref('')

// 修改 uploadFlag 为 ref,以便能够正确追踪状态
const uploadFlag = ref(false)

let token = ''

if (![null, undefined, ''].includes(props.oldUrl)) {
  fileList.value.push({
    url: props.oldUrl
  })
}

const emits = defineEmits(['success'])

const uploadUrl = globalConfigs.uploadUrl()

const beforeUpload = async (file) => {
  if (uploadFlag.value) {
    return false
  }
  uploadFlag.value = true
  oldFileName.value = file.name
  if (!imageTypeList.hasOwnProperty(oldFileName.value.split(".")[oldFileName.value.split(".").length - 1])) {
    ElMessage({type: "error", message: "仅支持上传图片!"})
    uploadFlag.value = false
    return false;
  }

  //uuid随机生成
  let uuid = uuidv4()
  let query = {
    uuid
  }
  let res = await uploadWithToken(query)
  if(!res){
    uploadFlag.value = false
    return false
  }
  token = res
  return true
}

const handlePictureCardPreview = (uploadFile) => {
  dialogImageUrl.value = uploadFile.url || uploadFile.response?.url
  dialogVisible.value = true
}

function changeRadioValue(value) {
  if (![null, undefined, ''].includes(value) && fileList.value.length < 1) fileList.value.push({url: value})
}

watch(() => props.oldUrl, value => changeRadioValue(value))

const handleRemove = () => {
  fileList.value = []
  emits('success', '')
}

const beforeRemove = ()=>{
  if(props.disableRemove){
    return false
  }
}
const uploadFormSuccess = async (options) => {
  const { file, onProgress, onSuccess, onError } = options
  bucketUpload(file,
      (res) => {
        let successRes = {
          url:res.aliyunAddress,
          filename:res.name,
          oldFilename:oldFileName.value,
        }
        emits('success',successRes)
        uploadFlag.value = false
        onSuccess(res)
      },
      (err) => {
        onError(err)            // 让 el-upload 知道失败
        ElMessage.error('上传失败')
        uploadFlag.value = false
      },
  )
};
</script>

<style scoped>
:deep(.showSelect .el-upload--picture-card) {
  display: none;
}

:deep(.showSelect .el-upload-list__item-actions) {
  display: inline-flex;
}

:deep(.hideSelect .el-upload--picture-card) {
  display: inline-flex;
}

:deep(.hideSelect .el-upload-list__item-actions) {
  display: none;
}

.fullscreen-dialog {
  margin: 0 !important; /* 移除默认的上下外边距 */
  position: absolute;
  top: 0;
  left: 0;

  display: flex;
  align-items: center;
  justify-content: center;
}


:deep(.el-dialog__body){
  padding: 30px 20px;
  color: #606266;
  font-size: 14px;
  word-break: break-all;
}

:deep(.el-upload-list__item .el-upload-list__item-actions .el-upload-list__item-delete){
  display: none;
}

:deep(.disable-remove .el-upload-list__item .el-upload-list__item-actions .el-upload-list__item-delete) {
  display: none;
}
:deep(.allow-remove .el-upload-list__item .el-upload-list__item-actions .el-upload-list__item-delete) {
  display: inline-flex;
}

</style>

上传文件组件:

<template>
  <div class="upload-container">
    <el-upload
        :action="uploadUrl"
        ref="upload"
        :limit="1"
        :http-request="uploadFormSuccess"
        :before-upload="beforeUpload"
        :on-exceed="handleExceed"
    >
      <!--    按钮上传-->
      <el-button
          icon="Plus"
          type="primary"
          style="width: 200px"
      >{{buttonName}}</el-button>
    </el-upload>
    <span v-if="oldFile && showOldFileName" class="file-name">{{ oldFile }}</span>
  </div>
</template>

<script setup>
import { v4 as uuidv4 } from 'uuid'

import { ElMessage, genFileId} from "element-plus";
import {getCurrentInstance, ref} from 'vue';
import {globalConfigs} from "../../utils/globalConfigs";
import {imageTypeList, pdfList, videoTypeList} from "../../utils/fileTypeList";
import {uploadWithToken} from "../../api/login";
import {bucketUpload} from "../../utils/fileUpload";

const props = defineProps({
  oldFilename:{
    type:String,
    default:''
  },
  newFileTitle:{
    type:String,
    default:''
  },
  onlyVideo:{
    type:Boolean,
    default:false
  },
  onlyPdf:{
    type:Boolean,
    default:false
  },
  onlyPic:{
    type:Boolean,
    default:false
  },
  //  按钮旁是否显示文件名
  showOldFileName:{
    type:Boolean,
    default:true
  },
  buttonName:{
    type:String,
    default:'上传'
  },
  needExcced:{
    type:Boolean,
    default:true
  }
})
const visible = ref(false)
const uploadUrl = globalConfigs.uploadUrl()
const emits = defineEmits(['success'])
//oldFile为当前原名称
const oldFile = ref(props.oldFilename)
const progressPercent = ref(0)
const upload = ref()
const { proxy } = getCurrentInstance();


let token = ''

const handleExceed  = (files) => {
  if(props.needExcced){
    upload.value.clearFiles()
    const file = files[0]

    file.uid = genFileId()
    upload.value.handleStart(file)
  }
}

const beforeUpload = async (file) => {
  upload.value.clearFiles()
  if(props.onlyVideo){
    if (!videoTypeList.hasOwnProperty(file.name.split('.')[file.name.split('.').length - 1])) {
      ElMessage.error('仅支持上传视频')
      return false
    }
  }
  if(props.onlyPic){
    if (!imageTypeList.hasOwnProperty(file.name.split('.')[file.name.split('.').length - 1])) {
      ElMessage.error('仅支持上传图片')
      return false
    }
  }
  //uuid随机生成
  let uuid = uuidv4()
  let query = {
    uuid
  }
  let res = await uploadWithToken(query)
  if(!res){
    return false
  }
  token = res
  return true
}

const uploadFormSuccess = async (options) => {
  const { file, onProgress, onSuccess, onError } = options
  oldFile.value = file.name
  bucketUpload(file,
      (res) => {
        let successRes = {
          url:res.aliyunAddress,
          oldFilename:oldFile.value,
          filename:res.name,
        }
        emits('success',successRes)
        onSuccess(res)          // 让 el-upload 知道成功
        proxy.$modal.msgSuccess("上传成功");
      },
      (err) => {
        onError(err)            // 让 el-upload 知道失败
        proxy.$modal.msgError("上传失败");
      },
  )
};

function changeRadioValue(value){
  oldFile.value = value
}

watch(() => props.oldFilename, value => changeRadioValue(value))

</script>

<style scoped>
.upload-container {
  display: flex;
  align-items: center;
  gap: 10px;
}

.file-name {
  color: #606266;
}
</style>

上传文件列表:

<template>
<div>
  <div class="top">
    <div style="width: 250px">请输入页码后再上传</div>
    <el-input-number v-model="picPages" style="width: 350px;margin-right: 30px" />
    <upload-button ref="uploadButton" @success="getUrl" :only-pic="true" :filename-use-old-file="true" :show-old-file-name="false" :need-excced="false"></upload-button>
  </div>

  <el-table
      v-loading="loading"
      :data="dataList"
  >
    <el-table-column label="图片" align="center" prop="fileUrl">
      <template #default="scope">
        <template v-if="scope.row.fileUrl">
          <el-image
              style="width: 100px; height: 100px"
              :src="scope.row.fileUrl"
              :zoom-rate="1.2"
              :max-scale="7"
              :min-scale="0.2"
              :preview-src-list="srcList(scope.row.fileUrl)"
              :z-index="9999"
              preview-teleported
          />
        </template>
        <template v-else>
          {{scope.row.fileUrl}}
        </template>

      </template>
    </el-table-column>
    <el-table-column label="页码" align="center" prop="pages" />
    <el-table-column
        label="操作"
        align="center"
        class-name="small-padding fixed-width"
        fixed="right"
        :min-width="120"
    >
      <template #default="scope">
        <el-button
            link
            type="primary"
            @click="handleEditPage(scope.row)"
            :icon="Edit"
        >修改页码</el-button>
        <el-button
            link
            type="danger"
            @click="handleDelete(scope.row)"
            :icon="Delete"
        >删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <el-dialog
      v-model="innerVisible"
      width="300"
      title="修改页码"
      append-to-body
  >
    <el-input-number v-model="innerForm.pages" style="width: 150px" />
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </template>
  </el-dialog>
</div>
</template>

<script setup>
import {getCurrentInstance} from "vue";

import {Delete,Edit} from '@element-plus/icons-vue';

import {reactive, toRefs} from "vue";
import {
  addOfficialKnowledgeFile,
  deleteOfficialKnowledgeFile,
  listOfficialKnowledgeFile, putOfficialKnowledgeFile
} from "../../api/official/knowledge";
import UploadButton from "../uploadButton/uploadButton.vue";

const {proxy} = getCurrentInstance()
//弹窗
const innerVisible = ref(false)
const innerForm = ref({})
//表格
const state = reactive({
  loading: false,
  total: 0,
  dataList: [],
  queryParams: {
    pageNum: 1,
    pageSize: 100,
  },
});
const picPages = ref(1)
const addIndex = ref(0)

const emits = defineEmits(['getFileList'])


// 将响应式数据解构出来
const { loading, total, dataList, queryParams} = toRefs(state);

const form = ref({})

//弹窗
function cancel() {
  innerForm.value = {}
  innerVisible.value = false
}
async function submitForm() {
  const pk = queryParams.value.pkKnowledge
  if ([null, undefined, ''].includes(pk)) {
    //  新增主表记录 修改页码
    for(let i=0;i<dataList.value.length;i++){
      if(dataList.value[i].index === innerForm.value.index){
        dataList.value[i].pages = innerForm.value.pages
        cancel()
        return
      }
    }
  } else {
    //  修改主表记录 修改页码
    await putOfficialKnowledgeFile(innerForm.value)
    getList()
    cancel()
  }
}

//表格
const srcList = (url) =>{
  let list = []
  list.push(url)
  return list
}

//上传获取url及文件原名 新名
const getUrl = async (res) => {
  const pk = queryParams.value.pkKnowledge
  form.value = {}
  form.value.fileUrl = res.url
  form.value.fileName = res.filename
  form.value.pages = picPages.value
  if ([null, undefined, ''].includes(pk)) {
    form.value.index = addIndex.value
    dataList.value.push(form.value)
    addIndex.value++
    picPages.value = 1
    emits('getFileList', dataList.value)
  } else {
    form.value.pkKnowledge = pk
    await addOfficialKnowledgeFile(form.value)
    picPages.value = 1
    getList()
  }
}

const getList = async (query) => {
  if(query) queryParams.value.pkKnowledge = query.pkKnowledge
  state.loading = true;
  let response = await listOfficialKnowledgeFile(queryParams.value)
  state.dataList = response.rows;
  state.total = response.total;
  state.loading = false;
};

function handleEditPage(row) {
  innerVisible.value = true
  innerForm.value = JSON.parse(JSON.stringify(row))
}

function handleDelete(row){
  const pk = queryParams.value.pkKnowledge
  if([null,undefined,''].includes(pk)){
    // dataList去除此条记录
    dataList.value.splice(dataList.value.indexOf(row),1)
    emits('getFileList',dataList.value)
  }else{
    proxy.$modal.confirm('是否确认页码为"' + row.pages + '"的数据项?,删除后将无法恢复!').then(function () {
      return deleteOfficialKnowledgeFile(row.pkKnowledgeFile);
    }).then(() => {
      getList();
      proxy.$modal.msgSuccess("删除成功");
    }).catch(() => { });
  }
}
defineExpose({
  getList
})
</script>

<style scoped>
:deep(.el-table tr){
  height: 50px;
}

.top{
  width: 650px;
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
}
</style>

上传阿里云js:

import request from "@/utils/request";
import OSS from "ali-oss";
import {dayjs} from "element-plus";

//生成uuid,防止同名文件被覆盖
function getUUID() {
    return dayjs().format("YYYYMMDD") + Math.floor(Math.random() * 100000000)
}

/**
 * 阿里云oss sdk文件上传
 * @param {*} file 文件流
 * @param {*} successCallback 成功回调
 * @param {*} errCallBack 失败回调
 * @param {*} bucketName 阿里云桶名(可以指定多个桶名)
 * @param {*} dir 上传文件夹路径  譬如images
 */

export function bucketUpload(
    file,
    successCallback = new Function(),
    errCallBack = new Function(),
    dir = ""
) {
    let fileName = file.name;
    // 先获取上传要的资料签名
    request({
        method: "get",
        url: "/auth/token/getOssToken", // 返回new OSS需要的参数
    })
        .then((res) => {
            let obj = res || {};
            let config = {};
            config.host = "oss-cn-shanghai.aliyuncs.com";
            // 实例化一个上传客户端
            const client = new OSS({
                // yourRegion填写Bucket所在地域。比如oss-cn-hangzhou。
                region: "oss-cn-shanghai",
                // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
                accessKeyId: obj.accessKeyId,
                accessKeySecret: obj.accessKeySecret,
                // 从STS服务获取的安全令牌(SecurityToken)。
                stsToken: obj.securityToken,
                // 填写Bucket名称。
                bucket: "",
                // 添加以下配置项以使用 HTTPS
                secure: true,
            });

            try {
                // 填写Object完整路径。Object完整路径中不能包含Bucket名称。
                // 您可以通过自定义文件名(例如exampleobject.txt)或文件完整路径(例如exampledir/exampleobject.txt)的形式实现将数据上传到当前Bucket或Bucket中的指定目录。
                // data对象可以自定义为file对象、Blob数据或者OSS Buffer。

                // 为保证唯一性,通过uuid将文件名替换
                let uuid = getUUID() + fileName.substring(fileName.lastIndexOf("."));
                if (dir.substring(dir.length - 1, 1) !== "/") {
                    dir += "/";
                }
                const result = client.put(dir + uuid, file);
                result.then(async (res) => {
                    let size =
                        file.size > 1000000
                            ? parseFloat(file.size / 1000000).toFixed(2) + "M"
                            : parseFloat(file.size / 1000).toFixed(2) + "KB";

                    //获取私有地址
                    let privateUrl = await request({
                        method: "get",
                        url: "/auth/token/getOssPri",
                        params: {
                            fileName: res.name,
                        },
                    });
                    successCallback({
                        name: res.name,
                        attachment: fileName,
                        aliyunAddress: privateUrl.data,
                        size: size,
                        host: config.host,
                    });
                })
                    .catch((err) => {
                        errCallBack(err);
                    });
            } catch (e) {
                console.log(e);
            }
        })
        .catch((err) => {
            errCallBack(err);
        });
}

翻书动画:

<template>
  <div class="turn-container">
    <div class="turn-banner">
      <div class="turn-content">
        <div id="flipbook">
          <el-image v-for="(item, index) in images" :key="index" fit="fill" :src="item.url" alt="" srcset="" />
        </div>
      </div>
      <div class="slider-bar">
        <div v-for="(item, index) in images" :key="index" class="slider" :class="{ 'slider-current': index + 1 == currentPage }" @click="toPage(index)"></div>
      </div>
    </div>
  </div>
</template>
<script>
import { ref, nextTick, onMounted } from 'vue'
import turn from '@/utils/turn.js'
import $ from 'jquery'
import {getKnowledge} from "@/api/waterSupply/knowledge";
import {useRoute} from "vue-router";
export default {
  components: {},
  setup() {
    const route = useRoute()
    const currentPage = ref(1)
    const images = ref([])

    onMounted(async () => {
      await getDetail()
      onTurn()
    })

    const onTurn = () => {
      nextTick(() => {
        $('#flipbook').turn({
          autoCenter: true,
          height: 646, //高度
          width: 996, //宽度
          display: 'double', //单页显示/双页显示  single/double
          elevation: 50,
          duration: 500, //翻页速度(毫秒), 默认600ms
          gradients: true, //翻页时的阴影渐变, 默认true
          autoCenter: true, //自动居中, 默认false
          acceleration: true, //硬件加速, 默认true, 如果是触摸设备设置为true
          page: 1, //设置当前显示第几页
          pages: images.value.length, //总页数
          when: {
            //监听事件
            turning: function (e, page, view) {
              // console.log(e, page, view)
              // 翻页前触发
            },
            turned: function (e, page) {
              // console.log(e, page)
              currentPage.value = page
              // 翻页后触发
            },
          },
        })
      })
    }

    const toPage = i => {
      currentPage.value = i + 1
      $('#flipbook').turn('page', currentPage.value) //进度条跳转到对应的页数
    }

    async function getDetail() {
      try {
        let res = await getKnowledge(route.query.pk);
        if (!res || res.code !== 200) {
          return;
        }
        let knowledgeFiles = res.data.knowledgeFiles;
        for(let i=0;i<knowledgeFiles.length;i++){
          images.value.push({
            url: knowledgeFiles[i].fileUrl,
          });
        }
      } catch (e) {
      }
    }
    return {
      images,
      toPage,
      getDetail,
      currentPage,
    }
  },
}
</script>
<style lang="scss" scoped>
.turn-banner {
  width: 100%;
  height: 700px;
  .turn-content {
    display: flex;
    margin: 0px auto;
    overflow: hidden;
  }
}
.slider-bar {
  width: 900px;
  height: 8px;
  border-radius: 5px;
  background-color: #ccc;
  margin-top: 15px;
  display: flex;
  overflow: hidden;
  .slider {
    flex: 1;
    cursor: pointer;
  }
  .slider-current {
    background-color: #666;
    border-radius: 5px;
  }
}
</style>