|
|
@@ -1,357 +0,0 @@
|
|
|
-<template>
|
|
|
- <div
|
|
|
- v-show="visible"
|
|
|
- class="smart-dialog"
|
|
|
- :style="dialogStyle"
|
|
|
- @mousedown="bringToFront"
|
|
|
- >
|
|
|
- <div
|
|
|
- class="dialog-header"
|
|
|
- :class="{ 'is-draggable': draggable }"
|
|
|
- @mousedown="startDrag"
|
|
|
- >
|
|
|
- <div class="title-content">
|
|
|
- <slot name="header">
|
|
|
- <span class="title">{{ title }}</span>
|
|
|
- </slot>
|
|
|
- </div>
|
|
|
- <span class="close-btn" @click.stop="close">✕</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div 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>
|
|
|
-// 全局 z-index 管理,保证多开时点击的窗口总在最上层
|
|
|
-let globalZIndex = 2000;
|
|
|
-
|
|
|
-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 }, // true: 屏幕居中; false: 跟随 position
|
|
|
- position: { type: Object, default: () => ({ x: 0, y: 0 }) },
|
|
|
-
|
|
|
- // 功能开关
|
|
|
- draggable: { type: Boolean, default: true }, // 是否允许拖拽
|
|
|
- resizable: { type: Boolean, default: true }, // 是否允许拉伸大小
|
|
|
- noPadding: { type: Boolean, default: false }, // 内容区无内边距
|
|
|
-
|
|
|
- // 尺寸配置(支持百分比字符串如 '78%' 或数字像素值如 350)
|
|
|
- defaultWidth: { type: [Number, String], default: 350 },
|
|
|
- defaultHeight: { type: [Number, String], default: 250 },
|
|
|
- minWidth: { type: Number, default: 200 },
|
|
|
- minHeight: { type: Number, default: 150 }
|
|
|
- },
|
|
|
- data() {
|
|
|
- return {
|
|
|
- x: 0,
|
|
|
- y: 0,
|
|
|
- w: this._parseSize(this.defaultWidth, window.innerWidth),
|
|
|
- h: this._parseSize(this.defaultHeight, window.innerHeight),
|
|
|
- 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
|
|
|
- };
|
|
|
- }
|
|
|
- },
|
|
|
- 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: {
|
|
|
- _parseSize(value, base) {
|
|
|
- if (typeof value === 'string' && value.endsWith('%')) {
|
|
|
- return Math.round((parseFloat(value) / 100) * base);
|
|
|
- }
|
|
|
- return Number(value);
|
|
|
- },
|
|
|
- _onWindowResize() {
|
|
|
- // 只对百分比尺寸的弹窗重新计算
|
|
|
- const isPercentW = typeof this.defaultWidth === 'string' && this.defaultWidth.endsWith('%');
|
|
|
- const isPercentH = typeof this.defaultHeight === 'string' && this.defaultHeight.endsWith('%');
|
|
|
- if (isPercentW) this.w = this._parseSize(this.defaultWidth, window.innerWidth);
|
|
|
- if (isPercentH) this.h = this._parseSize(this.defaultHeight, window.innerHeight);
|
|
|
- if ((isPercentW || isPercentH) && this.visible) {
|
|
|
- this.calculatePosition();
|
|
|
- }
|
|
|
- },
|
|
|
- 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;
|
|
|
-
|
|
|
- // 1. 先计算出理论上的“理想位置”
|
|
|
- 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;
|
|
|
- targetY = this.position.y || 0;
|
|
|
- }
|
|
|
-
|
|
|
- // ==========================================
|
|
|
- // 2. 【新增逻辑】碰撞检测与智能偏移 (级联效果)
|
|
|
- // ==========================================
|
|
|
- const offsetStep = 20; // 每次偏移 20 像素
|
|
|
- let collision = true;
|
|
|
- let attempts = 0; // 防止极端情况下的死循环
|
|
|
-
|
|
|
- // 获取页面上当前所有的弹窗 DOM 元素
|
|
|
- const existingDialogs = document.querySelectorAll('.smart-dialog');
|
|
|
-
|
|
|
- // 只要发现有重叠,就一直偏移,最多尝试 15 次
|
|
|
- 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();
|
|
|
-
|
|
|
- // 坐标重合检测 (加入 2px 的容差,防止浮点数计算误差)
|
|
|
- const isSameX = Math.abs(rect.left - targetX) < 2;
|
|
|
- const isSameY = Math.abs(rect.top - targetY) < 2;
|
|
|
-
|
|
|
- if (isSameX && isSameY) {
|
|
|
- collision = true;
|
|
|
- break; // 发现重叠,跳出 for 循环,准备进行偏移
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 如果发生重叠,向右下方偏移
|
|
|
- if (collision) {
|
|
|
- targetX += offsetStep;
|
|
|
- targetY += offsetStep;
|
|
|
- attempts++;
|
|
|
- }
|
|
|
- }
|
|
|
- // ==========================================
|
|
|
-
|
|
|
- // 3. 最终的边界安全检测 (防止偏移后右侧或底部超出屏幕)
|
|
|
- if (targetX + this.w > winWidth) targetX = winWidth - this.w - 10;
|
|
|
- if (targetY + this.h > winHeight) targetY = winHeight - this.h - 10;
|
|
|
-
|
|
|
- // 4. 赋值生效
|
|
|
- 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;
|
|
|
-
|
|
|
- this.x = Math.max(0, Math.min(newX, window.innerWidth - this.w));
|
|
|
- this.y = Math.max(0, Math.min(newY, window.innerHeight - 40)); // 40是头部高度
|
|
|
- },
|
|
|
- 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;
|
|
|
-
|
|
|
- this.w = Math.max(this.minWidth, this.resizeStart.w + deltaX);
|
|
|
- this.h = Math.max(this.minHeight, 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>
|
|
|
-/* 1. 弹窗主容器:毛玻璃与发光背景 */
|
|
|
-.smart-dialog {
|
|
|
- position: fixed;
|
|
|
- /* 使用半透明深蓝色,配合一点点径向渐变模拟左侧高光 */
|
|
|
- background: radial-gradient(circle at 20% 0%, rgba(40, 120, 200, 0.4) 0%, rgba(20, 60, 130, 0.8) 70%);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.15); /* 极细的半透明白边 */
|
|
|
- border-radius: 12px; /* 更大的圆角,贴合图片 */
|
|
|
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); /* 更柔和深邃的阴影 */
|
|
|
- backdrop-filter: blur(12px); /* 核心:毛玻璃模糊效果 */
|
|
|
- -webkit-backdrop-filter: blur(12px); /* Safari 兼容 */
|
|
|
-
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
- user-select: none;
|
|
|
-}
|
|
|
-
|
|
|
-/* 2. 头部区域:去除背景,调整内边距 */
|
|
|
-.dialog-header {
|
|
|
- height: auto;
|
|
|
- background: transparent; /* 去除原本的渐变色,融入整体背景 */
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- padding: 16px 20px 12px 20px; /* 上右下左留白 */
|
|
|
-}
|
|
|
-
|
|
|
-.dialog-header.is-draggable {
|
|
|
- cursor: move;
|
|
|
-}
|
|
|
-
|
|
|
-/* 标题容器:自适应填充 */
|
|
|
-.title-content {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* 标题样式:纯白、加粗 */
|
|
|
-.title {
|
|
|
- color: #ffffff;
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- letter-spacing: 1px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 关闭按钮:干净细致的 X */
|
|
|
-.close-btn {
|
|
|
- cursor: pointer;
|
|
|
- color: #ffffff;
|
|
|
- font-size: 18px;
|
|
|
- line-height: 1;
|
|
|
- font-weight: 300;
|
|
|
- opacity: 0.8;
|
|
|
- transition: all 0.2s;
|
|
|
-}
|
|
|
-.close-btn:hover {
|
|
|
- opacity: 1;
|
|
|
- transform: scale(1.1);
|
|
|
-}
|
|
|
-
|
|
|
-/* 3. 【核心】独立的分割线 */
|
|
|
-.dialog-divider {
|
|
|
- height: 1px;
|
|
|
- background-color: rgba(255, 255, 255, 0.3); /* 半透明白线 */
|
|
|
- margin: 0 20px; /* 左右留白,不撑满屏幕 */
|
|
|
-}
|
|
|
-
|
|
|
-/* 4. 内容区与拉伸把手 */
|
|
|
-.dialog-body {
|
|
|
- flex: 1;
|
|
|
- min-height: 0;
|
|
|
- padding: 20px;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.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>
|