| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- <template>
- <div v-show="visible" class="smart-dialog" :style="dialogStyle" @mousedown="bringToFront" @dblclick="handleDoubleClick">
- <div class="dialog-header" :class="{ 'is-draggable': draggable }" @mousedown="startDrag" v-if="title">
- <div class="title-content">
- <slot name="header">
- <span class="title">{{ title }}</span>
- </slot>
- </div>
- <span v-if="showClose" class="close-btn" @click.stop="close">✕</span>
- </div>
- <div v-else class="dialog-header not-title" :class="{ 'is-draggable': draggable }" @mousedown="startDrag"></div>
- <div v-if="title && false" class="dialog-divider"></div>
- <div class="dialog-body" :class="{ 'no-padding': noPadding }">
- <slot></slot>
- </div>
- <div v-if="resizable" class="resize-handle" @mousedown.prevent="startResize"></div>
- </div>
- </template>
- <script>
- let globalZIndex = 2000;
- // 在这里定义你的大屏设计稿基准宽度 (通常是 1920)
- const DESIGN_WIDTH = 1920;
- export default {
- name: 'SmartDialog',
- props: {
- id: { type: [String, Number], required: true },
- visible: { type: Boolean, default: false },
- title: { type: String, default: '提示' },
-
- center: { type: Boolean, default: true },
- position: { type: Object, default: () => ({ x: 0, y: 0 }) },
-
- showClose: { type: Boolean, default: true },
- draggable: { type: Boolean, default: true },
- resizable: { type: Boolean, default: true },
- noPadding: { type: Boolean, default: false },
- defaultWidth: { type: [Number, String], default: 350 },
- defaultHeight: { type: [Number, String], default: 250 },
- minWidth: { type: Number, default: 200 },
- minHeight: { type: Number, default: 150 },
- enableDblclickExpand: { type: Boolean, default: false },
- },
- data() {
- return {
- x: 0,
- y: 0,
- w: 0,
- h: 0,
- currentScale: 1,
- zIndex: globalZIndex,
- isDragging: false,
- isResizing: false,
- dragOffset: { x: 0, y: 0 },
- resizeStart: { x: 0, y: 0, w: 0, h: 0 }
- };
- },
- computed: {
- dialogStyle() {
- return {
- left: `${this.x}px`,
- top: `${this.y}px`,
- width: `${this.w}px`,
- height: `${this.h}px`,
- zIndex: this.zIndex,
- cursor: this.enableDblclickExpand ? 'pointer' : 'default'
- };
- }
- },
- created() {
- // 初始化时获取真实的缩放比例
- this.currentScale = this.getRealScale();
- this.w = this._parseSize(this.defaultWidth, window.innerWidth);
- this.h = this._parseSize(this.defaultHeight, window.innerHeight);
- },
- mounted() {
- window.addEventListener('resize', this._onWindowResize);
- if (this.visible) {
- this.bringToFront();
- this.calculatePosition();
- }
- },
- watch: {
- visible(newVal) {
- if (newVal) {
- this.bringToFront();
- this.calculatePosition();
- }
- },
- position: {
- deep: true,
- handler() {
- if (this.visible && !this.center) {
- this.calculatePosition();
- }
- }
- }
- },
- methods: {
- // 处理整个弹窗的双击事件
- handleDoubleClick(e) {
- // 1. 如果没开启这个功能,直接无视
- if (!this.enableDblclickExpand) return;
-
- // 2. 防误触:如果双击的是关闭按钮或者拉伸把手,不要触发
- if (e.target.classList.contains('close-btn') || e.target.classList.contains('resize-handle')) {
- return;
- }
- // 3. 告诉外层父组件:“我被双击了,请处理下一步”
- this.$emit('expand', this.id);
- },
- // 弹窗自己实时计算当前屏幕相当于设计稿的缩放比例
- getRealScale() {
- return window.innerWidth / DESIGN_WIDTH;
- },
- _parseSize(value, base) {
- if (typeof value === 'string' && value.endsWith('%')) {
- return Math.round((parseFloat(value) / 100) * base);
- }
- // 使用内部实时计算的 scale,绝不出错
- const scale = this.getRealScale();
- return Number(value) * scale;
- },
-
- _onWindowResize() {
- setTimeout(() => {
- // 重新获取当前最新比例
- const newScale = this.getRealScale();
- const scaleRatio = newScale / this.currentScale;
-
- // 重新按比例计算宽高
- this.w = this._parseSize(this.defaultWidth, window.innerWidth);
- this.h = this._parseSize(this.defaultHeight, window.innerHeight);
- if (this.visible) {
- // 保持相对位置等比缩放
- this.x = this.x * scaleRatio;
- this.y = this.y * scaleRatio;
- if (this.x + this.w > window.innerWidth) this.x = window.innerWidth - this.w;
- if (this.y + this.h > window.innerHeight) this.y = window.innerHeight - this.h;
- if (this.x < 0) this.x = 0;
- if (this.y < 0) this.y = 0;
- }
-
- this.currentScale = newScale;
- }, 50);
- },
-
- close() {
- this.$emit('update:visible', false);
- this.$emit('close');
- },
-
- bringToFront() {
- globalZIndex++;
- this.zIndex = globalZIndex;
- },
-
- calculatePosition() {
- this.$nextTick(() => {
- const winWidth = window.innerWidth;
- const winHeight = window.innerHeight;
- const scale = this.getRealScale();
-
- let targetX = 0;
- let targetY = 0;
- if (this.center) {
- targetX = Math.max(0, (winWidth - this.w) / 2);
- targetY = Math.max(0, (winHeight - this.h) / 2);
- } else {
- targetX = (this.position.x || 0) * scale;
- targetY = (this.position.y || 0) * scale;
- }
- const offsetStep = 20 * scale;
- let collision = true;
- let attempts = 0;
- const existingDialogs = document.querySelectorAll('.smart-dialog');
- while (collision && attempts < 15) {
- collision = false;
- for (let i = 0; i < existingDialogs.length; i++) {
- const el = existingDialogs[i];
- if (el === this.$el || el.style.display === 'none') continue;
- const rect = el.getBoundingClientRect();
- if (Math.abs(rect.left - targetX) < 2 && Math.abs(rect.top - targetY) < 2) {
- collision = true;
- break;
- }
- }
- if (collision) {
- targetX += offsetStep;
- targetY += offsetStep;
- attempts++;
- }
- }
- if (targetX + this.w > winWidth) targetX = winWidth - this.w - 10;
- if (targetY + this.h > winHeight) targetY = winHeight - this.h - 10;
- this.x = Math.max(0, targetX);
- this.y = Math.max(0, targetY);
- });
- },
- startDrag(e) {
- if (!this.draggable || e.target.classList.contains('close-btn')) return;
- this.isDragging = true;
- this.dragOffset.x = e.clientX - this.x;
- this.dragOffset.y = e.clientY - this.y;
- document.addEventListener('mousemove', this.onDrag);
- document.addEventListener('mouseup', this.stopDrag);
- },
- onDrag(e) {
- if (!this.isDragging) return;
- let newX = e.clientX - this.dragOffset.x;
- let newY = e.clientY - this.dragOffset.y;
-
- const scale = this.getRealScale();
- const safeHeaderHeight = 40 * scale;
-
- this.x = Math.max(0, Math.min(newX, window.innerWidth - this.w));
- this.y = Math.max(0, Math.min(newY, window.innerHeight - safeHeaderHeight));
- },
- stopDrag() {
- this.isDragging = false;
- document.removeEventListener('mousemove', this.onDrag);
- document.removeEventListener('mouseup', this.stopDrag);
- },
- startResize(e) {
- if (!this.resizable) return;
- this.isResizing = true;
- this.resizeStart = { x: e.clientX, y: e.clientY, w: this.w, h: this.h };
- document.addEventListener('mousemove', this.onResize);
- document.addEventListener('mouseup', this.stopResize);
- },
- onResize(e) {
- if (!this.isResizing) return;
- const deltaX = e.clientX - this.resizeStart.x;
- const deltaY = e.clientY - this.resizeStart.y;
-
- const scale = this.getRealScale();
- const currentMinWidth = this.minWidth * scale;
- const currentMinHeight = this.minHeight * scale;
-
- this.w = Math.max(currentMinWidth, this.resizeStart.w + deltaX);
- this.h = Math.max(currentMinHeight, this.resizeStart.h + deltaY);
- },
- stopResize() {
- this.isResizing = false;
- document.removeEventListener('mousemove', this.onResize);
- document.removeEventListener('mouseup', this.stopResize);
- }
- },
- beforeDestroy() {
- window.removeEventListener('resize', this._onWindowResize);
- document.removeEventListener('mousemove', this.onDrag);
- document.removeEventListener('mouseup', this.stopDrag);
- document.removeEventListener('mousemove', this.onResize);
- document.removeEventListener('mouseup', this.stopResize);
- }
- };
- </script>
- <style scoped>
- /* =========== CSS 保持不变 =========== */
- .smart-dialog {
- position: fixed;
- background: radial-gradient(circle at 20% 0%, rgba(40,120,200,0.2) 0%, rgba(20,60,130,0.4) 70%);
- 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);
- border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 12px;
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- user-select: none;
- }
- .dialog-header {
- height: auto;
- background: transparent;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 10px 0px 10px;
- }
- .dialog-header.not-title {
- padding: 0;
- }
- .dialog-header.is-draggable {
- cursor: move;
- height: 10px;
- }
- .title-content {
- flex: 1;
- min-width: 0;
- }
- .title {
- color: #ffffff;
- font-size: 14px;
- font-weight: 600;
- letter-spacing: 1px;
- }
- .close-btn {
- cursor: pointer;
- color: #ffffff;
- font-size: 16px;
- line-height: 1;
- font-weight: 300;
- opacity: 0.8;
- transition: all 0.2s;
- }
- .close-btn:hover {
- opacity: 1;
- transform: scale(1.1);
- }
- .dialog-divider {
- height: 1px;
- background-color: rgba(255, 255, 255, 0.3);
- margin: 0 20px;
- }
- .dialog-body {
- flex: 1;
- min-height: 0;
- padding: 20px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
- .dialog-body.no-padding {
- padding: 0;
- color: #e2e8f0;
- overflow: hidden;
- cursor: default;
- }
- .resize-handle {
- position: absolute;
- right: 0;
- bottom: 0;
- width: 16px;
- height: 16px;
- cursor: se-resize;
- background: linear-gradient(135deg, transparent 50%, rgba(255, 255, 255, 0.2) 50%);
- }
- </style>
|