18 changed files with 4056 additions and 2232 deletions
File diff suppressed because it is too large
@ -0,0 +1,167 @@ |
|||||
|
<template> |
||||
|
<div v-element-dialog-resize="{ draggable: draggable, fullscreen: draggable }"> |
||||
|
<el-dialog :title="title" :width="width" v-model="isShow" :modal="modal" |
||||
|
:class="['yv-dialog-wrap', ...calcClass]" |
||||
|
:style="calcStyle" |
||||
|
:closeOnClickModal="closeOnClickModal" |
||||
|
:closeOnPressEscape="closeOnPressEscape" |
||||
|
append-to-body destroyOnClose |
||||
|
@closed="onClosed" |
||||
|
@opened="onOpened"> |
||||
|
<component v-if="childCmp" :is="childCmp" |
||||
|
v-bind="_input" |
||||
|
ref="childCmpRef" |
||||
|
@ok="onOk" |
||||
|
@closeDialog="onClose" |
||||
|
/> |
||||
|
<template #footer> |
||||
|
<div class="footer"> |
||||
|
<el-button v-if="showCancelButton" @click="onClose">{{ cancelButtonText }}</el-button> |
||||
|
<el-button v-if="showOkButton" type="primary" @click="onOkHandle">{{ okButtonText }}</el-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script> |
||||
|
import { markRaw } from 'vue' |
||||
|
import ElementDialogResize from '@/components/element-dialog-resize' |
||||
|
|
||||
|
export default { |
||||
|
directives: { ElementDialogResize }, |
||||
|
props: { |
||||
|
title: { type: String, default: '对话框', required: false }, |
||||
|
draggable: { type: Boolean, default: () => false, required: false }, |
||||
|
width: { type: [String, Number], default: () => 600, required: false }, |
||||
|
height: { type: Number, defaults: () => 0, required: false }, |
||||
|
|
||||
|
showClose: { type: Boolean, default: () => true, required: false }, |
||||
|
showMax: { type: Boolean, default: () => false, required: false }, |
||||
|
modal: { type: Boolean, default: () => true, required: false }, |
||||
|
showCancelButton: { type: Boolean, default: () => true, required: false }, |
||||
|
showOkButton: { type: Boolean, default: () => true, required: false }, |
||||
|
cancelButtonText: { type: String, default: () => '取消', required: false }, |
||||
|
okButtonText: { type: String, default: () => '确定', required: false }, |
||||
|
|
||||
|
dialogClass: { type: String, default: '', required: false }, |
||||
|
|
||||
|
closeOnClickModal: { type: Boolean, default: () => false, required: false }, |
||||
|
closeOnPressEscape: { type: Boolean, default: () => true, required: false }, |
||||
|
|
||||
|
_insId: { type: String, required: true }, |
||||
|
_input: { type: Object, defaults: {}, required: true }, |
||||
|
dialogResolve: { type: Function, required: true }, |
||||
|
dialogReject: { type: Function, required: true }, |
||||
|
|
||||
|
childCmp: { type: Object, default: undefined, required: false } |
||||
|
}, |
||||
|
mounted() { |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isShow: true, |
||||
|
returnOk: false |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
onOk(result) { |
||||
|
// 通过收到业务组件 bizMixin.js 的 OK 事件,引起窗口关闭 |
||||
|
this.isShow = false |
||||
|
this.returnOk = true |
||||
|
this.$nextTick(() => { |
||||
|
this.dialogResolve(result) |
||||
|
}) |
||||
|
}, |
||||
|
onOkHandle() { |
||||
|
if (typeof this.$refs.childCmpRef['okClick'] === 'function') { |
||||
|
const data = this.$refs.childCmpRef['okClick']() |
||||
|
if (data) { |
||||
|
this.onOk(data) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onClose() { |
||||
|
this.isShow = false |
||||
|
}, |
||||
|
onClosed() { |
||||
|
if (!this.returnOk) { |
||||
|
// 只要不是正常通过 returnOk 关闭的,都是返回 Promise.reject |
||||
|
this.dialogReject() |
||||
|
} |
||||
|
_.remove(system.rootElementList, (item) => item.props._insId === this._insId) |
||||
|
}, |
||||
|
onOpened() { |
||||
|
|
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
calcClass() { |
||||
|
const clazz = [] |
||||
|
if (this.draggable) { |
||||
|
clazz.push('resize-dialog') |
||||
|
} |
||||
|
if (this.dialogClass) { |
||||
|
clazz.push(this.dialogClass) |
||||
|
} |
||||
|
return clazz |
||||
|
}, |
||||
|
calcStyle() { |
||||
|
const style = {} |
||||
|
if (this.height > 0) { |
||||
|
style.height = this.height + 'px' |
||||
|
} |
||||
|
return style |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.yv-dialog-wrap { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.resize-dialog { |
||||
|
min-height: 200px; |
||||
|
min-width: 300px; |
||||
|
padding: 5px !important; |
||||
|
//height: 400px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
|
||||
|
.fullbtn-container { |
||||
|
height: 100%; |
||||
|
line-height: 45px; |
||||
|
width: 30px; |
||||
|
text-align: center; |
||||
|
|
||||
|
&:hover { |
||||
|
color: var(--frist-menu-active-color) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.closebtn-container { |
||||
|
height: 100%; |
||||
|
line-height: 45px; |
||||
|
width: 30px; |
||||
|
text-align: center; |
||||
|
|
||||
|
&:hover { |
||||
|
color: var(--frist-menu-active-color) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-draggable { |
||||
|
& > .el-dialog__header { |
||||
|
cursor: move; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.el-dialog__body { |
||||
|
flex-grow: 1; |
||||
|
padding: 0 !important; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,38 @@ |
|||||
|
import $ from 'jquery' |
||||
|
import _ from 'lodash' |
||||
|
|
||||
|
/** |
||||
|
* el-drawer 拖拽指令 |
||||
|
*/ |
||||
|
export default { |
||||
|
mounted(el, binding, vnode, oldVnode) { |
||||
|
const drawerEle = $(el).closest('.el-drawer')[0] |
||||
|
// 创建触发拖拽的元素
|
||||
|
const dragItem = document.createElement('div') |
||||
|
// 将元素放置到抽屉的左边边缘
|
||||
|
dragItem.style.cssText = 'height: 100%;width: 5px;cursor: w-resize;position: absolute;left: 0;' |
||||
|
drawerEle?.append(dragItem) |
||||
|
|
||||
|
dragItem.onmousedown = (downEvent) => { |
||||
|
// 拖拽时禁用文本选中
|
||||
|
document.body.style.userSelect = 'none' |
||||
|
|
||||
|
document.onmousemove = _.debounce(function (moveEvent) { |
||||
|
// 获取鼠标距离浏览器右边缘的距离
|
||||
|
let realWidth = document.body.clientWidth - moveEvent.pageX |
||||
|
const width30 = document.body.clientWidth * 0.2 |
||||
|
const width80 = document.body.clientWidth * 0.8 |
||||
|
// 宽度不能大于浏览器宽度 80%,不能小于宽度的 20%
|
||||
|
realWidth = realWidth > width80 ? width80 : realWidth < width30 ? width30 : realWidth |
||||
|
drawerEle.style.width = realWidth + 'px' |
||||
|
}, 0) |
||||
|
|
||||
|
document.onmouseup = function (e) { |
||||
|
// 拖拽时结束时,取消禁用文本选中
|
||||
|
document.body.style.userSelect = 'initial' |
||||
|
document.onmousemove = null |
||||
|
document.onmouseup = null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
import {computed, nextTick, watchEffect} from 'vue' |
||||
|
import {isArray} from 'lodash' |
||||
|
import useDraggable, {addUnit} from './use-draggable' |
||||
|
import useResizer from './use-resizer' |
||||
|
import useFullscreen from './use-fullscreen' |
||||
|
import type {DirectiveBinding} from 'vue' |
||||
|
import useCssVariable from './use-css-variable' |
||||
|
|
||||
|
const beforeMountedFun: Array<(...args: any) => any> = [] |
||||
|
|
||||
|
function handle(el: HTMLElement, binding: DirectiveBinding, vnode: any) { |
||||
|
if (!(isArray(vnode.children) && |
||||
|
(vnode.children[0] as any)?.props?.modelValue && |
||||
|
(vnode.children[0] as any)?.component?.subTree?.children?.default)) { |
||||
|
|
||||
|
// console.log(vnode.children)
|
||||
|
// console.log((vnode.children[0] as any)?.props?.modelValue)
|
||||
|
// console.log((vnode.children[0] as any)?.component?.subTree)
|
||||
|
// console.log((vnode.children[0] as any)?.component?.subTree.children[0])
|
||||
|
|
||||
|
return undefined |
||||
|
} |
||||
|
|
||||
|
nextTick(() => { |
||||
|
const dialogVnode = vnode.children[0].component |
||||
|
// const rootEl = dialogVnode.subTree.children[0].el as HTMLElement //luoyifan element-ui 2.9 升级
|
||||
|
const rootEl = dialogVnode.subTree.component.subTree.children[0].el.nextElementSibling as HTMLElement |
||||
|
if (rootEl.dataset.initialized === 'ok') return undefined |
||||
|
rootEl.dataset.initialized = 'ok' |
||||
|
const dialogOverlay: HTMLElement = rootEl.querySelector('.el-overlay-dialog')! |
||||
|
// 隐藏element这个奇怪设计导致拉伸太大而出现滚动条
|
||||
|
dialogOverlay.style.overflow = 'hidden' |
||||
|
const dialogEl: HTMLElement = rootEl.querySelector('div.el-dialog')! |
||||
|
dialogEl.style.display = 'flex' |
||||
|
dialogEl.style.flexDirection = 'column' |
||||
|
const dialogBodyEl: HTMLElement = dialogEl.querySelector('.el-dialog__body')! |
||||
|
dialogBodyEl.style.flexGrow = '1' |
||||
|
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')! |
||||
|
|
||||
|
const isEnableResizer = computed(() => true) |
||||
|
const isDraggable = computed(() => binding.value?.draggable ?? false) |
||||
|
if (binding.value?.closeIsHidden) { |
||||
|
watchEffect(() => { |
||||
|
const isShow = binding.value?.isShowFn() |
||||
|
if (isShow) { |
||||
|
dialogEl.style.display = 'flex' |
||||
|
rootEl.style.display = 'block' |
||||
|
} else { |
||||
|
dialogEl.style.display = 'none' |
||||
|
rootEl.style.display = 'none' |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
useCssVariable(dialogEl) |
||||
|
useResizer(dialogEl, isEnableResizer, beforeMountedFun) |
||||
|
useDraggable(dialogEl, dialogHeaderEl, isDraggable, beforeMountedFun) |
||||
|
if (binding.value?.fullscreen) useFullscreen(dialogEl, dialogVnode, binding.value) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* element-plus dialog组件大小缩放功能 |
||||
|
* 不要使用ElDialog自带的 draggable 和 fullscreen |
||||
|
* @param draggable Boolean |
||||
|
* @example |
||||
|
* <div v-element-dialog-resize="{ draggable: true, fullscreen: true }"> |
||||
|
* <el-dialog v-model="dialogVisible" title="Tips" width="30%"> |
||||
|
* </el-dialog> |
||||
|
* </div> |
||||
|
*/ |
||||
|
export default { |
||||
|
mounted: handle, |
||||
|
updated: handle, |
||||
|
beforeUnmount: () => { |
||||
|
for (const fun of beforeMountedFun) { |
||||
|
if (typeof fun === 'function') { |
||||
|
fun() |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
import {watch, reactive} from 'vue' |
||||
|
import {useElementBounding} from '@vueuse/core' |
||||
|
import {addUnit} from './use-draggable' |
||||
|
|
||||
|
export default function useCssVariable(dialogEl: HTMLElement) { |
||||
|
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')! |
||||
|
{ |
||||
|
const dialogHeaderRect = useElementBounding(dialogHeaderEl) |
||||
|
watch( |
||||
|
() => dialogHeaderRect.height, |
||||
|
(height) => { |
||||
|
requestAnimationFrame(() => |
||||
|
dialogEl.style.setProperty('--el-dialog-header-height', addUnit(height.value)!) |
||||
|
) |
||||
|
}, |
||||
|
{immediate: true} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const dialogFooterEl: HTMLElement | null = dialogEl.querySelector('footer.el-dialog__footer') |
||||
|
if (dialogFooterEl) { |
||||
|
const dialogFooterRect = useElementBounding(dialogFooterEl) |
||||
|
watch( |
||||
|
() => dialogFooterRect.height, |
||||
|
(height) => { |
||||
|
requestAnimationFrame(() => |
||||
|
dialogEl.style.setProperty('--el-dialog-footer-height', addUnit(height.value)!) |
||||
|
) |
||||
|
}, |
||||
|
{immediate: true} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const dialogRect = useElementBounding(dialogEl) |
||||
|
watch( |
||||
|
() => reactive([dialogRect.width.value, dialogRect.height.value]), |
||||
|
([width, height]) => { |
||||
|
requestAnimationFrame(() => { |
||||
|
dialogEl.style.setProperty('--el-dialog-width', addUnit(width)!) |
||||
|
dialogEl.style.setProperty('--el-dialog-height', addUnit(height)!) |
||||
|
}) |
||||
|
}, |
||||
|
{immediate: true, deep: true} |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
import {watchEffect, unref} from 'vue' |
||||
|
import {isNumber, isString} from 'lodash' |
||||
|
import type {ComputedRef, Ref} from 'vue' |
||||
|
import useParseTranslate from './use-parse-translate' |
||||
|
|
||||
|
export function addUnit(value?: string | number, defaultUnit = 'px') { |
||||
|
if (!value) return '' |
||||
|
if (isString(value)) { |
||||
|
return value |
||||
|
} else if (isNumber(value)) { |
||||
|
return `${value}${defaultUnit}` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const useDraggable = ( |
||||
|
targetRef: Ref<HTMLElement | undefined> | HTMLElement, |
||||
|
dragRef: Ref<HTMLElement | undefined> | HTMLElement, |
||||
|
draggable: ComputedRef<boolean>, |
||||
|
beforeMountedFun: Array<(...args: any) => any> |
||||
|
) => { |
||||
|
const targetRef_ = unref(targetRef)! |
||||
|
const dragRef_ = unref(dragRef)! |
||||
|
|
||||
|
const onMousedown = (e: MouseEvent) => { |
||||
|
const downX = e.clientX |
||||
|
const downY = e.clientY |
||||
|
const {x: offsetX, y: offsetY} = useParseTranslate(targetRef_!.style.transform) |
||||
|
|
||||
|
const targetRect = targetRef_!.getBoundingClientRect() |
||||
|
const targetLeft = targetRect.left |
||||
|
const targetTop = targetRect.top |
||||
|
const targetWidth = targetRect.width |
||||
|
const targetHeight = targetRect.height |
||||
|
|
||||
|
const clientWidth = document.documentElement.clientWidth |
||||
|
const clientHeight = document.documentElement.clientHeight |
||||
|
|
||||
|
const minLeft = -targetLeft + offsetX |
||||
|
const minTop = -targetTop + offsetY |
||||
|
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX |
||||
|
const maxTop = clientHeight - targetTop - targetHeight + offsetY |
||||
|
|
||||
|
const onMousemove = (e: MouseEvent) => { |
||||
|
requestAnimationFrame(() => { |
||||
|
const moveX = Math.min(Math.max(offsetX + e.clientX - downX, minLeft), maxLeft) |
||||
|
const moveY = Math.min(Math.max(offsetY + e.clientY - downY, minTop), maxTop) |
||||
|
|
||||
|
targetRef_!.style.transform = `translate(${addUnit(moveX)}, ${addUnit(moveY)})` |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const onMouseup = () => { |
||||
|
document.removeEventListener('mousemove', onMousemove) |
||||
|
document.removeEventListener('mouseup', onMouseup) |
||||
|
} |
||||
|
|
||||
|
document.addEventListener('mousemove', onMousemove) |
||||
|
document.addEventListener('mouseup', onMouseup) |
||||
|
} |
||||
|
|
||||
|
const onDraggable = () => { |
||||
|
if (dragRef_ && targetRef_) { |
||||
|
targetRef_.className = `${targetRef_.className} is-draggable` |
||||
|
dragRef_.addEventListener('mousedown', onMousedown) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const offDraggable = () => { |
||||
|
if (dragRef_ && targetRef_) { |
||||
|
targetRef_.className = targetRef_.className.replace('is-draggable', '') |
||||
|
dragRef_.removeEventListener('mousedown', onMousedown) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (draggable.value) { |
||||
|
onDraggable() |
||||
|
} else { |
||||
|
offDraggable() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
beforeMountedFun.push(offDraggable) |
||||
|
} |
||||
|
export default useDraggable |
||||
@ -0,0 +1,82 @@ |
|||||
|
import { render, nextTick, h } from 'vue' |
||||
|
//@ts-ignore
|
||||
|
import { FullScreen as IconFullScreen, Close as IconClose } from '@element-plus/icons-vue' |
||||
|
import { renderIcon } from '@/utils/webutils' |
||||
|
|
||||
|
export default function useFullscreen(dialogEl: HTMLElement, dialogVnode: any, bindingValue: any) { |
||||
|
//@ts-ignore
|
||||
|
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')! |
||||
|
|
||||
|
let allowDraggable = false |
||||
|
let widthBeforeFullscreen: number | undefined = undefined |
||||
|
|
||||
|
function onToggleFullScreen() { |
||||
|
if (dialogEl.className.includes('is-draggable')) allowDraggable = true |
||||
|
if (!dialogVnode.props.fullscreen) { |
||||
|
// 准备切换全屏,记录当前宽度
|
||||
|
widthBeforeFullscreen = dialogHeaderEl.getBoundingClientRect().width |
||||
|
} |
||||
|
if (dialogVnode.props.fullscreen) { |
||||
|
if (!widthBeforeFullscreen) return undefined |
||||
|
// 准备缩小,恢复记录的宽度
|
||||
|
dialogEl.style.setProperty('--el-dialog-width', `${widthBeforeFullscreen}px`) |
||||
|
dialogEl.style.width = `${widthBeforeFullscreen}px` |
||||
|
widthBeforeFullscreen = undefined |
||||
|
} |
||||
|
dialogVnode.props.fullscreen = !dialogVnode.props.fullscreen |
||||
|
if (!dialogVnode.props.fullscreen && allowDraggable) { |
||||
|
// 从全屏变回窗口,要加上 is-draggable
|
||||
|
nextTick(() => { |
||||
|
dialogEl.className = `${dialogEl.className} is-draggable` |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onCloseDialog() { |
||||
|
if (bindingValue.closeIsHidden) { |
||||
|
// 关闭只是隐藏,就抛出 close 事件
|
||||
|
dialogVnode.emit('close') |
||||
|
|
||||
|
} else { |
||||
|
dialogVnode.props.modelValue = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dialogHeaderEl.ondblclick = onToggleFullScreen |
||||
|
const dialogHeaderButtonEl: HTMLElement | null = |
||||
|
dialogHeaderEl.querySelector('.el-dialog__headerbtn') |
||||
|
if (dialogHeaderButtonEl) { |
||||
|
dialogHeaderEl.removeChild(dialogHeaderButtonEl) |
||||
|
} |
||||
|
const customDialogHeaderButtonEl = document.createElement('div') |
||||
|
customDialogHeaderButtonEl.className = 'el-dialog__headerbtn' |
||||
|
customDialogHeaderButtonEl.style.display = 'flex' |
||||
|
customDialogHeaderButtonEl.style.alignItems = 'center' |
||||
|
customDialogHeaderButtonEl.style.width = 'auto' |
||||
|
customDialogHeaderButtonEl.style.marginRight = '5px' |
||||
|
|
||||
|
const iconFullscreenContainer = document.createElement('div') |
||||
|
iconFullscreenContainer.className = 'fullbtn-container' |
||||
|
iconFullscreenContainer.onclick = onToggleFullScreen |
||||
|
const iconFullscreenVnode = h(renderIcon('FullScreen')) |
||||
|
// <IconFullScreen onClick={onToggleFullScreen}/>
|
||||
|
render(iconFullscreenVnode, iconFullscreenContainer) |
||||
|
customDialogHeaderButtonEl.appendChild(iconFullscreenContainer) |
||||
|
|
||||
|
const iconCloseContainer = document.createElement('div') |
||||
|
iconCloseContainer.className = 'closebtn-container' |
||||
|
iconCloseContainer.onclick = onCloseDialog |
||||
|
const iconCloseVnode = h(renderIcon('Close')) |
||||
|
// <IconClose onClick={onCloseDialog}/>
|
||||
|
render(iconCloseVnode, iconCloseContainer) |
||||
|
customDialogHeaderButtonEl.appendChild(iconCloseContainer) |
||||
|
|
||||
|
dialogHeaderEl.appendChild(customDialogHeaderButtonEl) |
||||
|
|
||||
|
// 加入覆盖样式
|
||||
|
const style = document.createElement('style') |
||||
|
style.type = 'text/css' |
||||
|
// --el-dialog-width: 100%!important;
|
||||
|
style.innerHTML = '.el-dialog.is-fullscreen {width:100%!important;height:100%!important;}' |
||||
|
document.querySelector('head')!.appendChild(style) |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
export default function useParseTranslate(translate: string | undefined) { |
||||
|
if (translate === '') return {x: 0, y: 0} |
||||
|
return { |
||||
|
x: translate ? Number(translate.slice(translate.indexOf('(') + 1, translate.indexOf('px'))) : 0, |
||||
|
y: translate |
||||
|
? Number(translate.slice(translate.indexOf(',') + 1, translate.indexOf(')') - 2)) |
||||
|
: 0, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
import {watchEffect, unref} from 'vue' |
||||
|
import type {ComputedRef, Ref} from 'vue' |
||||
|
import useParseTranslate from './use-parse-translate' |
||||
|
import {addUnit} from './use-draggable' |
||||
|
|
||||
|
export default function useResizer( |
||||
|
dialogRef: Ref<HTMLElement | undefined> | HTMLElement, |
||||
|
isEnable: ComputedRef<boolean>, |
||||
|
beforeMountedFun: Array<(...args: any) => any> |
||||
|
) { |
||||
|
const dialogEl = unref(dialogRef)! |
||||
|
// dialogEl.style.setProperty('--el-dialog-width', addUnit(dialogEl.getBoundingClientRect().width)!)
|
||||
|
// dialogEl.style.setProperty(
|
||||
|
// '--el-dialog-height',
|
||||
|
// addUnit(dialogEl.getBoundingClientRect().height)!
|
||||
|
// )
|
||||
|
|
||||
|
const resizerEl = document.createElement('div') |
||||
|
resizerEl.className = 'el-dialog-resizer' |
||||
|
resizerEl.style.width = '15px' |
||||
|
resizerEl.style.height = '15px' |
||||
|
resizerEl.style.zIndex = '3000' |
||||
|
resizerEl.style.background = 'transparent' |
||||
|
resizerEl.style.position = 'absolute' |
||||
|
resizerEl.style.bottom = '0' |
||||
|
resizerEl.style.right = '0' |
||||
|
resizerEl.style.cursor = 'se-resize' |
||||
|
|
||||
|
let dialogDefaultRect: DOMRect | undefined = undefined |
||||
|
let resizerDefaultRect: DOMRect | undefined = undefined |
||||
|
const mouseOffsetInResizer = {x: 0, y: 0} |
||||
|
let dialogDefaultTranslate: string | undefined = undefined |
||||
|
|
||||
|
function onMouseMove(ev: MouseEvent) { |
||||
|
requestAnimationFrame(() => { |
||||
|
const deltaWidth = ev.clientX - dialogDefaultRect!.right + mouseOffsetInResizer.x |
||||
|
const deltaHeight = ev.clientY - dialogDefaultRect!.bottom + mouseOffsetInResizer.y |
||||
|
const {x: translateX, y: translateY} = useParseTranslate(dialogDefaultTranslate) |
||||
|
|
||||
|
const newWidth = `${dialogDefaultRect!.width + deltaWidth}px` |
||||
|
const newHeight = `${dialogDefaultRect!.height + deltaHeight}px` |
||||
|
// dialogEl.style.setProperty('--el-dialog-width', newWidth)
|
||||
|
// dialogEl.style.setProperty('--el-dialog-height', newHeight)
|
||||
|
dialogEl.style.width = newWidth |
||||
|
dialogEl.style.height = newHeight |
||||
|
dialogEl.style.transform = `translate(${translateX + deltaWidth / 2}px,${translateY}px)` |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const onMouseDown = (ev: MouseEvent) => { |
||||
|
document.body.style.userSelect = 'none' |
||||
|
document.body.style.cursor = 'se-resize' |
||||
|
dialogDefaultRect = dialogEl.getBoundingClientRect() |
||||
|
resizerDefaultRect = resizerEl.getBoundingClientRect() |
||||
|
mouseOffsetInResizer.x = resizerDefaultRect.right - ev.clientX |
||||
|
mouseOffsetInResizer.y = resizerDefaultRect.bottom - ev.clientY |
||||
|
dialogDefaultTranslate = dialogEl.style.transform === '' ? undefined : dialogEl.style.transform |
||||
|
|
||||
|
function onMouseUp() { |
||||
|
requestAnimationFrame(() => { |
||||
|
dialogDefaultRect = undefined |
||||
|
resizerDefaultRect = undefined |
||||
|
dialogDefaultTranslate = undefined |
||||
|
document.body.style.userSelect = '' |
||||
|
document.body.style.cursor = '' |
||||
|
}) |
||||
|
document.removeEventListener('mousemove', onMouseMove) |
||||
|
document.removeEventListener('mouseup', onMouseUp) |
||||
|
} |
||||
|
|
||||
|
document.addEventListener('mousemove', onMouseMove) |
||||
|
document.addEventListener('mouseup', onMouseUp) |
||||
|
} |
||||
|
|
||||
|
const onResizer = () => { |
||||
|
if (resizerEl && dialogEl) { |
||||
|
resizerEl.addEventListener('mousedown', onMouseDown) |
||||
|
dialogEl.appendChild(resizerEl) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const offResizer = () => { |
||||
|
if (resizerEl && dialogEl) { |
||||
|
resizerEl.removeEventListener('mousedown', onMouseDown) |
||||
|
try { |
||||
|
dialogEl.removeChild(resizerEl) |
||||
|
} catch (e) { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (isEnable.value) { |
||||
|
onResizer() |
||||
|
} else { |
||||
|
offResizer() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
beforeMountedFun.push(offResizer) |
||||
|
} |
||||
@ -0,0 +1,194 @@ |
|||||
|
<template> |
||||
|
<div class="model3d-view"> |
||||
|
<div class="toolbar"> |
||||
|
<input type="file" @change="handleFileUpload" accept=".fbx,.obj,.mtl" /> |
||||
|
<span>文件上传</span> |
||||
|
</div> |
||||
|
<div class="main-content"> |
||||
|
<!-- Three.js 渲染画布 --> |
||||
|
<div class="canvas-container" ref="canvasContainer"></div> |
||||
|
|
||||
|
<!-- 右侧面板 --> |
||||
|
<div class="gui-panel" ref="guiPanel"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import { ref, onMounted } from 'vue' |
||||
|
import * as THREE from 'three' |
||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
||||
|
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' |
||||
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' |
||||
|
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' |
||||
|
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js' |
||||
|
|
||||
|
// DOM refs |
||||
|
const canvasContainer = ref(null) |
||||
|
const guiPanel = ref(null) |
||||
|
|
||||
|
// Three.js 场景相关 |
||||
|
let scene, camera, renderer, controls |
||||
|
let modelGroup = new THREE.Group() |
||||
|
let gui |
||||
|
|
||||
|
// 状态变量 |
||||
|
const state = { |
||||
|
autoRotate: false, |
||||
|
showAxesHelper: true, |
||||
|
showGridHelper: true, |
||||
|
cameraPosition: [0, 5, 10] |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
initThree() |
||||
|
initGUI() |
||||
|
}) |
||||
|
|
||||
|
function initThree() { |
||||
|
// 场景 |
||||
|
scene = new THREE.Scene() |
||||
|
scene.background = new THREE.Color(0xeeeeee) |
||||
|
|
||||
|
// 相机 |
||||
|
camera = new THREE.PerspectiveCamera( |
||||
|
75, |
||||
|
window.innerWidth / 2, |
||||
|
0.1, |
||||
|
1000 |
||||
|
) |
||||
|
camera.position.set(...state.cameraPosition) |
||||
|
|
||||
|
// 渲染器 |
||||
|
renderer = new THREE.WebGLRenderer({ antialias: true }) |
||||
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8) |
||||
|
canvasContainer.value.appendChild(renderer.domElement) |
||||
|
|
||||
|
// 控制器 |
||||
|
controls = new OrbitControls(camera, renderer.domElement) |
||||
|
|
||||
|
// 辅助线 |
||||
|
const axesHelper = new THREE.AxesHelper(5) |
||||
|
scene.add(axesHelper) |
||||
|
const gridHelper = new THREE.GridHelper(20, 20) |
||||
|
scene.add(gridHelper) |
||||
|
|
||||
|
// 光照 |
||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6) |
||||
|
scene.add(ambientLight) |
||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8) |
||||
|
directionalLight.position.set(10, 10, 10) |
||||
|
scene.add(directionalLight) |
||||
|
|
||||
|
// 动画循环 |
||||
|
function animate() { |
||||
|
requestAnimationFrame(animate) |
||||
|
if (state.autoRotate) { |
||||
|
modelGroup.rotation.y += 0.01 |
||||
|
} |
||||
|
controls.update() |
||||
|
renderer.render(scene, camera) |
||||
|
} |
||||
|
|
||||
|
animate() |
||||
|
|
||||
|
// 调整窗口大小 |
||||
|
window.addEventListener('resize', () => { |
||||
|
camera.aspect = canvasContainer.value.clientWidth / window.innerHeight |
||||
|
camera.updateProjectionMatrix() |
||||
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function initGUI() { |
||||
|
gui = new dat.GUI({ autoPlace: false }) |
||||
|
guiPanel.value.appendChild(gui.domElement) |
||||
|
|
||||
|
// 自动旋转 |
||||
|
gui.add(state, 'autoRotate').name('自动旋转') |
||||
|
|
||||
|
// 显示辅助线 |
||||
|
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => { |
||||
|
scene.getObjectByName('AxesHelper').visible = val |
||||
|
}) |
||||
|
|
||||
|
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => { |
||||
|
scene.getObjectByName('GridHelper').visible = val |
||||
|
}) |
||||
|
|
||||
|
// 相机位置 |
||||
|
const cameraFolder = gui.addFolder('相机位置') |
||||
|
cameraFolder.add(state.cameraPosition, 0, -10, 10).name('X') |
||||
|
cameraFolder.add(state.cameraPosition, 1, -10, 10).name('Y') |
||||
|
cameraFolder.add(state.cameraPosition, 2, 5, 30).name('Z').onChange(() => { |
||||
|
camera.position.set(...state.cameraPosition) |
||||
|
}) |
||||
|
cameraFolder.open() |
||||
|
} |
||||
|
|
||||
|
function handleFileUpload(event) { |
||||
|
const file = event.target.files[0] |
||||
|
if (!file) return |
||||
|
|
||||
|
// 移除旧模型 |
||||
|
if (modelGroup.children.length > 0) { |
||||
|
modelGroup.children.forEach(child => modelGroup.remove(child)) |
||||
|
} |
||||
|
|
||||
|
const fileName = file.name.toLowerCase() |
||||
|
const reader = new FileReader() |
||||
|
|
||||
|
if (fileName.endsWith('.fbx')) { |
||||
|
reader.readAsArrayBuffer(file) |
||||
|
reader.onload = () => { |
||||
|
const loader = new FBXLoader() |
||||
|
const content = loader.parse(reader.result, '') |
||||
|
modelGroup.add(content) |
||||
|
scene.add(modelGroup) |
||||
|
} |
||||
|
} else if (fileName.endsWith('.obj')) { |
||||
|
reader.readAsText(file) |
||||
|
reader.onload = () => { |
||||
|
const loader = new OBJLoader() |
||||
|
const content = loader.parse(reader.result) |
||||
|
modelGroup.add(content) |
||||
|
scene.add(modelGroup) |
||||
|
} |
||||
|
} else if (fileName.endsWith('.mtl')) { |
||||
|
alert('需要同时上传 .obj 和 .mtl 文件,请先实现多文件上传处理逻辑。') |
||||
|
} else { |
||||
|
alert('不支持的文件类型!') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style scoped> |
||||
|
.dialog-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100vh; |
||||
|
} |
||||
|
|
||||
|
.toolbar { |
||||
|
padding: 10px; |
||||
|
background-color: #f0f0f0; |
||||
|
border-bottom: 1px solid #ccc; |
||||
|
} |
||||
|
|
||||
|
.main-content { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.canvas-container { |
||||
|
flex: 3; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.gui-panel { |
||||
|
flex: 1; |
||||
|
border-left: 1px solid #ccc; |
||||
|
background: #fff; |
||||
|
padding: 10px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,26 @@ |
|||||
|
import { defineMenu } from '@/runtime/DefineMenu.ts' |
||||
|
import Model3DView from '@/designer/Model3DView.vue' |
||||
|
|
||||
|
export default defineMenu((menus) => { |
||||
|
menus.insertChildren('tool', |
||||
|
{ |
||||
|
name: 'tool', label: '小工具', order: 3, disabled: false |
||||
|
}, |
||||
|
[ |
||||
|
{ |
||||
|
name: 'model3dview', label: '模型查看器', order: 1, |
||||
|
click: () => { |
||||
|
system.showDialog(Model3DView, { |
||||
|
title: '模型查看器', |
||||
|
width: 800, |
||||
|
height: 400, |
||||
|
showClose: true, |
||||
|
showMax: true, |
||||
|
showCancelButton: false, |
||||
|
showOkButton: false |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
) |
||||
|
}) |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { defineMenu } from '@/runtime/DefineMenu.ts' |
||||
|
import { renderIcon } from '@/utils/webutils.ts' |
||||
|
|
||||
|
export default defineMenu((menus) => { |
||||
|
menus.insertChildren('file', |
||||
|
{ |
||||
|
name: 'tool', label: '小工具', order: 3, disabled: false |
||||
|
}, |
||||
|
[] |
||||
|
) |
||||
|
}) |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue