公共组件
上传文件包含上传、下载、和预览功能
<!-- 上传文件 -->
<template><div class="demo-image__preview"><el-image-viewerhide-on-click-modal@close="() => {showViewer = false;}"v-if="showViewer":url-list="srcList"/></div><el-uploadstyle="width: 100%"action=""v-model:file-list="fileList"class="upload-demo":http-request="uploadFile"multiple:on-preview="handlePreview":on-success="handleSuccess":on-remove="handleRemove":on-error="handleError":before-upload="handleBefore":limit="attr.limit":on-exceed="handleExceed"ref="fileRef":disabled="attr.readonly"><el-button type="primary" v-if="!attr.readonly">上传</el-button></el-upload><el-dialog title="查看视频" v-model="videoShow"><videoref="veo"@click.prevent.once="onPlay"@loadeddata="poster ? () => false : getPoster()":src="src":autoplay="autoplay"controls="true"style="width: 100%; height: 100%"></video></el-dialog>
</template><script setup lang="ts" name="ImportExcel">
import { uploadFile as uploadFun, downloadFileByFileId } from "@/api/modules/upload";
import type { UploadProps, UploadUserFile, UploadRawFile } from "element-plus";
import { ElMessage, genFileId } from "element-plus";
const emit = defineEmits(["update:modelValue"]);
const videoShow = ref(false);
const props = defineProps({modelValue: [Array],attr: {type: Object,required: false,default: () => {}}
});const fileRef = ref<any>(null);
//上传文件
function uploadFile(params: any) {let formData = new FormData();//传值formData.append("file", params.file);uploadFun(formData).then(res => {if (res.code == 200) {params.onSuccess(res.data);} else {params.onError(res.data);}});
}
function handleBefore(row: any) {if (row.size / 1024 / 1024 > 100) {ElMessage.error("文件大小不能超过100M");return false;}return true;
}
const fileList = ref<UploadUserFile[]>([]);
//上传成功之后的操作
const handleSuccess = (res: any, file: UploadUserFile, fileList: UploadUserFile[]) => {console.log(file);file.uid = res.id;let arr = fileList.map(item => {let obj: any = {};obj[props.attr.name] = item.name;obj[props.attr.id] = item.uid;return obj;});console.log(fileList);emit("update:modelValue", arr);
};watch(() => props.modelValue,val => {if (val) {fileList.value = val.map((item: any) => {item.name = item[props.attr.name];item.uid = item[props.attr.id];// item.response = item;return item;});} else {fileList.value = [];return [];}},{ deep: true, immediate: true }
);const veo = ref();const originPlay = ref(true);
const autoplay = ref(false);
const hidden = ref(false); // 是否隐藏播放器中间的播放按钮
const second = ref(0.5);const poster = ref("");
const getPoster = () => {// 在未设置封面时,自动截取视频0.5s对应帧作为视频封面// 由于不少视频第一帧为黑屏,故设置视频开始播放时间为0.5s,即取该时刻帧作为封面图veo.value.currentTime = second.value;// 创建canvas元素const canvas = document.createElement("canvas");const ctx = canvas.getContext("2d");// canvas画图canvas.width = veo.value.videoWidth;canvas.height = veo.value.videoHeight;ctx?.drawImage(veo.value, 0, 0, canvas.width, canvas.height);
};
//是否播放
const onPlay = () => {if (originPlay.value) {veo.value.currentTime = 0;originPlay.value = false;}if (autoplay.value) {veo.value?.pause();} else {hidden.value = true;veo.value?.play();}
};
const isView = (ext: any) => {return ["png", "jpg", "jpeg", "bmp", "gif", "webp", "psd", "svg"].indexOf(ext.toLowerCase()) !== -1;
};
//上传失败
const handleError = (res: any) => {console.log(res);ElMessage.error({ message: res.msg || "上传失败!" });
};
const src = ref("");
const srcList = ref([] as any[]);
const showViewer = ref(false);
//下载文件
const handlePreview: UploadProps["onPreview"] = uploadFile => {let suffix = uploadFile.name.substring(uploadFile.name.lastIndexOf(".") + 1);if (suffix == "mp4") {videoShow.value = true;src.value = "/api/dems-resource/file/loadOnlineVideo?id=" + uploadFile.uid;return;}if (isView(suffix)) {src.value = "/api/dems-resource/file/loadOnlineImage?id=" + uploadFile.uid;srcList.value = [src.value];showViewer.value = true;return;}downloadFileByFileId(uploadFile.name, uploadFile.uid + "");
};
//删除文件
const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {let arr = uploadFiles.map(item => {let obj: any = {};obj[props.attr.name] = item.name;obj[props.attr.id] = item.uid;return obj;});emit("update:modelValue", arr);
};const handleExceed: UploadProps["onExceed"] = files => {if (props.attr.limit == 1) {fileRef.value!.clearFiles();const file = files[0] as UploadRawFile;file.uid = genFileId();fileRef.value!.handleStart(file);fileRef.value!.submit();}
};
</script>
<style lang="scss" scoped>
.upload {width: 100%;
}
:deep(.el-dialog .el-dialog__header) {border-bottom: none !important;
}
</style>
main.ts全局引用
import customUpload from "@/components/Upload/custom-upload.vue";
app.component("CustomUpload", customUpload);
组件页面使用
多文件<custom-upload v-model="formInline.attachmentResultList" :attr="attrResult" />
js
const attrResult = ref({id: "fileUploadId",name: "attachmentName",limit: 999,readonly: true
});
单文件
<custom-upload v-model="faultList" :attr="attr" />const attr = ref({id: "reportFileUploadId",name: "faultReport",limit: 1,readonly: false
});子组件方法
//获取故障文件信息(永远只能上传一个,然后可以替换那一个文件)
const getBreakdownData = () => {if (faultList.value.length > 0) {let file = faultList.value[0];formInline.value.faultReport = file.faultReport;formInline.value.reportFileUploadId = file.reportFileUploadId;}getFaultDurationStr();return formInline.value;
};
defineExpose({getBreakdownData
});
父组件方法
const failureRecordSn = ref("");
save(){// 接收故障记录formInline.value.failureRecord = breakdownRef.value.getBreakdownData();formInline.value.failureRecord!.failureRecordSn = failureRecordSn.value;let failureRecord = { ...formInline.value.failureRecord };// 故障记录为空时删除if (!Object.values(failureRecord).some(i => !!i)) {delete formInline.value.failureRecord;}
}
用到的文件
upload.ts
//文件的接口类型
import { Upload } from "@/api/interface/index";
api封装
import http from "@/api";
//前缀
import { UPLOAD_FILE } from "@/api/config/servicePort";
import { ElMessage } from "element-plus";
//上传文件
export const uploadFile = (params: FormData) => {return http.upload<Upload.ResFileList>(UPLOAD_FILE + `/file/uploadFile`, params, {headers: { "Content-Type": "multipart/form-data" }});
};
// * 用文件名称下载文件
export const downloadFileByFileId = (fileName: string, id: string) => {http.download(UPLOAD_FILE + `/file/downloadFileByFileId`, { fileName, fileId: id }).then(res => {ElMessage.success({ message: "下载成功!" });const blob = new Blob([res]); //处理文档流const link = document.createElement("a");link.download = fileName;link.style.display = "none";link.href = URL.createObjectURL(blob);document.body.appendChild(link);link.click();URL.revokeObjectURL(link.href); // 释放URL 对象document.body.removeChild(link);});
};
定义接口interface
import { Upload } from “@/api/interface/index”;
接口如下
// * 文件上传模块
export namespace Upload {export interface ResFileUrl {fileUrl: string;}export interface ResFileList {id: string;fileName: string;fileUrl: string;uploadTime: string;operator: string;fileType: string;fileSize: number;fileOldname: string;}
}
http在api/index.ts文件里面
包含拦截器,响应器和请求方式的封装
import http from “@/api”;
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { showFullScreenLoading, tryHideFullScreenLoading } from "@/config/serviceLoading";
import { ResultData, GatewayResultData } from "@/api/interface";
import { ResultEnum } from "@/enums/httpEnum";
import { checkStatus } from "./helper/checkStatus";
import { ElMessage, ElMessageBox } from "element-plus";
import { GlobalStore } from "@/stores";
import { CallCenterStore } from "@/stores/modules/ccs";//这个可以不要(其他功能的)
import { LOGIN_URL } from "@/config/config";//主题颜色(// * 登录页地址(默认)
export const LOGIN_URL: string = "/login";)
import { encodeHtml, decodeHtml } from "@/utils/htmlUtil";//富文本
import router from "@/routers";
import qs from "qs";
const config = {// 默认地址请求地址,可在 .env.*** 文件中修改baseURL: import.meta.env.VITE_API_URL as string,// 设置超时时间(10s)timeout: ResultEnum.TIMEOUT as number,// 跨域时候允许携带凭证withCredentials: true
};class RequestHttp {service: AxiosInstance;public constructor(config: AxiosRequestConfig) {// 实例化axiosthis.service = axios.create(config);/*** @description 请求拦截器* 客户端发送请求 -> [请求拦截器] -> 服务器* token校验(JWT) : 接受服务器返回的token,存储到vuex/pinia/本地储存当中*/this.service.interceptors.request.use((config: InternalAxiosRequestConfig) => {encodeHtml(config);const globalStore = GlobalStore();// * 如果当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading,参见loginApiconfig.headers!.noLoading || showFullScreenLoading();const token = globalStore.token;if (config.headers && typeof config.headers?.set === "function") config.headers.set("Authorization", token);return config;},(error: AxiosError) => {return Promise.reject(error);});/*** @description 响应* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息*/this.service.interceptors.response.use((response: AxiosResponse) => {const { data } = response;const globalStore = GlobalStore();const callCenterStore = CallCenterStore();// * 在请求结束后,并关闭请求 loadingtryHideFullScreenLoading();// * 登陆失效(code == 401)if (data.code == ResultEnum.OVERDUE) {if (document.getElementsByClassName("el-message").length == 0) {ElMessage.error(data.msg);//错误提示}globalStore.setToken("");globalStore?.webSocket?.close();globalStore.setWebSocket(null);callCenterStore.clearAllData();router.replace(LOGIN_URL);return Promise.reject(data);}// * 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错)if (data.code && data.code !== ResultEnum.SUCCESS) {if (data.code > 200 && data.code < 300) {ElMessage.warning(data.msg);} else {if (data.code !== 502) {ElMessage.error(data.msg);}}return Promise.reject(data);}decodeHtml(data, response.config.url);// * 成功请求(在页面上除非特殊情况,否则不用在页面处理失败逻辑)return data;},async (error: AxiosError) => {tryHideFullScreenLoading();const { response } = error;const { data, code } = (response?.data as any) || {};if (code == 400) {let arr = [];for (let v in data) {arr.push(data[v]);}ElMessageBox.alert(arr.join("</br>"), "错误", {confirmButtonText: "确认",dangerouslyUseHTMLString: true});return Promise.reject();}// 由于后端的微服务网关报错时返回的是特殊响应数据,此处对登录失败的情况做特殊处理if (response?.config?.url === "auth/token/login") {return { data: response.data };}tryHideFullScreenLoading();// 上传失败如果不return 不进el-upload的on-errorif (response?.config?.url === "/resource/file/uploadFiles" ||response?.config?.url === "/dems-resource/file/uploadFile") {return { data: response?.data };}// 请求超时 && 网络错误单独判断,没有 responseif (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");// 根据响应的错误状态码,做不同的处理if (response && response.data) {let data = <any>response.data;let obj = data.body ?? data;if ("msg" in obj) {ElMessage.error(obj.msg);} else {checkStatus(response.status);}} else if (response) {checkStatus(response.status);}// * 登陆失效(code == 401)if (response?.status == ResultEnum.OVERDUE) {if (document.getElementsByClassName("el-message").length == 0) {ElMessage.error(data.msg);//错误提示}const globalStore = GlobalStore();globalStore.setToken("");globalStore?.webSocket?.close();globalStore.setWebSocket(null);const callCenterStore = CallCenterStore();callCenterStore.clearAllData();router.replace(LOGIN_URL);}// 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面if (!window.navigator.onLine) router.replace("/500");return Promise.reject(error);});}// * 常用请求方法封装get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {return this.service.get(url, { params, ..._object });}post<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {return this.service.post(url, qs.stringify(params), _object);}postJson<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {return this.service.post(url, params, _object);}put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {return this.service.put(url, params, _object);}delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {return this.service.delete(url, { params, ..._object });}download(url: string, params?: object, _object = {}): Promise<BlobPart> {return this.service.get(url, { params, ..._object, responseType: "blob", timeout: 0 });}getRequest<T>(url: string, params?: object, _object = {}): Promise<GatewayResultData<T>> {return this.service.get(url, { params, ..._object });}upload<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {return this.service.post(url, params, { ..._object, timeout: 0 });}
}export default new RequestHttp(config);
import { showFullScreenLoading, tryHideFullScreenLoading } from “@/config/serviceLoading”;
import { ElLoading } from "element-plus";/* 全局请求 loading(服务方式调用) */
let loadingInstance: ReturnType<typeof ElLoading.service>;/*** @description 开启 Loading* */
const startLoading = () => {loadingInstance = ElLoading.service({fullscreen: true,lock: true,text: "Loading",background: "rgba(0, 0, 0, 0.7)"});
};/*** @description 结束 Loading* */
const endLoading = () => {loadingInstance.close();
};/*** @description 显示全屏加载* */
let needLoadingRequestCount = 0;
export const showFullScreenLoading = () => {if (needLoadingRequestCount === 0) {startLoading();}needLoadingRequestCount++;
};/*** @description 隐藏全屏加载* */
export const tryHideFullScreenLoading = () => {if (needLoadingRequestCount <= 0) return;needLoadingRequestCount--;if (needLoadingRequestCount === 0) {endLoading();}
};
interface接口
import { ResultData, GatewayResultData } from “@/api/interface”;
// * 请求响应参数(不包含data)
export interface Result {code: number;msg: string;
}/*** 请求响应参数(包含data)*/
export interface ResultData<T = any> extends Result {data: T;
}/*** 请求响应参数(网关的特殊响应)*/
export interface GatewayResultData<T = any> extends Result {body: T;
}/*** 后端返回的分页响应参数*/
export interface ResPage<T> {// 查询数据列表records: T[];// 当前页current: number;// 每页显示条数size: number;// 总数total: number;
}/*** 分页请求参数*/
export interface ReqPage {// 当前页pageNum: number;//每页记录数pageSize: number;// 排序字段orderByColumn?: string;// 排序的方向isAsc?: "asc" | "desc";
}// * 文件上传模块
export namespace Upload {export interface ResFileUrl {fileUrl: string;}export interface ResFileList {id: string;fileName: string;fileUrl: string;uploadTime: string;operator: string;fileType: string;fileSize: number;fileOldname: string;}
}
枚举配置
import { ResultEnum } from “@/enums/httpEnum”;
// * 请求枚举配置
/*** @description:请求配置*/
export enum ResultEnum {SUCCESS = 200,ERROR = 500,OVERDUE = 401,TIMEOUT = 10000,TYPE = "success"
}/*** @description:请求方法*/
export enum RequestEnum {GET = "GET",POST = "POST",PATCH = "PATCH",PUT = "PUT",DELETE = "DELETE"
}/*** @description:常用的contentTyp类型*/
export enum ContentTypeEnum {// jsonJSON = "application/json;charset=UTF-8",// textTEXT = "text/plain;charset=UTF-8",// form-data 一般配合qsFORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",// form-data 上传FORM_DATA = "multipart/form-data;charset=UTF-8"
}
状态检查
import { checkStatus } from “./helper/checkStatus”;
import { ElMessage } from "element-plus";/*** @description: 校验网络请求状态码* @param {Number} status* @return void*/
export const checkStatus = (status: number): void => {switch (status) {case 400:ElMessage.error("请求失败!请您稍后重试");break;case 401:ElMessage.error("登录失效!请您重新登录");break;case 403:ElMessage.error("当前账号无权限访问!");break;case 404:ElMessage.error("你所访问的资源不存在!");break;case 405:ElMessage.error("请求方式错误!请您稍后重试");break;case 408:ElMessage.error("请求超时!请您稍后重试");break;case 500:ElMessage.error("服务异常!");break;case 502:ElMessage.error("网关错误!");break;case 503:ElMessage.error("服务不可用!");break;case 504:ElMessage.error("网关超时!");break;default:ElMessage.error("请求失败!");}
};
import { GlobalStore } from “@/stores”;
import { defineStore, createPinia } from "pinia";
import { GlobalState, ThemeConfigProps, AssemblySizeType } from "./interface";
import { DEFAULT_PRIMARY } from "@/config/config";
import piniaPersistConfig from "@/config/piniaPersist";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";// defineStore 调用后返回一个函数,调用该函数获得 Store 实体
export const GlobalStore = defineStore({// id: 必须的,在所有 Store 中唯一id: "GlobalState",// state: 返回对象的函数state: (): GlobalState => ({// tokentoken: "",// 用户信息userInfo: "",// webSocket 对象webSocket: null,// element组件大小assemblySize: "default",// languagelanguage: "",// isAdminisAdmin: false,// themeConfigthemeConfig: {// 当前页面是否全屏maximize: false,// 布局切换 ==> 纵向:vertical | 经典:classic | 横向:transverse | 分栏:columnslayout: "vertical",// 默认 primary 主题颜色primary: DEFAULT_PRIMARY,// 深色模式isDark: false,// 灰色模式isGrey: false,// 色弱模式isWeak: false,// 折叠菜单isCollapse: false,// 面包屑导航breadcrumb: true,// 面包屑导航图标breadcrumbIcon: true,// 标签页tabs: true,// 标签页图标tabsIcon: true,// 页脚footer: true}}),getters: {},actions: {// setTokensetToken(token: string) {this.token = token;},// setUserInfosetUserInfo(userInfo: any) {this.userInfo = userInfo;},// setWebSocketsetWebSocket(webSocket: WebSocket | null) {this.webSocket = webSocket;},// setAssemblySizeSizesetAssemblySizeSize(assemblySize: AssemblySizeType) {this.assemblySize = assemblySize;},// updateLanguageupdateLanguage(language: string) {this.language = language;},// setThemeConfigsetThemeConfig(themeConfig: ThemeConfigProps) {this.themeConfig = themeConfig;},setIsAdmin(isAdmin: boolean) {this.isAdmin = isAdmin;}},persist: piniaPersistConfig("GlobalState")
});// piniaPersist(持久化)
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);export default pinia;
import { GlobalState, ThemeConfigProps, AssemblySizeType } from “./interface”;
/* GlobalState */
export interface GlobalState {token: string;userInfo: any;webSocket: WebSocket | null;assemblySize: AssemblySizeType;language: string;themeConfig: ThemeConfigProps;isAdmin: boolean;
}/* themeConfigProp */
export interface ThemeConfigProps {maximize: boolean;layout: LayoutType;primary: string;isDark: boolean;isGrey: boolean;isCollapse: boolean;isWeak: boolean;breadcrumb: boolean;breadcrumbIcon: boolean;tabs: boolean;tabsIcon: boolean;footer: boolean;
}export type AssemblySizeType = "default" | "small" | "large";export type LayoutType = "vertical" | "classic" | "transverse" | "columns";/* tabsMenuProps */
export interface TabsMenuProps {icon: string;title: string;path: string;name: string;close: boolean;
}/* TabsState */
export interface TabsState {tabsMenuList: TabsMenuProps[];
}/* AuthState */
export interface AuthState {routeName: string;authButtonList: {[key: string]: string[];};authMenuList: Menu.MenuOptions[];
}/* keepAliveState */
export interface keepAliveState {keepAliveName: string[];
}
import { DEFAULT_PRIMARY } from “@/config/config”;
// ? 全局不动配置项 只做导出不做修改// * 首页地址(默认)
export const HOME_URL: string = "/home";// * 登录页地址(默认)
export const LOGIN_URL: string = "/login";// * 默认主题颜色
export const DEFAULT_PRIMARY: string = "#009688";// * 路由白名单地址(必须是本地存在的路由 staticRouter.ts)
export const ROUTER_WHITE_LIST: string[] = ["/500"];// * 高德地图 key
export const AMAP_MAP_KEY: string = "";// * 百度地图 key
export const BAIDU_MAP_KEY: string = "";
import piniaPersistConfig from “@/config/piniaPersist”;
import { PersistedStateOptions } from "pinia-plugin-persistedstate";
/*** @description pinia持久化参数配置* @param {String} key 存储到持久化的 name* @param {Array} paths 需要持久化的 state name* @return persist* */
const piniaPersistConfig = (key: string, paths?: string[]) => {const persist: PersistedStateOptions = {key,// storage: localStorage,storage: sessionStorage,paths};return persist;
};export default piniaPersistConfig;