公共组件
上传文件包含上传、下载、和预览功能
<!-- 上传文件 -->
<template>
<div class="demo-image__preview">
<el-image-viewer
hide-on-click-modal
@close="
() => {
showViewer = false;
}
"
v-if="showViewer"
:url-list="srcList"
/>
</div>
<el-upload
style="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">
<video
ref="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) {
// 实例化axios
this.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,参见loginApi
config.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();
// * 在请求结束后,并关闭请求 loading
tryHideFullScreenLoading();
// * 登陆失效(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-error
if (
response?.config?.url === "/resource/file/uploadFiles" ||
response?.config?.url === "/dems-resource/file/uploadFile"
) {
return { data: response?.data };
}
// 请求超时 && 网络错误单独判断,没有 response
if (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 {
// json
JSON = "application/json;charset=UTF-8",
// text
TEXT = "text/plain;charset=UTF-8",
// form-data 一般配合qs
FORM_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 => ({
// token
token: "",
// 用户信息
userInfo: "",
// webSocket 对象
webSocket: null,
// element组件大小
assemblySize: "default",
// language
language: "",
// isAdmin
isAdmin: false,
// themeConfig
themeConfig: {
// 当前页面是否全屏
maximize: false,
// 布局切换 ==> 纵向:vertical | 经典:classic | 横向:transverse | 分栏:columns
layout: "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: {
// setToken
setToken(token: string) {
this.token = token;
},
// setUserInfo
setUserInfo(userInfo: any) {
this.userInfo = userInfo;
},
// setWebSocket
setWebSocket(webSocket: WebSocket | null) {
this.webSocket = webSocket;
},
// setAssemblySizeSize
setAssemblySizeSize(assemblySize: AssemblySizeType) {
this.assemblySize = assemblySize;
},
// updateLanguage
updateLanguage(language: string) {
this.language = language;
},
// setThemeConfig
setThemeConfig(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;