Nuxt3+naive-ui表单和上传插件的使用
上代码把,代码里有注释,anyscript【捂脸】将就看
<template>
<div class="set-page">
<ClientOnly>
<n-config-provider :theme-overrides="themeOverrides">
<n-form ref="formRef" :model="model" label-placement="left" label-width="110"
:show-require-mark="false" require-mark-placement="right-hanging" size="medium">
<div class="flex justify-between w-full">
<n-form-item label="性别" :validation-status="errorsObj.sex ? 'error' : undefined"
:feedback="errorsObj.sex" class="w-2/4 shrink-0">
<n-select v-model:value="model.sex" placeholder="请选择性别" :options="sexOptions" />
</n-form-item>
<n-form-item label="个人视频" :validation-status="errorsObj.videos ? 'error' : undefined"
:feedback="errorsObj.videos">
<n-upload :default-file-list="videos" v-model:file-list="videos" accept="video/*"
:custom-request="customRequestVideo" @remove="handleRemoveVideo" @preview="handlePreviewVideo"
list-type="image-card">
</n-upload>
</n-form-item>
<n-form-item label="个人相册" :validation-status="errorsObj.photos ? 'error' : undefined"
:feedback="errorsObj.photos">
<n-upload list-type="image-card" :default-file-list="photos" v-model:file-list="photos" accept="image/*"
:custom-request="customRequestPhoto" @remove="handleRemovePhoto">
</n-upload>
</n-form-item>
<div class="btn-con">
<div @click="handleSubmit" class="btn">提交</div>
</div>
</n-form>
</n-config-provider>
</ClientOnly>
<!-- 视频播放 -->
<div v-if="videoVisible" class="video-bg">
<div class="video-body">
<iframe id="iframeContain" name="iframeContain" seamless scrolling="yes" :src="iframeURL" width="800px"
height="460px">
</iframe>
<div class="video-btn" @click="videoVisible = false"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
xx, xx} from "@/utils/api"
import {
NConfigProvider, NSelect, NUpload, NForm, NFormItem } from 'naive-ui'
import type {
UploadInst, UploadFileInfo, UploadCustomRequestOptions } from 'naive-ui'
import {
nextTick } from 'vue'
// 组件的样式重置,可以根据官网右下角的编辑后导出
const themeOverrides = {
common: {
primaryColor: '#000000',
primaryColorHover: "#000000"
},
Form: {
feedbackPadding: "0px 0 0 2px",
feedbackHeightMedium: "14px",
feedbackFontSizeMedium: "12px"
}
}
// 性别选择项
const sexOptions = [
{
label: "男", value: 1 },
{
label: '女', value: 2 },
{
label: '保密', value: 3 }
]
let model = reactive({
sex: null
})
let photos = ref<UploadFileInfo[]>([])
let videos = ref<UploadFileInfo[]>([])
let errorsObj = reactive({
sex: '',
photos: '',
videos: ''
})
const statetoken = useUserToken()
let auth = statetoken.value['token'] ? statetoken.value['token'] : ''
let videosList = ref([]) //主要为了自定义上传后,原本进入页面回显的视频拿不到一些自定义添加的字段,所以用它来另外存一下
let photosList = ref([]) //主要为了自定义上传后,原本进入页面回显的视频拿不到一些自定义添加的字段,所以用它来另外存一下
// 视频播放相关设置
const options = reactive({
width: "800px", //播放器高度
height: "450px", //播放器高度
color: "#409eff", //主题色
title: "", //视频名称
src: "https://cdn.jsdelivr.net/gh/xdlumia/files/video-play/IronMan.mp4", //视频源
poster: '',
muted: false, //静音
webFullScreen: false,
speedRate: ["0.75", "1.0", "1.25", "1.5", "2.0"], //播放倍速
autoPlay: false, //自动播放
loop: false, //循环播放
mirror: false, //镜像画面
ligthOff: false, //关灯模式
volume: 0.3, //默认音量大小
control: true, //是否显示控制
controlBtns: [
"audioTrack",
"quality",
"speedRate",
"volume",
"setting",
"pip",
"pageFullScreen",
"fullScreen",
], //显示所有按钮,
});
const iframeURL = ref('')
const videoVisible = ref(false)
const getFileURL = (file) => {
var url = null;
if (window.URL != undefined) {
// mozilla(firefox)
url = window.URL.createObjectURL(file);
} else if (window.webkitURL != undefined) {
// webkit or chrome
url = window.webkitURL.createObjectURL(file);
}
return url;
}
// 截取视频第一帧
const getVideoBase64 = (url) => {
return new Promise(function (resolve) {
let dataURL = "";
const video = document.createElement("video");
video.setAttribute("crossOrigin", "anonymous"); // 处理跨域
video.setAttribute("src", url);
video.setAttribute("preload", "auto");
video.addEventListener("loadeddata", function () {
const canvas = document.createElement("canvas");
console.log("video.clientWidth", video.videoWidth); // 视频宽
console.log("video.clientHeight", video.videoHeight); // 视频高
const width = video.videoWidth || 400; // canvas的尺寸和图片一样
const height = video.videoHeight || 240; // 设置默认宽高为 400 240
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(video, 0, 0, width, height); // 绘制canvas
dataURL = canvas.toDataURL("image/jpeg"); // 转换为base64
resolve(dataURL);
})
})
}
// base64转图片file
const getFileFromBase64 = (base64URL, filename) => {
var arr = base64URL.split(","),
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {
type: "image/png" });
}
// 上传视频
const customRequestVideo = async ({
file, onFinish, onError }: UploadCustomRequestOptions) => {
console.log('file', file)
const objUrl = getFileURL(file.file as File)
const objBase = await getVideoBase64(objUrl) // 截取视频第一帧
const objCoverFile = await getFileFromBase64(objBase, `${
file.name.split('.')[0]}.jpg`) // 第一帧转file
const videoformData = new FormData()
videoformData.append('file', file.file as File)
videoformData.append('cover', objCoverFile as File) // 后端要我截取封面图传给他,话说在服务端截取不好一些吗,谁叫我人微言轻呢
const {
data: dataV, pending, refresh, error } = await useFetch('http://xxx/upload/video', {
key: uuid(),
method: 'post',
headers: {
Authorization: auth
},
body: videoformData,
})
console.log('接口返回', dataV.value['data'])
// 上传file给后端后,接口返回的视频地址和封面图地址,加到videos的url和thumbnaiUrl上
if (dataV.value['success']) {
onFinish()
errorsObj.videos = ''
videos.value.map(it => {
if (it.id === file.id) {
it.url = dataV.value['data']['file']['path']
it.status = 'finished'
it.thumbnailUrl = dataV.value['data']['cover']['path']
it.type = null // 一定要把type设置为null,不然视频的类型为video/mp4,在页面缩率图thumbnailUrl会不起作用
it['extension'] = dataV.value['data']['file']['extension'] // 提交的时候后端需要的自定义参数
it['size'] = dataV.value['data']['file']['size'] // 提交的时候后端需要的自定义参数
videosList.value.push(it) // 提交的时候后端需要的自定义参数在添加文件之后之前videos里的会没有,所以我要用videosList重新保存一下,并在删除的时候要从videosList里删除
}
})
} else {
onError()
errorsObj.photos = dataV.value['data']['errors']['image'] + ',请删除后重新选择'
}
console.log('videos', videos.value, videosList.value)
}
// 删除视频
const handleRemoveVideo = (data: {
file: UploadFileInfo; fileList: UploadFileInfo[] }) => {
console.log('remove', data.file)
let arr = []
videosList.value.forEach(it => {
if (it.id !== data.file.id) {
arr.push(it)
}
})
videosList.value = arr
console.log('jj', videosList.value)
}
// 预览视频,把视频的url给iframe
const handlePreviewVideo = (file: UploadFileInfo) => {
console.log('预览', file)
options.src = file['url']
options.poster = file['thumbnailUrl']
iframeURL.value = file['url']l
videoVisible.value = true
}
// 上传照片
const customRequestPhoto = async ({
file, onFinish, onError, onProgress }: UploadCustomRequestOptions) => {
console.log('file', file)
const photoformData = new FormData()
photoformData.append('image', file.file as File)
const {
data: dataV, pending, refresh, error } = await useFetch('http://xxx/upload/image', {
key: uuid(),
method: 'post',
headers: {
Authorization: 'auth
},
body: photoformData,
})
console.log('接口返回', dataV.value['data'])
if (dataV.value['success']) {
onFinish() // 上传接口成功调用后记得调用onFinish,不然你上传的时候看看file都打印的啥
errorsObj.photos = ''
photos.value.map(it => {
if (it.id === file.id) {
it.url = dataV.value['data']['file']['path']
it['extension'] = dataV.value['data']['file']['extension']
it['size'] = dataV.value['data']['file']['size']
photosList.value.push(it)
}
})
console.log('photos', photos.value, photosList.value)
} else {
errorsObj.photos = dataV.value['data']['errors']['image'] + ',请删除后重新选择'
onError()
}
}
// 删除照片
const handleRemovePhoto = (data: {
file: UploadFileInfo; fileList: UploadFileInfo[] }) => {
console.log('remove', data.file)
let arr = []
photosList.value.forEach(it => {
if (it.id !== data.file.id) {
arr.push(it)
}
})
photosList.value = arr
}
// 提交
const handleSubmit = async () => {
console.log('提交', videos.value, videosList.value)
console.log('提交', photos.value, photosList.value)
// 因为videos.value和photos.value里一些自己加的字段没有,所以采用videosList.value 和photosList.value数据进行提交
}
// url转file
const getImageFileFromUrl = (url, imageName, type) => {
return new Promise((resolve, reject) => {
let blob = null;
let imgFile = null;
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.setRequestHeader("Accept", type);
// xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
// xhr.setAttribute("crossOrigin", "anonymous"); // 处理跨域
xhr.responseType = "blob";
xhr.onload = () => {
blob = xhr.response;
imgFile = new File([blob], imageName, {
type });
resolve(imgFile);
};
xhr.onerror = e => {
reject(e);
};
xhr.send();
});
}
// 获取已设置
const getSetInfo = async () => {
await nextTick()
getInfo({
key: uuid() }).then(async (res) => {
model.sex= res.sex
res.videos.length && res.videos.forEach(async (it, index) => {
let obj = {
}
obj['id'] = index.toString()
obj['name'] = it.name
obj['status'] = 'finished' // 要给视频设置已完成状态,不然预览不了
obj['url'] = it.path // 设置视频的url,path是后端返回的url字段
obj['thumbnailUrl'] = it.cover // 要给视频设置封面图
v.push(obj)
})
res.photos.length && res.photos.forEach(async (it, index) => {
let ff = await getImageFileFromUrl(it.path, it.name, "image/png") // 一个把url转为file文件的方法
let obj = {
}
obj['id'] = index.toString()
obj['name'] = it.name
obj['status'] = 'finished'
obj['url'] = it.path
obj['file'] = ff //这个file可以不需要,只是调用customRequestPhoto photos.value里一开始回显的文件file会没有
obj['size'] = it.size
obj['extension'] = it.extension // 提交的时候后端需要的一些字段,本身插件没有该字段
obj['thumbnailUrl'] = it.path
p.push(obj)
})
setTimeout(() => {
videos.value = v
photos.value = p
photosList.value = p
videosList.value = v
console.log('set-p', photos.value, photosList.value)
console.log('set', videos.value, videosList.value)
}, 500)
})
}
getSetInfo()
</script>