前端双屏显示与通信

基础实现

  • 首先,让我们创建一个基础的多显示器窗口管理工具:
javascript 复制代码
// multi-screen-manager.js
import { 
  ElMessage, 
  ElMessageBox, 
  ElNotification,
  ElLoading
} from 'element-plus';

/**
 * 尝试在第二个显示器打开窗口
 * @param {string} url 要打开的URL
 * @param {object} options 窗口选项
 * @param {number} options.width 窗口宽度
 * @param {number} options.height 窗口高度
 * @param {string} options.windowName 窗口名称
 * @returns {Promise<object>} 窗口控制对象
 */
export const openInSecondScreen = async (url, options = {}) => {
  const loading = ElLoading.service({
    lock: true,
    text: '正在打开第二屏幕窗口...',
    background: 'rgba(0, 0, 0, 0.7)',
  });
  
  try {
    // 首先尝试使用Window Management API
    if ('getScreenDetails' in window) {
      try {
        const windowControl = await tryWithWindowManagementAPI(url, options);
        if (windowControl) {
          loading.close();
          return windowControl;
        }
      } catch (error) {
        console.warn('使用Window Management API失败:', error);
      }
    }

    // 回退方案
    console.log('使用回退方案打开窗口');
    const windowControl = fallbackMaximizedOpen(url, options);
    
    if (windowControl) {
      loading.close();
      return windowControl;
    } else {
      showPopupBlockedMessage();
      throw new Error('弹出窗口被浏览器阻止,请允许此站点的弹出窗口');
    }
  } catch (error) {
    loading.close();
    console.error('打开第二屏幕失败:', error);
    
    ElMessageBox.alert(`无法在第二屏幕打开窗口: ${error.message}`, '错误', {
      confirmButtonText: '确定',
      type: 'error',
    });
    
    throw error;
  }
};

/**
 * 使用Window Management API尝试打开第二屏幕
 */
const tryWithWindowManagementAPI = async (url, options) => {
  try {
    // 检查是否支持Permissions API
    if (!navigator.permissions || !navigator.permissions.query) {
      console.log('浏览器不支持Permissions API,使用回退方案');
      return null;
    }

    // 查询窗口管理权限状态
    const permissionStatus = await navigator.permissions.query({ 
      name: 'window-management' 
    });
    
    console.log('窗口管理权限状态:', permissionStatus.state);

    // 根据权限状态处理
    switch (permissionStatus.state) {
      case 'granted':
        // 已授权,直接获取屏幕信息
        return await handleGrantedState(url, options);
        
      case 'prompt':
        // 需要用户授权,尝试请求权限
        return await handlePromptState(url, options);
        
      case 'denied':
        // 权限被拒绝,提供用户指引
        handleDeniedState();
        return null;
        
      default:
        console.warn('未知的权限状态:', permissionStatus.state);
        return null;
    }
  } catch (error) {
    console.error('处理窗口管理权限时出错:', error);
    return null;
  }
};

/**
 * 处理已授权状态
 */
const handleGrantedState = async (url, options) => {
  try {
    const screenDetails = await window.getScreenDetails();
    
    // 检查是否有多个屏幕
    if (screenDetails.screens.length <= 1) {
      console.log('只检测到一个显示器,无法打开第二屏幕');
      showSingleScreenMessage();
      return null;
    }
    
    // 查找第二个屏幕(排除当前屏幕)
    const secondScreen = screenDetails.screens.find(
      screen => screen !== screenDetails.currentScreen
    );
    
    if (!secondScreen) {
      console.log('未找到第二个显示器');
      return null;
    }
    
    // 在第二个屏幕上打开窗口
    return openWindowOnScreen(url, secondScreen, options);
    
  } catch (error) {
    console.error('获取屏幕信息失败:', error);
    return null;
  }
};

/**
 * 处理待提示状态(需要用户授权)
 */
