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