export enum FileType {
XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
XLS = 'application/vnd.ms-excel',
ZIP = 'application/x-zip-compressed',
PDF = 'application/pdf',
PNG = 'image/png',
JPEG = 'image/jpeg',
DOC = 'application/msword',
DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
export const mimeMap = {
xlsx: FileType.XLSX,
xls: FileType.XLS,
zip: FileType.ZIP,
};
/**
* 解析blob响应内容并下载
* @param {*} res blob响应内容
* @param {String} mimeType MIME类型
*/
export function resolveBlob(res: any, mimeType: string) {
const aLink = document.createElement('a');
const blob = new Blob([res.data], { type: mimeType });
// //从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名;
const patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*');
const contentDisposition = decodeURI(res.headers['content-disposition']);
const result = patt.exec(contentDisposition);
let fileName = result ? result[1] : '';
fileName = fileName.replace(/"/g, '');
aLink.style.display = 'none';
aLink.href = URL.createObjectURL(blob);
aLink.setAttribute('download', decodeURI(fileName)); // 设置下载文件名称
document.body.appendChild(aLink);
aLink.click();
URL.revokeObjectURL(aLink.href); //清除引用
document.body.removeChild(aLink);
}
ElMessageBox
import { ElMessageBox } from 'element-plus';
import { App } from 'vue';
/**
* @description 全局的弹窗对话函数,element messagebox二次封装
* @export
* @param {string} message
* @param {boolean} [html=false]
* @param {string} [title='提示']
* @return {*}
*/
export function messageBox(message: string, html = false, title = '提示') {
return new Promise(resovle => {
ElMessageBox.confirm(message, title, {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: html
})
.then(() => {
resovle(0);
})
.catch(() => {});
});
}
/**
* @description 弹窗确认框
* @export
* @param {string} message
* @param {boolean} [html=false]
* @param {string} [title='请确认']
* @return {*}
*/
export function messagePrompt(message: string, html = false, title = '请确认') {
return new Promise(resovle => {
ElMessageBox.prompt(message, title, {
confirmButtonText: '确认',
cancelButtonText: '取消',
dangerouslyUseHTMLString: html
})
.then(({ value }) => {
resovle(value);
})
.catch(() => {});
});
}
export default function (app: App) {
/**
* @description 弹窗对话
*/
app.config.globalProperties.messageBox = messageBox;
app.config.globalProperties.messagePrompt = messagePrompt;
}
/**
* @description 格式化时间戳
* @param {number} time
* @param {string} type yyyy-MM-dd/yyyy-MM-dd hh:mm:ss/hh:mm:ss
*/
export function dateFormat(time: number, type: string) {
if (!time) {
return;
}
let formatTime;
let date;
if (time === 0) {
date = new Date();
} else {
date = new Date(time);
}
const Year =
date.getFullYear() < 10 ? '0' + date.getFullYear() : date.getFullYear();
const month =
date.getMonth() + 1 < 10
? '0' + (date.getMonth() + 1)
: date.getMonth() + 1;
const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
const Hour = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
const Minute =
date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
const Second =
date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
if (type === 'yyyy-MM-dd') {
formatTime = Year + '-' + month + '-' + day;
return formatTime;
} else if (type === 'yyyy-MM-dd hh:mm:ss') {
formatTime =
Year + '-' + month + '-' + day + ' ' + Hour + ':' + Minute + ':' + Second;
return formatTime;
} else if (type === 'hh:mm:ss') {
formatTime = Hour + ':' + Minute + ':' + Second;
return formatTime;
} else {
return 'error type!';
}
}
<template>
<el-dialog
v-model="state.visible"
:title="props.title"
center
:close-on-click-modal="false"
@close="close"
width="700"
>
<el-form
:model="state.form"
label-width="100px"
:rules="state.rules"
ref="formRef"
>
<el-form-item label="文件" prop="file">
<el-upload
ref="fileRef"
class="upload-demo"
:auto-upload="false"
drag
:data="props.data"
:limit="props.limit"
:action="url"
:on-change="changeFile"
:on-exceed="handleExceed"
:on-success="onSuccess"
:on-error="onError"
:on-remove="onRemove"
:headers="uploadHeader()"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
>
<el-icon class="el-icon--upload"><upload-filled />el-icon>
<div class="el-upload__text">
拖拽文件到这里 或者<em>点击上传em>
div>
<template #tip>
<div class="el-upload__tip">请上传Excel文件,大小不超过2Mdiv>
template>
el-upload>
el-form-item>
el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submit(formRef)"> 确认 el-button>
<el-button @click="cancel">取消el-button>
span>
template>
el-dialog>
template>
<script lang="ts">
export default { name: 'ImportDialog' };
script>
<script lang="ts" setup>
import {
ElMessage,
UploadFile,
UploadFiles,
UploadInstance,
UploadProps,
UploadRawFile,
genFileId,
FormInstance
} from 'element-plus';
import { reactive, ref } from 'vue';
import { useUpload } from '@/hooks/useUpload';
const props = defineProps({
title: {
type: String,
default: '文件上传'
},
limit: {
type: Number,
default: 1
},
url: {
type: String,
required: true
},
data: {
type: Object,
default: () => {}
}
});
const emit = defineEmits(['success']);
const state = reactive({
visible: false,
form: {
file: ''
},
rules: {
file: [{ required: true, message: '请选择上传文件!', trigger: 'change' }]
}
});
const fileRef = ref<UploadInstance>();
const formRef = ref<FormInstance>();
const { uploadHeader, validateFile } = useUpload();
/**
* @description 文件上传
*/
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
fileRef.value?.submit();
}
});
};
/**
* @description 取消文件上传
*/
const cancel = () => {
close();
};
/**
* @description 关闭
*/
const close = () => {
clearFile();
state.visible = false;
};
/**
* @description 文件上传之前验证
*/
const changeFile: UploadProps['onChange'] = (
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => {
const type = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
validateFile(
uploadFile,
type,
2,
() => {
state.form.file = uploadFile.name;
formRef.value?.validateField('file');
},
() => {
clearFile();
}
);
};
/**
* @description 覆盖前一个文件
*/
const handleExceed: UploadProps['onExceed'] = files => {
fileRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
fileRef.value!.handleStart(file);
};
/**
* @description 清空数据
*/
const clearFile = () => {
fileRef.value?.clearFiles();
state.form.file = '';
formRef.value?.resetFields();
};
/**
* @description 上传成功
*/
const onSuccess: UploadProps['onSuccess'] = (response: any) => {
if (response.code === 500 || response.code === 2) {
ElMessage.error(response.msg || '上传失败!');
clearFile();
} else {
emit('success');
ElMessage.success('上传成功!');
close();
}
};
/**
* @description 上传失败
*/
const onError: UploadProps['onError'] = () => {
ElMessage.error('上传失败!');
clearFile();
};
/**
* @description 文件删除
*/
const onRemove: UploadProps['onRemove'] = () => {
state.form.file = '';
};
const open = () => {
state.visible = true;
};
defineExpose({
open
});
script>
<style lang="scss" scoped>
.upload-demo {
width: 90%;
}
style>
<template>
<el-upload
ref="fileRef"
style="width: 100%"
:auto-upload="true"
:data="props.data"
:limit="props.limit"
:action="url"
:on-exceed="handleExceed"
:on-success="onSuccess"
:on-error="onError"
:on-remove="onRemove"
:before-upload="beforeAvatarUpload"
:headers="uploadHeader()"
:accept="accept"
:file-list="props.fileList"
>
<slot><el-button type="primary">上传附件el-button>slot>
<template #tip>
<div class="el-upload__tip">{{ tips }}div>
template>
el-upload>
template>
<script lang="ts">
export default { name: 'UploadButton' };
script>
<script lang="ts" setup>
import {
ElMessage,
UploadInstance,
UploadProps,
UploadRawFile,
UploadUserFile,
genFileId
} from 'element-plus';
import { reactive, ref } from 'vue';
import { useUpload } from '@/hooks/useUpload';
import { baseSettingUrl, baseUrl } from '@/constant';
import { FileType } from '@/types/common';
const props = defineProps({
title: {
type: String,
default: '文件上传'
},
limit: {
type: Number,
default: 1
},
url: {
type: String,
default: `${baseUrl}${baseSettingUrl}/file/upload`
},
data: {
type: Object,
default: () => {}
},
accept: {
type: String,
default: () => {
return `${FileType.XLS},${FileType.XLSX},${FileType.ZIP}`;
}
},
tips: {
type: String,
default: '文件大小不超过2M'
},
fileList: {
type: Array<UploadUserFile>,
default: () => [] as UploadUserFile[]
}
});
const emits = defineEmits(['success', 'update:file-list']);
const state = reactive({
file: ''
});
const fileRef = ref<UploadInstance>();
const { uploadHeader } = useUpload();
/**
* @description 文件上传之前验证
*/
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
if (
!(
rawFile.type === FileType.XLS ||
rawFile.type === FileType.XLSX ||
rawFile.type === FileType.ZIP
)
) {
ElMessage.warning('请上传EXCLE表格或者ZIP压缩包!');
return false;
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.warning('文件大小不超过2M!');
return false;
}
return true;
};
/**
* @description 覆盖前一个文件
*/
const handleExceed: UploadProps['onExceed'] = files => {
fileRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
fileRef.value!.handleStart(file);
fileRef.value?.submit();
};
/**
* @description 清空数据
*/
const clearFile = () => {
fileRef.value?.clearFiles();
state.file = '';
};
/**
* @description 上传成功
*/
const onSuccess: UploadProps['onSuccess'] = (response: any) => {
if (response.code === 500) {
ElMessage.error('上传失败!');
clearFile();
} else {
emits('success', response);
}
};
/**
* @description 上传失败
*/
const onError: UploadProps['onError'] = () => {
ElMessage.error('上传失败!');
clearFile();
};
/**
* @description 文件删除
*/
const onRemove: UploadProps['onRemove'] = () => {
state.file = '';
};
defineExpose({
clearFile
});
script>
<style lang="scss" scoped>style>
仅做展示
<template>
<el-table
:data="props.data"
border
:max-height="props.maxHeight"
:row-key="rowKey"
:highlight-current-row="true"
ref="tableRef"
@row-click="rowClick"
:span-method="props.spanMethod"
>
<slot name="index">
<el-table-column
type="index"
label="序号"
width="60"
align="center"
>el-table-column>
slot>
<el-table-column
v-for="col in props.column"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:show-overflow-tooltip="col.showOverflowTooltip === false ? false : true"
align="center"
:width="col.width ? col.width : 'auto'"
>
<template v-if="col.children">
<el-table-column
v-for="child in col.children"
:key="child.prop"
:prop="child.prop"
:label="child.label"
:show-overflow-tooltip="true"
align="center"
:width="child.width ? child.width : 'auto'"
>
<template #default="{ row }" v-if="col.template === 'children'">
<span
v-if="
child.prop === 'exceedEstimate' || child.prop === 'exceedRange'
"
:class="[getDangerClass(row[col.prop][child.prop])]"
>{{ row[col.prop][child.prop] }}span
>
<span v-else>{{ row[col.prop][child.prop] }}span>
template>
<template #default="{ row }" v-else>
<span
v-if="
child.prop === 'exceedEstimate' || child.prop === 'exceedRange'
"
:class="[getDangerClass(row[child.prop])]"
>{{ row[child.prop] }}span
>
<span v-else>{{ row[child.prop] }}span>
template>
el-table-column>
template>
<template v-if="col.template === 'date'" #default="{ row }">{{
dateFormat(row[col.prop], 'yyyy-MM-dd')
}}template>
<template v-else-if="col.template === 'trueOrFalse'" #default="{ row }">
<el-tag :type="row[col.prop] === 0 ? 'success' : 'info'" size="small">
{{ row[col.prop] === 0 ? '是' : '否' }}
el-tag>
template>
<template v-else-if="col.template === 'status'" #default="{ row }">
<slot name="status" :row="row">slot>
template>
<template
v-else-if="col.template === 'left' || col.template === 'right'"
#default="{ row }"
>
<div :style="{ textAlign: col.template }" class="text-overflow">
{{ row[col.prop] }}
div>
template>
<template v-else-if="col.template === 'download'" #default="{ row }">
<DownloadButton
v-if="row[col.prop]"
:id="row[getDownloadId(col.prop)]"
:mime="getFileType(row[col.prop])"
>{{ row[col.prop] }}DownloadButton
>
template>
<template v-else-if="col.prop === 'evalType'" #default="{ row }">{{
row.evalTyp === 1 ? '油藏' : '气藏'
}}template>
<template v-else-if="col.prop === 'afterYield'" #default="{ row }">{{
row.afterYield ? row.afterYield + '%' : ''
}}template>
el-table-column>
el-table>
template>
<script lang="ts">
export default { name: 'SimpleTable' };
script>
<script lang="ts" setup>
import { FileType, TableColumn } from '@/types/common';
import { dateFormat } from '@/utils/filter';
import { useTableClick } from '@/utils/tools';
import { TableColumnCtx, TableInstance } from 'element-plus';
import { computed, ref } from 'vue';
import DownloadButton from '@/components/Upload/DownloadButton/index.vue';
const tableRef = ref<TableInstance>();
const emit = defineEmits(['rowClick']);
const props = defineProps<{
data: object[];
column: TableColumn[];
rowKey?: string;
maxHeight: number | string;
spanMethod?: (data: {
row: any;
rowIndex: number;
column: TableColumnCtx<any>;
columnIndex: number;
}) =>
| number[]
| {
rowspan: number;
colspan: number;
}
| undefined;
}>();
const rowKey = computed(() => {
return props.rowKey ? props.rowKey : 'id';
});
const { tableRowData, tableRowClick, clearTableClick } = useTableClick(
rowKey.value
);
const rowClick = (row: any) => {
tableRowClick(row, tableRef.value);
emit('rowClick', tableRowData.value);
};
const getDangerClass = (value: string | number) => {
return Number(value) > 0 ? 'danger' : '';
};
type type = keyof typeof FileType;
/**
* @description 获取文件后缀
*/
const getFileType = (name: string) => {
if (name) {
const typeArr = name.split('.');
const realType = typeArr[typeArr.length - 1].toUpperCase() as type;
return FileType[realType];
}
return;
};
/**
* @description 获取文件Id
*/
const getDownloadId = (key: string) => {
if (key) {
return key.replace(/(Url)/, 'Id');
}
return;
};
defineExpose({
clearTableClick: () => clearTableClick(tableRef.value)
});
script>
<style lang="scss" scoped>
.danger {
color: var(--el-color-danger);
font-weight: bold;
}
style>
可修改表格值
<template>
<el-table
:data="props.data"
border
:max-height="props.maxHeight"
:highlight-current-row="true"
ref="tableRef"
:row-key="rowKey"
@row-click="rowClick"
:span-method="props.spanMethod"
>
<slot name="index"> slot>
<el-table-column
v-for="col in props.column"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:show-overflow-tooltip="false"
:align="col.align ? col.align : 'center'"
:width="col.width ? col.width : 'auto'"
>
<template #default="{ row }">
<div v-show="props.isEdit">
<div v-if="row.isEditor === 1 && col.canEdit" @click.stop>
<el-input-number
v-model="row[col.prop]"
:controls="false"
:min="0"
:max="9999999999"
v-if="col.isNumber"
@blur="changeValue(row)"
>el-input-number>
<el-input
v-model="row[col.prop]"
v-else
maxlength="10"
@blur="changeValue(row)"
>el-input>
div>
<div v-else-if="row.isEditor === 2 && col.canEditOther" @click.stop>
<el-input-number
v-model="row[col.prop]"
:controls="false"
:min="0"
:max="9999999999"
v-if="col.isNumber"
@blur="changeValue(row)"
>el-input-number>
<el-input
v-model="row[col.prop]"
v-else
maxlength="10"
@blur="changeValue(row)"
>el-input>
div>
<div v-else>
<div v-if="col.template === 'left' || col.template === 'right'">
<div :style="{ textAlign: col.template }" class="text-overflow">
{{ row[col.prop] }}
div>
div>
<div v-else>{{ row[col.prop] }}div>
div>
div>
<div v-show="!props.isEdit">
<div v-if="col.template === 'left' || col.template === 'right'">
<div :style="{ textAlign: col.template }" class="text-overflow">
{{ row[col.prop] }}
div>
div>
<div v-else>{{ row[col.prop] }}div>
div>
template>
el-table-column>
el-table>
template>
<script lang="ts">
export default { name: 'SimpleTable' };
script>
<script lang="ts" setup>
import { TableColumn } from '@/types/common';
import { useTableClick } from '@/utils/tools';
import { TableColumnCtx, TableInstance } from 'element-plus';
import { computed, ref } from 'vue';
const props = defineProps<{
data: object[];
column: TableColumn[];
maxHeight: number | string;
isEdit: boolean;
rowKey?: string;
spanMethod?: (data: {
row: any;
rowIndex: number;
column: TableColumnCtx<any>;
columnIndex: number;
}) =>
| number[]
| {
rowspan: number;
colspan: number;
}
| undefined;
}>();
const tableRef = ref<TableInstance>();
const emits = defineEmits(['change', 'rowClick']);
const changeValue = (row: any) => {
emits('change', row);
};
const rowKey = computed(() => {
return props.rowKey ? props.rowKey : 'id';
});
const { tableRowData, tableRowClick, clearTableClick } = useTableClick(
rowKey.value
);
const rowClick = (row: any) => {
tableRowClick(row, tableRef.value);
emits('rowClick', tableRowData.value);
};
defineExpose({
clearTableClick: () => clearTableClick(tableRef.value)
});
script>
<style lang="scss" scoped>style>
可通过简单的column配置进行展示
/**
* @description 表格
* @export
* @interface TableColumn
*/
export interface TableColumn {
prop: string;
label: string;
template?: string;
align?: string;
width?: string;
isNumber?: boolean;
canEdit?: boolean;
canEditOther?: boolean;
children?: TableColumn[];
showOverflowTooltip?: boolean;
}
export const preDrillingEngineeringColumn: TableColumn[] = [
{
prop: 'year',
label: '年度',
width: '70'
},
{
prop: 'projectName',
label: '项目名称',
template: 'left'
},
{
prop: 'startDate',
label: '开工日期',
template: 'date',
width: '100'
},
{
prop: 'amount',
label: '金额',
canEdit: true,
isNumber: true
},
{
prop: 'costFileUrl',
label: '造价文件',
template: 'download',
width: '120'
},
{
prop: 'landFee',
label: '土地费(万元)',
template: 'children',
children: [
{
prop: 'landFeeEstimate',
label: '概算',
width: '80'
},
{
prop: 'landFeeSettlement',
label: '结算',
width: '80'
},
{
prop: 'landFeeDetermine',
label: '决算',
width: '80'
}
]
},
]
手动触发
/F11
/ESC
冲突的问题全屏方法
import { ref } from 'vue';
const useFullScreen = () => {
const isFullscreen = ref(false);
const enterFullscreen = (element: HTMLElement) => {
if (element.requestFullscreen) {
element
.requestFullscreen()
.then(() => {
isFullscreen.value = true;
})
.catch(() => {
// iframe下全屏报错 新开窗口
window.open(
'http://xxx',
'_blank'
);
});
}
// else if (element.mozRequestFullScreen) { /* Firefox */
// element.mozRequestFullScreen();
// } else if (element.webkitRequestFullscreen) { /* Chrome, Safari & Opera */
// element.webkitRequestFullscreen();
// } else if (element.msRequestFullscreen) { /* IE/Edge */
// element.msRequestFullscreen();
// }
};
const exitFullscreen = () => {
if (document.exitFullscreen) {
document
.exitFullscreen()
.then(() => {
isFullscreen.value = false;
})
.catch(() => {
// iframe下全屏报错 新开窗口
window.open(
'http://xxx',
'_blank'
);
});
}
// else if (document.mozCancelFullScreen) { /* Firefox */
// document.mozCancelFullScreen();
// } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
// document.webkitExitFullscreen();
// } else if (document.msExitFullscreen) { /* IE/Edge */
// document.msExitFullscreen();
// }
};
return {
isFullscreen,
enterFullscreen,
exitFullscreen
};
};
export default useFullScreen;
监听全屏和键盘事件,注意F11进入全屏后,再次F11无法触发keydown
,需要在fullscreenchange
中处理
const fullscreenHandler = () => {
if (document.fullscreenElement) {
// 浏览器已进入全屏模式
nextTick(() => {
state.width = window.screen.width;
state.height = window.screen.height;
// 全屏动画结束后设置宽高比例
requestAnimationFrame(setStyle);
});
} else {
// 浏览器已退出全屏模式
nextTick(() => {
state.width = window.innerWidth;
state.height = window.innerHeight;
// 全屏动画结束后设置宽高比例
requestAnimationFrame(setStyle);
isFullscreen.value = false; //这里设置是因为全屏之后按F11无法触发keydown,也就无法赋值
});
}
};
const keydownHandler = (event: any) => {
if (event.keyCode === 122) {
// F11 key
event.preventDefault();
enterFullscreen(document.getElementById('fullscreen')!);
}
if (event.keyCode === 27) {
// ESC key
event.preventDefault();
exitFullscreen();
}
};
onMounted(() => {
document.addEventListener('fullscreenchange', fullscreenHandler);
document.addEventListener('keydown', keydownHandler);
});
onDeactivated(() => {
document.removeEventListener('fullscreenchange', fullscreenHandler);
document.removeEventListener('keydown', keydownHandler);
});