index.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <template>
  2. <a-modal
  3. :class="['my-modal', modalClass, simpleClass]"
  4. :open="visible"
  5. v-bind="props"
  6. :width="modalWidth"
  7. :wrap-class-name="wrapClassName + fullscreenClass"
  8. @cancel="handleCancel"
  9. >
  10. <template #closeIcon>
  11. <template v-if="fullscreen">
  12. <a-tooltip title="还原" placement="bottom" v-if="fullscreenStatus">
  13. <fullscreen-exit-outlined @click="handleFullScreen" />
  14. </a-tooltip>
  15. <a-tooltip title="最大化" placement="bottom" v-else>
  16. <fullscreen-outlined @click="handleFullScreen" />
  17. </a-tooltip>
  18. </template>
  19. <a-tooltip title="关闭" placement="bottom">
  20. <close-outlined />
  21. </a-tooltip>
  22. </template>
  23. <slot></slot>
  24. <template #footer>
  25. <slot name="insertFooter"></slot>
  26. <slot name="footer">
  27. <a-button @click="handleCancel">
  28. {{ props.cancelText || '取消' }}
  29. </a-button>
  30. <slot name="centerFooter"></slot>
  31. <a-button type="primary" @click="handleOk" :loading="loading">
  32. {{ props.okText || '确定' }}
  33. </a-button>
  34. </slot>
  35. <slot name="appendFooter"></slot>
  36. </template>
  37. </a-modal>
  38. </template>
  39. <script setup>
  40. import mixinProps from './props.js'
  41. import { useSlots } from 'vue'
  42. const slots = useSlots()
  43. const props = defineProps({
  44. ...mixinProps,
  45. // 容器的类名
  46. modalClass: {
  47. type: String,
  48. default: 'modal-box'
  49. },
  50. // 对话框外层容器的类名
  51. wrapClassName: {
  52. type: String,
  53. default: ''
  54. },
  55. helpMessage: {
  56. type: String
  57. },
  58. // 可全屏
  59. fullscreen: {
  60. type: Boolean,
  61. default: true
  62. },
  63. // 可拖拽
  64. drag: {
  65. type: Boolean,
  66. default: true
  67. },
  68. // 可拉伸
  69. resize: {
  70. type: Boolean,
  71. default: false
  72. },
  73. // 是否显示
  74. visible: {
  75. type: Boolean,
  76. default: false
  77. },
  78. // 标题
  79. title: {
  80. type: String,
  81. default: undefined
  82. },
  83. // 宽度
  84. width: {
  85. type: [Number, String],
  86. default: '70%'
  87. },
  88. loading: {
  89. type: Boolean,
  90. default: undefined
  91. }
  92. })
  93. const emit = defineEmits(['ok', 'close', 'fullscreen'])
  94. const modalWidth = ref('')
  95. const contain = ref(null)
  96. // 拖拽
  97. const header = ref(null)
  98. const modalContent = ref(null)
  99. const mouseDownX = ref(0)
  100. const mouseDownY = ref(0)
  101. const deltaX = ref(0)
  102. const deltaY = ref(0)
  103. const sumX = ref(0)
  104. const sumY = ref(0)
  105. const onmousedown = ref(false)
  106. // 缩放
  107. const modalBody = ref(null)
  108. const myBody = ref(null)
  109. const prevModalWidth = ref(0)
  110. const prevModalHeight = ref(0)
  111. const prevBodyWidth = ref(0)
  112. const prevBodyHeight = ref(0)
  113. const startX = ref(0)
  114. const startY = ref(0)
  115. // 全屏
  116. const fullscreenClass = ref('')
  117. const fullscreenStatus = ref(false)
  118. const slotKeys = computed(() => {
  119. return Object.keys(slots)
  120. })
  121. const simpleClass = computed(() => {
  122. return Math.random().toString(36).substring(2)
  123. })
  124. onMounted(() => {
  125. nextTick(() => {
  126. initialEvent(props.visible)
  127. })
  128. })
  129. watch(
  130. () => props.visible,
  131. (newValue) => {
  132. nextTick(() => {
  133. initialEvent(props.visible)
  134. })
  135. }
  136. )
  137. watch(
  138. () => fullscreenStatus.value,
  139. (newValue) => {
  140. fullscreenClass.value = fullscreenStatus.value ? ' full-modal' : ''
  141. }
  142. )
  143. onBeforeUnmount(() => {
  144. removeMove()
  145. document.removeEventListener('mouseup', removeUp, false)
  146. removeResize()
  147. document.removeEventListener('mouseup', removeResize)
  148. })
  149. const changeWidth = (width) => {
  150. modalWidth.value = width
  151. }
  152. const handleFullScreen = (e) => {
  153. e?.stopPropagation()
  154. e?.preventDefault()
  155. fullscreenStatus.value = !fullscreenStatus.value
  156. emit('fullscreen', e)
  157. }
  158. const handleOk = (e) => {
  159. reset()
  160. emit('ok', e)
  161. }
  162. const handleCancel = (e) => {
  163. const classList = e.target?.classList
  164. // 过滤自定义关闭按钮的空白区域
  165. if (classList.contains('ant-modal-close-x') || classList.contains('ant-space-item')) {
  166. return
  167. }
  168. reset()
  169. emit('close', e)
  170. }
  171. const reset = () => {
  172. // 拖拽
  173. mouseDownX.value = 0
  174. mouseDownY.value = 0
  175. deltaX.value = 0
  176. deltaY.value = 0
  177. sumX.value = 0
  178. sumY.value = 0
  179. // 缩放
  180. prevModalWidth.value = 0
  181. prevModalHeight.value = 0
  182. prevBodyWidth.value = 0
  183. prevBodyHeight.value = 0
  184. startX.value = 0
  185. startY.value = 0
  186. // 全屏
  187. fullscreenStatus.value = false
  188. }
  189. const initialEvent = (visible) => {
  190. if (visible) {
  191. reset()
  192. // 获取控件
  193. document.removeEventListener('mouseup', removeUp, false)
  194. contain.value = document.getElementsByClassName(simpleClass.value)[0]
  195. changeWidth(props.width)
  196. if (props.drag === true) {
  197. header.value = contain.value.getElementsByClassName('ant-modal-header')[0]
  198. modalContent.value = contain.value.getElementsByClassName('ant-modal-content')[0]
  199. header.value.style.cursor = 'all-scroll'
  200. modalContent.value.style.left = 0
  201. modalContent.value.style.transform = 'translate(0px,0px)'
  202. // 拖拽事件监听
  203. header.value.onmousedown = (event) => {
  204. onmousedown.value = true
  205. mouseDownX.value = event.pageX
  206. mouseDownY.value = event.pageY
  207. document.body.onselectstart = () => false
  208. document.addEventListener('mousemove', handleMove, false)
  209. }
  210. document.addEventListener('mouseup', removeUp, false)
  211. }
  212. if (props.resize === true) {
  213. modalBody.value = contain.value.getElementsByClassName('ant-modal-content')[0]
  214. myBody.value = contain.value.getElementsByClassName('ant-modal-body')[0]
  215. modalBody.value.style.overflow = 'hidden'
  216. modalBody.value.style.resize = 'both'
  217. myBody.value.style.overflow = 'auto'
  218. myBody.value.style.height = 'auto'
  219. // 缩放事件监听
  220. modalBody.value.onmousedown = (event) => {
  221. event.preventDefault()
  222. const rect = modalBody.value.getBoundingClientRect()
  223. const rightBorder = rect.x + rect.width - 17
  224. const bottomBorder = rect.y + rect.height - 17
  225. if (event.clientX >= rightBorder && event.clientY >= bottomBorder) {
  226. prevModalWidth.value = modalBody.value.offsetWidth
  227. prevModalHeight.value = modalBody.value.offsetHeight
  228. prevBodyWidth.value = myBody.value.offsetWidth
  229. prevBodyHeight.value = myBody.value.offsetHeight
  230. startX.value = event.clientX
  231. startY.value = event.clientY
  232. document.addEventListener('mousemove', handleResize)
  233. }
  234. document.addEventListener('mouseup', removeResize)
  235. }
  236. }
  237. }
  238. }
  239. const handleMove = (event) => {
  240. if (fullscreenStatus.value) {
  241. return
  242. }
  243. const delta1X = event.pageX - mouseDownX.value
  244. const delta1Y = event.pageY - mouseDownY.value
  245. deltaX.value = delta1X
  246. deltaY.value = delta1Y
  247. modalContent.value.style.transform = `translate(${delta1X + sumX.value}px, ${delta1Y + sumY.value}px)`
  248. }
  249. const removeMove = () => {
  250. document.removeEventListener('mousemove', handleMove, false)
  251. }
  252. const removeUp = (event) => {
  253. document.body.onselectstart = () => true
  254. if (onmousedown.value && !(event.pageX === mouseDownX.value && event.pageY === mouseDownY.value)) {
  255. onmousedown.value = false
  256. sumX.value = sumX.value + deltaX.value
  257. sumY.value = sumY.value + deltaY.value
  258. }
  259. removeMove()
  260. }
  261. const handleResize = (event) => {
  262. if (fullscreenStatus.value) {
  263. return
  264. }
  265. const diffX = event.clientX - startX.value
  266. const diffY = event.clientY - startY.value
  267. const minWidth = 180
  268. const minHeight = 0
  269. if (prevBodyWidth.value + diffX > minWidth) {
  270. changeWidth(prevModalWidth.value + diffX + 'px')
  271. }
  272. if (prevBodyHeight.value + diffY > minHeight) {
  273. myBody.value.style.height = prevBodyHeight.value + diffY + 'px'
  274. }
  275. }
  276. const removeResize = () => {
  277. document.removeEventListener('mousemove', handleResize)
  278. }
  279. </script>
  280. <style lang="less">
  281. .ant-modal-close-x {
  282. margin-right: 10px;
  283. width: auto;
  284. .anticon {
  285. padding: 20px 10px;
  286. }
  287. }
  288. .full-modal {
  289. .ant-modal {
  290. top: 0 !important;
  291. right: 0 !important;
  292. bottom: 0 !important;
  293. left: 0 !important;
  294. width: 100% !important;
  295. height: 100% !important;
  296. max-width: 100% !important;
  297. max-height: 100% !important;
  298. }
  299. .ant-modal-content {
  300. display: flex;
  301. flex-direction: column;
  302. height: calc(100vh) !important;
  303. transform: translate(0px, 0px) !important;
  304. resize: none !important;
  305. }
  306. .ant-modal-header {
  307. cursor: default !important;
  308. }
  309. .ant-modal-body {
  310. flex: 1;
  311. }
  312. }
  313. </style>