@truenewx/tnxvue3
Version:
互联网技术解决方案:Vue3扩展支持
576 lines (528 loc) • 21.5 kB
text/typescript
/**
* 基于ElementPlus的扩展支持
*/
import * as Vue from 'vue';
import ElementPlus, {
ElLoading,
ElMessage,
ElMessageBox,
ElMessageBoxOptions,
MessageParams,
LoadingOptions,
LoadingInstance,
} from 'element-plus';
import ElementPlus_zh_CN from 'element-plus/es/locale/lang/zh-cn';
import TnxVue, {DialogOptions, DrawerOptions, util} from '../tnxvue.ts';
import Validator from './tnxel-validator.ts';
import CaptchaVerify from './aj-captcha/Verify.vue';
import Avatar from './avatar/Avatar.vue';
import Alert from './alert/Alert.vue';
import Button from './button/Button.vue';
import CheckIcon from './check-icon/CheckIcon.vue';
import CloseErrorButton from './close-error-button/CloseErrorButton.vue';
import Curd from './curd/Curd.vue';
import DatePicker from './date-picker/DatePicker.vue';
import DateRange from './date-range/DateRange.vue';
import DateTimePicker from './datetime-picker/DateTimePicker.vue';
import DetailForm from './detail-form/DetailForm.vue';
import Dialog from './dialog/Dialog.vue';
import Drawer from './drawer/Drawer.vue';
import DropdownItem from './dropdown-item/DropdownItem.vue';
import EditTable from './edit-table/EditTable.vue';
import EnumSelect from './enum-select/EnumSelect.vue';
import EnumView from './enum-view/EnumView.vue';
import FetchCascader from './fetch-cascader/FetchCascader.vue';
import FetchSelect from './fetch-select/FetchSelect.vue';
import FetchTags from './fetch-tags/FetchTags.vue';
import FssUpload from './fss-upload/FssUpload.vue';
import FssView from './fss-view/FssView.vue';
import Icon from './icon/Icon.vue';
import InputDropdown from './input-dropdown/InputDropdown.vue';
import InputNumber from './input-number/InputNumber.vue';
import Paged from './paged/Paged.vue';
import PermissionTree from './permission-tree/PermissionTree.vue';
import QueryForm from './query-form/QueryForm.vue';
import QueryTable from './query-table/QueryTable.vue';
import RegionCascader from './region-cascader/RegionCascader.vue';
import Select from './select/Select.vue';
import Slider from './slider/Slider.vue';
import StepsNav from './steps-nav/StepsNav.vue';
import SubmitForm from './submit-form/SubmitForm.vue';
import TabColumn from './table-column/TableColumn.vue';
import Tabs from './tabs/Tabs.vue';
import Transfer from './transfer/Transfer.vue';
import Upload from './upload/Upload.vue';
import './tnxel.css';
export {util};
export {Validator};
const dialogContainerClass = 'tnxel-dialog-container';
const drawerContainerClass = 'tnxel-drawer-container';
type VueComponent = Vue.Component & {
components?: Record<string, Vue.Component>;
};
type ModalComponentInstance = Vue.ComponentPublicInstance & {
id: string;
close: () => Promise<void>;
}
export default class TnxEl extends TnxVue {
validator: Validator;
constructor(apiBaseUrl: string, id: string = 'tnxel') {
super(apiBaseUrl, id);
this.foundations.Validator = Validator;
this.validator = new Validator();
this.libs.ElementPlus = ElementPlus;
Object.assign(this.components, {
CaptchaVerify,
Avatar,
Alert,
Button,
CheckIcon,
CloseErrorButton,
Curd,
DatePicker,
DateRange,
DateTimePicker,
DetailForm,
Dialog,
Drawer,
DropdownItem,
EditTable,
EnumSelect,
EnumView,
FetchCascader,
FetchSelect,
FetchTags,
FssUpload,
FssView,
Icon,
InputDropdown,
InputNumber,
Paged,
PermissionTree,
QueryForm,
QueryTable,
RegionCascader,
Select,
Slider,
StepsNav,
SubmitForm,
TabColumn,
Tabs,
Transfer,
Upload,
});
if (this.router) {
this.router.beforeLeave = this.util.function.around(this.router.beforeLeave, (beforeLeave, to) => {
// 页面跳转前关闭当前页面中可能存在的所有消息框和对话框
this.closeMessage();
this.closeDialog(true);
beforeLeave.call(this.router, to);
});
}
}
install(app: Vue.App): void {
super.install(app);
// 始终安装ElementPlus,以避免对于不同Vue实例未安装的问题
app.use(ElementPlus, {
locale: ElementPlus_zh_CN,
});
}
private dialogInstances: ModalComponentInstance[] = []; // 对话框实例堆栈
dialog(content: string | Vue.Component | Vue.DefineComponent,
title?: string,
options: DialogOptions = {},
contentProps: Record<string, any> = {}): ModalComponentInstance {
this.closeMessage();
let componentDefinition: VueComponent = Object.assign({}, Dialog);
if (this.isComponent(content)) {
const components = Object.assign({}, Dialog.components || {});
components['tnxel-dialog-content'] = content;
componentDefinition.components = components;
content = null;
}
const dialogId = 'dialog-' + (new Date().getTime());
const container = document.createElement('div');
container.className = dialogContainerClass;
container.id = dialogId;
document.body.appendChild(container);
const buttons = this.getOptionsButtons(options);
const containerSelector = '.' + dialogContainerClass + '#' + dialogId;
let dialogVm = this.createVueApp(componentDefinition, null, {
modelValue: true,
container: containerSelector,
title: title,
content: content,
contentProps: contentProps,
buttons: buttons,
theme: options.theme,
});
const dialog = dialogVm.mount(containerSelector) as ModalComponentInstance;
dialog.$options = Object.assign(dialog.$options || {}, options);
dialog.$options.onClosed = this.util.function.around(dialog.$options.onClosed, (onClosed: () => void): void => {
dialogVm.unmount();
this.dialogInstances.remove(dialog);
const containerElement = document.querySelector(containerSelector) as HTMLElement | null;
if (containerElement) {
const nextElement = containerElement.nextElementSibling;
if (nextElement && nextElement.classList.contains('el-overlay')) {
nextElement.remove();
}
containerElement.remove();
}
if (onClosed) {
onClosed.call(dialog);
}
});
this.dialogInstances.push(dialog);
return dialog;
}
closeDialog(all?: boolean): Promise<void> {
return new Promise<void>((resolve) => {
if (this.dialogInstances.length) {
let dialog = this.dialogInstances.pop();
let promises = [];
while (dialog) {
promises.push(dialog.close());
if (all) {
dialog = this.dialogInstances.pop();
} else {
break;
}
}
Promise.all(promises).then(() => {
resolve();
});
} else {
resolve();
}
});
}
private drawerInstances: ModalComponentInstance[] = []; // 抽屉实例堆栈
drawer(content: string | Vue.Component,
title?: string,
options: DrawerOptions = {},
contentProps: Record<string, any> = {}): ModalComponentInstance {
this.closeMessage();
let componentDefinition: VueComponent = Object.assign({}, Drawer);
if (this.isComponent(content)) {
const components = Object.assign({}, Drawer.components || {});
components['tnxel-drawer-content'] = content;
componentDefinition.components = components;
content = null;
}
const drawerId = 'drawer-' + (new Date().getTime());
const container = document.createElement('div');
container.className = drawerContainerClass;
container.id = drawerId;
document.body.appendChild(container);
const buttons = this.getOptionsButtons(options);
const containerSelector = '.' + drawerContainerClass + '#' + drawerId;
let drawerVm = this.createVueApp(componentDefinition, null, {
content: content,
title: title,
contentProps: contentProps,
buttons: buttons,
theme: options.theme,
});
const drawer = drawerVm.mount(containerSelector) as ModalComponentInstance;
drawer.$options = Object.assign(drawer.$options || {}, options);
drawer.$options.onClosed = this.util.function.around(drawer.$options.onClosed, (onClosed: () => void): void => {
drawerVm.unmount();
this.drawerInstances.remove(drawer);
const containerElement = document.querySelector(containerSelector) as HTMLElement | null;
if (containerElement) {
const nextElement = containerElement.nextElementSibling;
if (nextElement && nextElement.classList.contains('el-overlay')) {
nextElement.remove();
}
containerElement.remove();
}
if (onClosed) {
onClosed.call(drawer);
}
});
this.drawerInstances.push(drawer);
return drawer;
}
closeDrawer(all?: boolean): Promise<void> {
return new Promise<void>((resolve) => {
if (this.drawerInstances.length) {
let drawer = this.drawerInstances.pop();
let promises = [];
while (drawer) {
promises.push(drawer.close());
if (all) {
drawer = this.drawerInstances.pop();
} else {
break;
}
}
Promise.all(promises).then(() => {
resolve();
});
} else {
resolve();
}
});
}
private closeMessage() {
ElMessage.closeAll();
this.closeLoading();
}
private handleZIndex(selector: string): void {
setTimeout(() => {
const topZIndex = window.tnx.util.dom.minTopZIndex(2);
if (selector.endsWith(':last')) {
selector = selector.substring(0, selector.length - ':last'.length);
}
const elements = document.querySelectorAll(selector);
const el = elements.length > 0 ? elements.item(elements.length - 1) as HTMLElement : null;
if (el) {
const zIndexStr = window.getComputedStyle(el).zIndex;
const zIndex = Number(zIndexStr);
if (isNaN(zIndex) || topZIndex > zIndex) {
el.style.zIndex = String(topZIndex);
const modal = el.nextElementSibling as HTMLElement | null;
if (modal && modal.classList.contains('v-modal')) {
modal.style.zIndex = String(topZIndex - 1);
}
}
}
});
}
toast(message: string, timeout?: number): Promise<void> {
this.closeMessage();
return new Promise<void>((resolve) => {
let options: MessageParams = {
type: 'success', // 默认为成功主题,可更改为其它主题
offset: this.util.dom.getDocHeight() * 0.4,
dangerouslyUseHTMLString: true,
showClose: false,
message: message,
duration: timeout || 2000,
customClass: 'tnxel-toast',
onClose: () => {
resolve();
}
};
ElMessage(options);
this.handleZIndex('.el-message:last');
this.eventBus.emit('tnx.toast', options);
});
}
async alert(message: string, title: string = '提示'): Promise<void> {
let options: ElMessageBoxOptions = {
title: title,
dangerouslyUseHTMLString: true,
type: 'warning',
confirmButtonText: '确定',
};
this.closeMessage();
await ElMessageBox.alert(message, options);
this.handleZIndex('.el-message-box__wrapper:last');
this.eventBus.emit('tnx.alert', Object.assign({}, options, {message}));
}
async success(message: string): Promise<void> {
let options: ElMessageBoxOptions = {
title: '成功',
dangerouslyUseHTMLString: true,
type: 'success',
confirmButtonText: '确定',
};
this.closeMessage();
await ElMessageBox.alert(message, options);
this.handleZIndex('.el-message-box__wrapper:last');
this.eventBus.emit('tnx.success', Object.assign({}, options, {message}));
}
async error(message: string): Promise<void> {
let options: ElMessageBoxOptions = {
title: '错误',
dangerouslyUseHTMLString: true,
type: 'error',
confirmButtonText: '确定',
};
this.closeMessage();
await ElMessageBox.alert(message, options);
this.handleZIndex('.el-message-box__wrapper:last');
this.eventBus.emit('tnx.error', Object.assign({}, options, {message}));
}
confirm(message: string, title = '确认') {
let options: ElMessageBoxOptions = {
title: title,
type: 'info',
icon: Vue.markRaw(Icon.components.QuestionFilled),
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
distinguishCancelAndClose: true,
};
// if (options.reverse) {
// options.customClass = 'reverse';
// let buttonText = options.confirmButtonText;
// options.confirmButtonText = options.cancelButtonText;
// options.cancelButtonText = buttonText;
// }
this.closeMessage();
return new Promise<boolean>(resolve => {
ElMessageBox.confirm(message, options).then(() => {
resolve(true);
}).catch(() => {
resolve(false);
});
this.handleZIndex('.el-message-box__wrapper:last');
this.eventBus.emit('tnx.confirm', Object.assign({}, options, {message}));
})
}
private loadingInstance: LoadingInstance | null;
showLoading(message?: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.closeMessage();
try {
let options: LoadingOptions = {
text: message,
};
this.loadingInstance = ElLoading.service(options);
this.handleZIndex('.el-loading-mask');
this.eventBus.emit('tnx.showLoading', options);
let vm = this.loadingInstance.vm;
if (vm) {
this.nextTickTimeout(vm, () => {
resolve();
}, 500);
} else {
resolve();
}
} catch (e) {
this.loadingInstance = null;
console.error(e);
reject(e);
}
});
}
closeLoading(): void {
if (this.loadingInstance) { // 确保绝对的单例
this.loadingInstance.close();
this.loadingInstance = null;
}
}
hideLoading(): void {
this.closeLoading();
}
/**
* 对表格组件的格式化支持
*/
table = {
date: {
formatDateTime(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (cellValue) {
return new Date(cellValue).formatDateTime();
}
return undefined;
},
formatDate(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (cellValue) {
return new Date(cellValue).formatDate();
}
return undefined;
},
formatTime(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (typeof cellValue === 'number') {
cellValue = new Date(cellValue);
}
if (cellValue instanceof Date) {
cellValue = cellValue.formatTime();
}
if (typeof cellValue === 'string') {
return cellValue;
}
return undefined;
},
formatTimeMinute(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (typeof cellValue === 'number') {
cellValue = new Date(cellValue);
}
if (cellValue instanceof Date) {
cellValue = cellValue.formatTimeMinute();
}
if (typeof cellValue === 'string') {
let array = cellValue.split(':');
if (array.length > 1) {
return array[0] + ':' + array[1];
}
}
return undefined;
},
formatDateMinute(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (cellValue) {
return new Date(cellValue).formatDateMinute();
}
return undefined;
},
formatDateMonth(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (cellValue) {
return new Date(cellValue).formatDateMonth();
}
return undefined;
},
formatPermanentableDate(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (Array.isArray(cellValue)) {
cellValue = cellValue[columnIndex];
}
return util.date.formatPermanentableDate(cellValue);
},
/**
* 将Java标准的日期格式转换为Day.js的日期格式
* @param format Java标准的日期格式
* @returns {String} Day.js的日期格式
*/
toDayJsDateFormat(format: string): string {
return format.replaceAll('y', 'Y').replaceAll('d', 'D');
},
},
number: {
formatPercent(row: Record<string, any>, columnIndex: number, cellValue: any): string | undefined {
if (typeof cellValue !== 'number') {
cellValue = parseFloat(cellValue);
}
if (!isNaN(cellValue)) {
return cellValue.toPercent();
}
return undefined;
}
},
boolean: {
items: {
getText(type: string, value: any): string | undefined {
let items = this[type];
if (Array.isArray(items)) {
for (let item of items) {
if (item.value === value) {
return item.text;
}
}
}
return undefined;
},
has: [{
value: true,
text: '有',
}, {
value: false,
text: '无',
}]
},
format(row: Record<string, any>, columnIndex: number, cellValue: any): any {
if (typeof cellValue === 'boolean') {
cellValue = cellValue.toText();
}
return cellValue;
},
formatHas(row: Record<string, any>, columnIndex: number, cellValue: any): any {
if (typeof cellValue === 'boolean') {
cellValue = this.items.getText('has', cellValue);
}
return cellValue;
}
},
}
}