UNPKG

@truenewx/tnxvue3

Version:

互联网技术解决方案:Vue3扩展支持

576 lines (528 loc) 21.5 kB
/** * 基于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; } }, } }