SmartDialog.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <template>
  2. <div v-show="visible" class="smart-dialog" :style="dialogStyle" @mousedown="bringToFront" @dblclick="handleDoubleClick">
  3. <div class="dialog-header" :class="{ 'is-draggable': draggable }" @mousedown="startDrag" v-if="title">
  4. <div class="title-content">
  5. <slot name="header">
  6. <span class="title">{{ title }}</span>
  7. </slot>
  8. </div>
  9. <span v-if="showClose" class="close-btn" @click.stop="close">✕</span>
  10. </div>
  11. <div v-else class="dialog-header not-title" :class="{ 'is-draggable': draggable }" @mousedown="startDrag"></div>
  12. <div v-if="title && false" class="dialog-divider"></div>
  13. <div class="dialog-body" :class="{ 'no-padding': noPadding }">
  14. <slot></slot>
  15. </div>
  16. <div v-if="resizable" class="resize-handle" @mousedown.prevent="startResize"></div>
  17. </div>
  18. </template>
  19. <script>
  20. let globalZIndex = 2000;
  21. // 在这里定义你的大屏设计稿基准宽度 (通常是 1920)
  22. const DESIGN_WIDTH = 1920;
  23. export default {
  24. name: 'SmartDialog',
  25. props: {
  26. id: { type: [String, Number], required: true },
  27. visible: { type: Boolean, default: false },
  28. title: { type: String, default: '提示' },
  29. center: { type: Boolean, default: true },
  30. position: { type: Object, default: () => ({ x: 0, y: 0 }) },
  31. showClose: { type: Boolean, default: true },
  32. draggable: { type: Boolean, default: true },
  33. resizable: { type: Boolean, default: true },
  34. noPadding: { type: Boolean, default: false },
  35. defaultWidth: { type: [Number, String], default: 350 },
  36. defaultHeight: { type: [Number, String], default: 250 },
  37. minWidth: { type: Number, default: 200 },
  38. minHeight: { type: Number, default: 150 },
  39. enableDblclickExpand: { type: Boolean, default: false },
  40. },
  41. data() {
  42. return {
  43. x: 0,
  44. y: 0,
  45. w: 0,
  46. h: 0,
  47. currentScale: 1,
  48. zIndex: globalZIndex,
  49. isDragging: false,
  50. isResizing: false,
  51. dragOffset: { x: 0, y: 0 },
  52. resizeStart: { x: 0, y: 0, w: 0, h: 0 }
  53. };
  54. },
  55. computed: {
  56. dialogStyle() {
  57. return {
  58. left: `${this.x}px`,
  59. top: `${this.y}px`,
  60. width: `${this.w}px`,
  61. height: `${this.h}px`,
  62. zIndex: this.zIndex,
  63. cursor: this.enableDblclickExpand ? 'pointer' : 'default'
  64. };
  65. }
  66. },
  67. created() {
  68. // 初始化时获取真实的缩放比例
  69. this.currentScale = this.getRealScale();
  70. this.w = this._parseSize(this.defaultWidth, window.innerWidth);
  71. this.h = this._parseSize(this.defaultHeight, window.innerHeight);
  72. },
  73. mounted() {
  74. window.addEventListener('resize', this._onWindowResize);
  75. if (this.visible) {
  76. this.bringToFront();
  77. this.calculatePosition();
  78. }
  79. },
  80. watch: {
  81. visible(newVal) {
  82. if (newVal) {
  83. this.bringToFront();
  84. this.calculatePosition();
  85. }
  86. },
  87. position: {
  88. deep: true,
  89. handler() {
  90. if (this.visible && !this.center) {
  91. this.calculatePosition();
  92. }
  93. }
  94. }
  95. },
  96. methods: {
  97. // 处理整个弹窗的双击事件
  98. handleDoubleClick(e) {
  99. // 1. 如果没开启这个功能,直接无视
  100. if (!this.enableDblclickExpand) return;
  101. // 2. 防误触:如果双击的是关闭按钮或者拉伸把手,不要触发
  102. if (e.target.classList.contains('close-btn') || e.target.classList.contains('resize-handle')) {
  103. return;
  104. }
  105. // 3. 告诉外层父组件:“我被双击了,请处理下一步”
  106. this.$emit('expand', this.id);
  107. },
  108. // 弹窗自己实时计算当前屏幕相当于设计稿的缩放比例
  109. getRealScale() {
  110. return window.innerWidth / DESIGN_WIDTH;
  111. },
  112. _parseSize(value, base) {
  113. if (typeof value === 'string' && value.endsWith('%')) {
  114. return Math.round((parseFloat(value) / 100) * base);
  115. }
  116. // 使用内部实时计算的 scale,绝不出错
  117. const scale = this.getRealScale();
  118. return Number(value) * scale;
  119. },
  120. _onWindowResize() {
  121. setTimeout(() => {
  122. // 重新获取当前最新比例
  123. const newScale = this.getRealScale();
  124. const scaleRatio = newScale / this.currentScale;
  125. // 重新按比例计算宽高
  126. this.w = this._parseSize(this.defaultWidth, window.innerWidth);
  127. this.h = this._parseSize(this.defaultHeight, window.innerHeight);
  128. if (this.visible) {
  129. // 保持相对位置等比缩放
  130. this.x = this.x * scaleRatio;
  131. this.y = this.y * scaleRatio;
  132. if (this.x + this.w > window.innerWidth) this.x = window.innerWidth - this.w;
  133. if (this.y + this.h > window.innerHeight) this.y = window.innerHeight - this.h;
  134. if (this.x < 0) this.x = 0;
  135. if (this.y < 0) this.y = 0;
  136. }
  137. this.currentScale = newScale;
  138. }, 50);
  139. },
  140. close() {
  141. this.$emit('update:visible', false);
  142. this.$emit('close');
  143. },
  144. bringToFront() {
  145. globalZIndex++;
  146. this.zIndex = globalZIndex;
  147. },
  148. calculatePosition() {
  149. this.$nextTick(() => {
  150. const winWidth = window.innerWidth;
  151. const winHeight = window.innerHeight;
  152. const scale = this.getRealScale();
  153. let targetX = 0;
  154. let targetY = 0;
  155. if (this.center) {
  156. targetX = Math.max(0, (winWidth - this.w) / 2);
  157. targetY = Math.max(0, (winHeight - this.h) / 2);
  158. } else {
  159. targetX = (this.position.x || 0) * scale;
  160. targetY = (this.position.y || 0) * scale;
  161. }
  162. const offsetStep = 20 * scale;
  163. let collision = true;
  164. let attempts = 0;
  165. const existingDialogs = document.querySelectorAll('.smart-dialog');
  166. while (collision && attempts < 15) {
  167. collision = false;
  168. for (let i = 0; i < existingDialogs.length; i++) {
  169. const el = existingDialogs[i];
  170. if (el === this.$el || el.style.display === 'none') continue;
  171. const rect = el.getBoundingClientRect();
  172. if (Math.abs(rect.left - targetX) < 2 && Math.abs(rect.top - targetY) < 2) {
  173. collision = true;
  174. break;
  175. }
  176. }
  177. if (collision) {
  178. targetX += offsetStep;
  179. targetY += offsetStep;
  180. attempts++;
  181. }
  182. }
  183. if (targetX + this.w > winWidth) targetX = winWidth - this.w - 10;
  184. if (targetY + this.h > winHeight) targetY = winHeight - this.h - 10;
  185. this.x = Math.max(0, targetX);
  186. this.y = Math.max(0, targetY);
  187. });
  188. },
  189. startDrag(e) {
  190. if (!this.draggable || e.target.classList.contains('close-btn')) return;
  191. this.isDragging = true;
  192. this.dragOffset.x = e.clientX - this.x;
  193. this.dragOffset.y = e.clientY - this.y;
  194. document.addEventListener('mousemove', this.onDrag);
  195. document.addEventListener('mouseup', this.stopDrag);
  196. },
  197. onDrag(e) {
  198. if (!this.isDragging) return;
  199. let newX = e.clientX - this.dragOffset.x;
  200. let newY = e.clientY - this.dragOffset.y;
  201. const scale = this.getRealScale();
  202. const safeHeaderHeight = 40 * scale;
  203. this.x = Math.max(0, Math.min(newX, window.innerWidth - this.w));
  204. this.y = Math.max(0, Math.min(newY, window.innerHeight - safeHeaderHeight));
  205. },
  206. stopDrag() {
  207. this.isDragging = false;
  208. document.removeEventListener('mousemove', this.onDrag);
  209. document.removeEventListener('mouseup', this.stopDrag);
  210. },
  211. startResize(e) {
  212. if (!this.resizable) return;
  213. this.isResizing = true;
  214. this.resizeStart = { x: e.clientX, y: e.clientY, w: this.w, h: this.h };
  215. document.addEventListener('mousemove', this.onResize);
  216. document.addEventListener('mouseup', this.stopResize);
  217. },
  218. onResize(e) {
  219. if (!this.isResizing) return;
  220. const deltaX = e.clientX - this.resizeStart.x;
  221. const deltaY = e.clientY - this.resizeStart.y;
  222. const scale = this.getRealScale();
  223. const currentMinWidth = this.minWidth * scale;
  224. const currentMinHeight = this.minHeight * scale;
  225. this.w = Math.max(currentMinWidth, this.resizeStart.w + deltaX);
  226. this.h = Math.max(currentMinHeight, this.resizeStart.h + deltaY);
  227. },
  228. stopResize() {
  229. this.isResizing = false;
  230. document.removeEventListener('mousemove', this.onResize);
  231. document.removeEventListener('mouseup', this.stopResize);
  232. }
  233. },
  234. beforeDestroy() {
  235. window.removeEventListener('resize', this._onWindowResize);
  236. document.removeEventListener('mousemove', this.onDrag);
  237. document.removeEventListener('mouseup', this.stopDrag);
  238. document.removeEventListener('mousemove', this.onResize);
  239. document.removeEventListener('mouseup', this.stopResize);
  240. }
  241. };
  242. </script>
  243. <style scoped>
  244. /* =========== CSS 保持不变 =========== */
  245. .smart-dialog {
  246. position: fixed;
  247. background: radial-gradient(circle at 20% 0%, rgba(40,120,200,0.2) 0%, rgba(20,60,130,0.4) 70%);
  248. box-shadow: inset 0px 0px 0.625rem 0px rgba(88, 146, 255, 0.4), inset 1.25rem 0px 1.875rem -0.625rem rgba(88, 146, 255, 0.15);
  249. border: 1px solid rgba(255, 255, 255, 0.15);
  250. border-radius: 12px;
  251. backdrop-filter: blur(8px);
  252. -webkit-backdrop-filter: blur(8px);
  253. display: flex;
  254. flex-direction: column;
  255. overflow: hidden;
  256. user-select: none;
  257. }
  258. .dialog-header {
  259. height: auto;
  260. background: transparent;
  261. display: flex;
  262. justify-content: space-between;
  263. align-items: center;
  264. padding: 10px 10px 0px 10px;
  265. }
  266. .dialog-header.not-title {
  267. padding: 0;
  268. }
  269. .dialog-header.is-draggable {
  270. cursor: move;
  271. height: 10px;
  272. }
  273. .title-content {
  274. flex: 1;
  275. min-width: 0;
  276. }
  277. .title {
  278. color: #ffffff;
  279. font-size: 14px;
  280. font-weight: 600;
  281. letter-spacing: 1px;
  282. }
  283. .close-btn {
  284. cursor: pointer;
  285. color: #ffffff;
  286. font-size: 16px;
  287. line-height: 1;
  288. font-weight: 300;
  289. opacity: 0.8;
  290. transition: all 0.2s;
  291. }
  292. .close-btn:hover {
  293. opacity: 1;
  294. transform: scale(1.1);
  295. }
  296. .dialog-divider {
  297. height: 1px;
  298. background-color: rgba(255, 255, 255, 0.3);
  299. margin: 0 20px;
  300. }
  301. .dialog-body {
  302. flex: 1;
  303. min-height: 0;
  304. padding: 20px;
  305. overflow: hidden;
  306. display: flex;
  307. flex-direction: column;
  308. }
  309. .dialog-body.no-padding {
  310. padding: 0;
  311. color: #e2e8f0;
  312. overflow: hidden;
  313. cursor: default;
  314. }
  315. .resize-handle {
  316. position: absolute;
  317. right: 0;
  318. bottom: 0;
  319. width: 16px;
  320. height: 16px;
  321. cursor: se-resize;
  322. background: linear-gradient(135deg, transparent 50%, rgba(255, 255, 255, 0.2) 50%);
  323. }
  324. </style>