const handlePromptState = async (url, options) => {
  try {
    // 显示友好提示
    const userConfirmed = await showPermissionRequestDialog();
    
    if (!userConfirmed) {
      console.log('用户取消了权限请求');
      return null;
    }
    
    // 尝试获取权限(这会触发浏览器权限请求对话框)
    const screenDetails = await window.getScreenDetails();
    
    // 如果成功获取,说明用户点击了"允许"
    console.log('用户已授权窗口管理权限');
    return await handleGrantedState(url, options);
    
  } catch (error) {
    if (error.name === 'SecurityError' || error.name === 'NotAllowedError') {
      // 用户拒绝了权限请求
      console.warn('用户拒绝了窗口管理权限');
      showPermissionDeniedMessage();
    } else {
      console.error('请求权限时出错:', error);
    }
    return null;
  }
};

/**
 * 处理被拒绝状态
 */
const handleDeniedState = () => {
  console.warn('窗口管理权限已被拒绝');
  
  const guideMessage = `
    窗口管理权限已被拒绝,无法自动检测第二个显示器。
    
    解决方法:
    1. 点击浏览器地址栏左侧的锁图标或网站图标
    2. 选择"网站设置"或"权限"
    3. 找到"窗口管理"选项并设为"允许"
    4. 刷新页面后重试
    
    或者您可以选择:
    • 手动在第二个显示器上打开窗口
    • 使用系统快捷键(Win+P / Cmd+F2)管理显示器
  `;
  
  ElMessageBox.alert(guideMessage, '权限被拒绝', {
    confirmButtonText: '确定',
    dangerouslyUseHTMLString: false,
    type: 'warning',
  });
};

/**
 * 在指定屏幕上打开窗口
 */
const openWindowOnScreen = (url, screen, options = {}) => {
  const width = options.width || screen.availWidth;
  const height = options.height || screen.availHeight;
  const left = options.left || screen.availLeft;
  const top = options.top || screen.availTop;
  
  const features = `
    left=${left},
    top=${top},
    width=${width},
    height=${height},
    scrollbars=${options.scrollbars !== false ? 'yes' : 'no'},
    resizable=${options.resizable !== false ? 'yes' : 'no'},
    menubar=${options.menubar ? 'yes' : 'no'},
    toolbar=${options.toolbar ? 'yes' : 'no'},
    status=${options.status ? 'yes' : 'no'},
    location=${options.location ? 'yes' : 'no'}
  `.replace(/\s+/g, '');
  
  const windowName = options.windowName || 'secondScreenMaximized_' + Date.now();
  const newWindow = window.open(url, windowName, features);
  
  if (newWindow) {
    console.log('成功在第二个显示器上打开窗口');
    
    // 创建窗口控制对象
    const windowControl = createWindowControl(newWindow, url, windowName);
    
    // 显示成功消息
    ElMessage({
      message: '窗口已在第二屏幕打开',
      type: 'success',
      duration: 3000,
    });
    
    return windowControl;
  }
  
  return null;
};

/**
 * 回退方案:基于主显示器尺寸计算第二个显示器的位置
 */
const fallbackMaximizedOpen = (url, options = {}) => {
  console.log('使用回退方案打开窗口');
  
  const dualScreenLeft = window.screen.availLeft || window.screenLeft || 0;
  const dualScreenTop = window.screen.availTop || window.screenTop || 0;
  const width = window.screen.availWidth || window.innerWidth;

  // 假设第二个显示器在主显示器右侧(常见配置)
  const left = options.left || (dualScreenLeft + width);
  const top = options.top || dualScreenTop;
  const screenWidth = options.width || window.screen.availWidth;
  const screenHeight = options.height || window.screen.availHeight;

  const features = `
    left=${left},
    top=${top},
    width=${screenWidth},
    height=${screenHeight},
    scrollbars=yes,
    resizable=yes,
    location=yes
  `.replace(/\s+/g, '');

  const windowName = options.windowName || 'fallbackWindow_' + Date.now();
  const newWindow = window.open(url, windowName, features);

  if (newWindow) {
    console.log('全屏窗口已打开(使用回退方案,可能不在第二个显示器上)');
    
    // 检查窗口是否真的在屏幕上
    setTimeout(() => {
      if (newWindow.closed) return;
      
      try {
        const windowLeft = newWindow.screenX || newWindow.screenLeft;
        if (windowLeft > window.screen.availWidth * 1.5) {
          console.log('检测到可能的显示器配置不匹配,提供手动调整建议');
        }
      } catch (e) {
        // 跨域限制,无法访问新窗口的属性
      }
    }, 500);
    
    // 创建窗口控制对象
    const windowControl = createWindowControl(newWindow, url, windowName);
    
    // 显示提示消息
    ElMessage({
      message: '窗口已打开(使用回退方案)',
      type: 'info',
      duration: 3000,
    });
    
    return windowControl;
  }
  
  return null;
};

