Aucune description
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

window.service.ts 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { Injectable } from '@angular/core';
  2. /**
  3. * 浏览器窗口管理服务
  4. * 封装 window.open 操作,提供项目标签页管理功能
  5. *
  6. * 功能:
  7. * 1. 生成正确的绝对 URL
  8. * 2. 管理标签页状态(localStorage 存储)
  9. * 3. 实现窗口复用(已打开则聚焦)
  10. * 4. 标签页数量限制(最多7个,自动关闭最旧)
  11. * 5. 错误处理和诊断日志
  12. */
  13. @Injectable({
  14. providedIn: 'root'
  15. })
  16. export class WindowService {
  17. private readonly MAX_TABS = 7;
  18. private readonly TAB_STORAGE_KEY = 'browser_tabs';
  19. private windowReferences = new Map<string, Window>();
  20. /**
  21. * 打开项目标签页
  22. * @param projectId 项目ID
  23. * @returns 是否成功打开或聚焦窗口
  24. */
  25. openProjectTab(projectId: string): boolean {
  26. console.log('🔍 [WindowService] 打开项目标签页:', projectId);
  27. // 生成窗口名称和URL
  28. const windowName = this.generateWindowName(projectId);
  29. const url = this.generateProjectUrl(projectId);
  30. console.log('🔍 [WindowService] 参数:', { windowName, url });
  31. // 检查内存中是否已跟踪此窗口
  32. const existingWindow = this.windowReferences.get(windowName);
  33. if (existingWindow && !existingWindow.closed) {
  34. console.log('🔍 [WindowService] 窗口已在内存中跟踪,聚焦:', windowName);
  35. existingWindow.focus();
  36. return true;
  37. }
  38. // 清理无效的窗口引用
  39. this.cleanupWindowReferences();
  40. // 检查标签页数量限制
  41. const openTabCount = this.windowReferences.size;
  42. console.log('🔍 [WindowService] 当前跟踪的标签页数量:', openTabCount, '限制:', this.MAX_TABS);
  43. if (openTabCount >= this.MAX_TABS) {
  44. console.error('🔍 [WindowService] 标签页数量超限');
  45. this.showError('已达到最大标签页数量(7个),请手动关闭一个标签页后再试');
  46. return false;
  47. }
  48. // 使用 _blank 目标打开新窗口
  49. console.log('🔍 [WindowService] 使用 _blank 目标打开新窗口');
  50. const newWindow = window.open(url, '_blank');
  51. console.log('🔍 [WindowService] window.open 结果:', !!newWindow);
  52. if (!newWindow) {
  53. console.error('🔍 [WindowService] 无法打开新标签页');
  54. this.showError('无法打开新标签页,可能被浏览器阻止,请检查浏览器弹出窗口设置');
  55. return false;
  56. }
  57. // 在内存中跟踪此窗口
  58. this.windowReferences.set(windowName, newWindow);
  59. // 设置关闭监听器
  60. const checkClosed = () => {
  61. if (newWindow.closed) {
  62. console.log('🔍 [WindowService] 检测到窗口已关闭:', windowName);
  63. this.windowReferences.delete(windowName);
  64. } else {
  65. // 继续检查
  66. setTimeout(checkClosed, 1000);
  67. }
  68. };
  69. setTimeout(checkClosed, 1000);
  70. console.log('🔍 [WindowService] 窗口已打开并跟踪');
  71. return true;
  72. }
  73. /**
  74. * 生成窗口名称
  75. */
  76. private generateWindowName(projectId: string): string {
  77. return `project-${projectId}`;
  78. }
  79. /**
  80. * 生成项目URL(绝对路径)
  81. */
  82. private generateProjectUrl(projectId: string): string {
  83. // 生成绝对URL确保在新标签页中可靠加载
  84. const origin = window.location.origin;
  85. const url = `${origin}/project/${projectId}`;
  86. console.log('🔍 [WindowService] 生成的URL:', { origin, url });
  87. return url;
  88. }
  89. /**
  90. * 清理无效的窗口引用
  91. */
  92. private cleanupWindowReferences(): void {
  93. for (const [name, win] of this.windowReferences.entries()) {
  94. if (win.closed) {
  95. console.log('🔍 [WindowService] 清理已关闭的窗口引用:', name);
  96. this.windowReferences.delete(name);
  97. }
  98. }
  99. }
  100. /**
  101. * 根据名称获取已存在的窗口(简化版,不依赖window.open('', name))
  102. */
  103. private getWindowByName(name: string): Window | null {
  104. // 只从内存引用中获取
  105. const win = this.windowReferences.get(name);
  106. if (win && !win.closed) {
  107. return win;
  108. }
  109. return null;
  110. }
  111. /**
  112. * 获取标签页数据
  113. */
  114. private getTabData(): Record<string, number> {
  115. try {
  116. const tabsJson = localStorage.getItem(this.TAB_STORAGE_KEY);
  117. return tabsJson ? JSON.parse(tabsJson) : {};
  118. } catch (e) {
  119. console.error('🔍 [WindowService] 解析标签页数据失败:', e);
  120. return {};
  121. }
  122. }
  123. /**
  124. * 保存标签页数据
  125. */
  126. private saveTabData(tabData: Record<string, number>): void {
  127. try {
  128. localStorage.setItem(this.TAB_STORAGE_KEY, JSON.stringify(tabData));
  129. } catch (e) {
  130. console.error('🔍 [WindowService] 保存标签页数据失败:', e);
  131. }
  132. }
  133. /**
  134. * 获取当前应用的基URL(用于诊断)
  135. */
  136. getBaseUrl(): string {
  137. return window.location.origin;
  138. }
  139. /**
  140. * 显示错误提示给用户
  141. */
  142. private showError(message: string): void {
  143. console.error('🔍 [WindowService] 错误:', message);
  144. // 使用alert简单提示用户,实际项目中可替换为toast等UI组件
  145. alert(`打开标签页失败: ${message}`);
  146. }
  147. /**
  148. * 诊断方法:输出当前窗口状态信息
  149. */
  150. diagnose(): {
  151. baseUrl: string;
  152. openTabCount: number;
  153. maxTabs: number;
  154. trackedWindows: string[];
  155. windowLocation: string;
  156. } {
  157. return {
  158. baseUrl: this.getBaseUrl(),
  159. openTabCount: this.windowReferences.size,
  160. maxTabs: this.MAX_TABS,
  161. trackedWindows: Array.from(this.windowReferences.keys()),
  162. windowLocation: window.location.href
  163. };
  164. }
  165. }