/**
 * 检查显示器数量
 * @returns {Promise<object>} 屏幕信息
 */
export const checkScreenCount = async () => {
  const loading = ElLoading.service({
    text: '正在检测显示器...',
    background: 'rgba(0, 0, 0, 0.5)',
  });
  
  try {
    if ('getScreenDetails' in window) {
      try {
        const permissionStatus = await navigator.permissions.query({ 
          name: 'window-management' 
        });
        
        if (permissionStatus.state === 'granted') {
          const screenDetails = await window.getScreenDetails();
          
          loading.close();
          
          return {
            count: screenDetails.screens.length,
            details: screenDetails.screens,
            currentScreen: screenDetails.currentScreen
          };
        }
      } catch (error) {
        console.error('检查屏幕数量失败:', error);
      }
    }
    
    // 回退:基于浏览器推断
    loading.close();
    
    return {
      count: 1, // 默认假设只有1个
      details: [window.screen],
      currentScreen: window.screen
    };
  } catch (error) {
    loading.close();
    console.error('检查屏幕数量失败:', error);
    
    return { 
      count: 1, 
      details: [window.screen], 
      currentScreen: window.screen 
    };
  }
};

/**
 * 手动调整窗口位置
 * @param {Window} windowRef 窗口引用
 * @param {number} offsetX X轴偏移
 * @param {number} offsetY Y轴偏移
 * @returns {boolean} 是否成功
 */
export const adjustWindowPosition = (windowRef, offsetX = 0, offsetY = 0) => {
  if (windowRef && !windowRef.closed) {
    try {
      windowRef.moveTo(
        (windowRef.screenX || windowRef.screenLeft) + offsetX,
        (windowRef.screenY || windowRef.screenTop) + offsetY
      );
      return true;
    } catch (e) {
      console.warn('无法移动窗口(可能受安全策略限制):', e);
      return false;
    }
  }
  return false;
};

/**
 * 显示多屏操作帮助信息
 */
export const showMultiScreenHelp = () => {
  const helpText = `
多显示器操作指南:

1. 自动检测(推荐):
   • 授予窗口管理权限
   • 系统自动识别第二显示器
   • 一键打开全屏窗口

2. 手动操作:
   • 使用 Win+P(Windows)或 Cmd+F2(Mac)切换显示器模式
   • 将窗口拖动到屏幕边缘
   • 使用系统快捷键管理窗口

3. 常见问题:
   • 看不到第二个显示器?检查连接线和显示器电源
   • 权限被拒绝?在浏览器设置中重新授权
   • 窗口位置不对?尝试手动拖动调整

4. 系统支持:
   • Windows 10/11:支持良好
   • macOS:需要系统权限
   • Linux:因发行版而异
  `;
  
  ElMessageBox.alert(helpText, '多显示器操作指南', {
    confirmButtonText: '确定',
    type: 'info',
  });
};

/**
 * 显示权限请求对话框
 */
const showPermissionRequestDialog = () => {
  return new Promise((resolve) => {
    const message = `
      为了在第二个显示器上打开窗口,需要访问您的显示器信息。
      
      点击"确定"后,浏览器会弹出权限请求对话框,请选择"允许"。
      
      这可以让我们:
      • 检测您的多显示器配置
      • 在正确的显示器上打开窗口
      • 提供更好的多屏体验
    `;
    
    ElMessageBox.confirm(message, '请求权限', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'info',
    }).then(() => {
      resolve(true);
    }).catch(() => {
      resolve(false);
    });
  });
};

/**
 * 显示单屏幕提示信息
 */
const showSingleScreenMessage = () => {
  const message = '当前只检测到一个显示器。请连接第二个显示器后重试。';
  
  ElMessageBox.alert(message, '单显示器', {
    confirmButtonText: '确定',
    type: 'warning',
  });
};

/**
 * 显示权限被拒绝信息
 */
const showPermissionDeniedMessage = () => {
  const message = '您拒绝了窗口管理权限,将使用常规方式打开窗口。';
  
  ElMessage({
    message: message,
    type: 'info',
    duration: 3000,
  });
};

/**
 * 显示弹出窗口被阻止信息
 */
const showPopupBlockedMessage = () => {
  const message = '弹出窗口被浏览器阻止。请允许此站点的弹出窗口,然后重试。';
  
  ElMessageBox.alert(message, '弹出窗口被阻止', {
    confirmButtonText: '确定',
    type: 'error',
  });
};

/**
 * 创建窗口控制对象
 */
const createWindowControl = (newWindow, url, windowName) => {
  // 添加窗口关闭监听
  const checkWindowClosed = setInterval(() => {
    if (newWindow.closed) {
      clearInterval(checkWindowClosed);
      console.log('第二屏幕窗口已关闭');
    }
  }, 1000);
  
  return {
    window: newWindow,
    id: windowName,
    url: url,
    move: (x, y) => adjustWindowPosition(newWindow, x, y),
    close: () => {
      if (newWindow && !newWindow.closed) {
        newWindow.close();
      }
    },
    refresh: () => {
      if (newWindow && !newWindow.closed) {
        newWindow.location.reload();
      }
    },
    focus: () => {
      if (newWindow && !newWindow.closed) {
        newWindow.focus();
        return true;
      }
      return false;
    },
    resize: (width, height) => {
      if (newWindow && !newWindow.closed) {
        try {
          newWindow.resizeTo(width, height);
          return true;
        } catch (e) {
          console.warn('调整窗口大小失败:', e);
          return false;
        }
      }
      return false;
    },
    getState: () => {
      if (newWindow && !newWindow.closed) {
        try {
          return {
            closed: newWindow.closed,
            screenX: newWindow.screenX,
            screenY: newWindow.screenY,
            innerWidth: newWindow.innerWidth,
            innerHeight: newWindow.innerHeight,
            outerWidth: newWindow.outerWidth,
            outerHeight: newWindow.outerHeight
          };
        } catch (e) {
          return null;
        }
      }
      return null;
    },
    onClose: (callback) => {
      const checkClose = setInterval(() => {
        if (newWindow.closed) {
          clearInterval(checkClose);
          callback();
        }
      }, 500);
    }
  };
};
  • 使用示例
javascript 复制代码
import { 
  openInSecondScreen, 
  checkScreenCount, 
  showMultiScreenHelp 
} from './multi-screen-manager.js';

// 1. 检查显示器
const screenInfo = await checkScreenCount();
console.log('检测到', screenInfo.count, '个显示器');

// 2. 显示帮助
showMultiScreenHelp();

// 3. 在第二屏幕打开窗口
const windowControl = await openInSecondScreen('https://example.com', {
  width: 1920,
  height: 1080,
  windowName: 'presentation'
});

// 4. 控制打开的窗口
windowControl.focus();
windowControl.move(100, 100); // 移动窗口
windowControl.resize(800, 600); // 调整大小
windowControl.onClose(() => {
  console.log('窗口已关闭');
});

双屏通讯实现

javascript 复制代码
// 主窗口代码
const channel = new BroadcastChannel('screen_sync')
// 发送消息到第二屏幕
channel.postMessage({ order: 'changeStage'})

//第二屏幕代码
const channel = new BroadcastChannel('screen_sync')
//接收主窗口的消息
channel.onmessage = (event) => {
   console.log('收到来自主窗口的消息:', event.data);
   // 处理来自第二屏幕的消息
};
相关推荐
IT_陈寒2 小时前
JavaScript性能优化:7个被低估的V8引擎技巧让你的代码提速50%
前端·人工智能·后端
PieroPc2 小时前
Html +css+js 写的一个小商城系统(POS系统)
javascript·css·html
richxu202510012 小时前
Java是当今最优雅的开发语言
java·开发语言
2501_918126912 小时前
用Python开发一个三进制程序开发工具
开发语言·汇编·python·个人开发
顾安r2 小时前
1.1 脚本网页 战推棋
java·前端·游戏·html·virtualenv
一颗小青松2 小时前
vue 腾讯地图经纬度转高德地图经纬度
前端·javascript·vue.js
zh_xuan2 小时前
kotlin的常见空检查
android·开发语言·kotlin
Justin3go10 小时前
HUNT0 上线了——尽早发布,尽早发现
前端·后端·程序员