tabs.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import { defineStore } from 'pinia';
  2. import { ref, computed, markRaw, type Component } from 'vue';
  3. // 标签页类型
  4. export type TabType = 'page' | 'browser';
  5. // 标签页接口
  6. export interface Tab {
  7. id: string;
  8. title: string;
  9. type: TabType;
  10. // 页面类型标签页的路由路径
  11. path?: string;
  12. // 页面组件(用于缓存)
  13. component?: Component;
  14. // 浏览器类型标签页的数据
  15. browserData?: {
  16. platform: string;
  17. sessionId?: string;
  18. url?: string;
  19. cookieData?: string; // 预设的 Cookie 数据(JSON 格式)
  20. status?: 'loading' | 'ready' | 'login_pending' | 'login_success' | 'login_failed';
  21. groupId?: number;
  22. isAdminMode?: boolean; // 是否是管理后台模式(从后台按钮打开,不需要登录检测和保存账号)
  23. };
  24. // 是否可关闭
  25. closable?: boolean;
  26. // 图标
  27. icon?: string;
  28. }
  29. // 页面路由配置
  30. export const PAGE_CONFIG: Record<string, { title: string; icon: string }> = {
  31. '/': { title: '数据看板', icon: 'DataAnalysis' },
  32. '/accounts': { title: '账号管理', icon: 'User' },
  33. '/works': { title: '作品管理', icon: 'Film' },
  34. '/publish': { title: '发布管理', icon: 'Upload' },
  35. '/comments': { title: '评论管理', icon: 'ChatDotRound' },
  36. '/schedule': { title: '定时任务', icon: 'Clock' },
  37. '/analytics/overview': { title: '数据总览', icon: 'TrendCharts' },
  38. '/analytics/platform': { title: '平台数据', icon: 'TrendCharts' },
  39. '/analytics/account': { title: '账号数据', icon: 'TrendCharts' },
  40. '/analytics/work': { title: '作品数据', icon: 'TrendCharts' },
  41. '/settings': { title: '系统设置', icon: 'Setting' },
  42. '/profile': { title: '个人中心', icon: 'User' },
  43. };
  44. export const useTabsStore = defineStore('tabs', () => {
  45. // 所有标签页
  46. const tabs = ref<Tab[]>([]);
  47. // 当前激活的标签页ID
  48. const activeTabId = ref<string>('');
  49. // 账号列表刷新计数器(当计数器变化时,Accounts 页面会刷新)
  50. const accountRefreshTrigger = ref(0);
  51. // 标签页刷新计数器(用于触发标签页刷新)
  52. const tabRefreshTrigger = ref<Record<string, number>>({});
  53. // 当前激活的标签页
  54. const activeTab = computed(() =>
  55. tabs.value.find(tab => tab.id === activeTabId.value)
  56. );
  57. // 浏览器类型的标签页
  58. const browserTabs = computed(() =>
  59. tabs.value.filter(tab => tab.type === 'browser')
  60. );
  61. // 页面类型的标签页
  62. const pageTabs = computed(() =>
  63. tabs.value.filter(tab => tab.type === 'page')
  64. );
  65. // 生成唯一ID
  66. function generateId(): string {
  67. return `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  68. }
  69. // 添加标签页
  70. function addTab(tab: Omit<Tab, 'id'> & { id?: string }): Tab {
  71. const newTab: Tab = {
  72. ...tab,
  73. id: tab.id || generateId(),
  74. closable: tab.closable ?? true,
  75. };
  76. // 检查是否已存在相同路径的页面标签
  77. if (tab.type === 'page' && tab.path) {
  78. const existingTab = tabs.value.find(t => t.type === 'page' && t.path === tab.path);
  79. if (existingTab) {
  80. activeTabId.value = existingTab.id;
  81. return existingTab;
  82. }
  83. }
  84. tabs.value.push(newTab);
  85. activeTabId.value = newTab.id;
  86. return newTab;
  87. }
  88. // 打开页面标签页
  89. function openPageTab(path: string, component?: Component): Tab {
  90. // 检查是否已存在(支持动态路由匹配)
  91. const existingTab = tabs.value.find(t => {
  92. if (t.type === 'page' && t.path) {
  93. // 精确匹配
  94. if (t.path === path) return true;
  95. // 动态路由匹配:/analytics/platform-detail/:platform
  96. if (path.startsWith('/analytics/platform-detail/') &&
  97. t.path?.startsWith('/analytics/platform-detail/')) {
  98. return true;
  99. }
  100. }
  101. return false;
  102. });
  103. if (existingTab) {
  104. // 如果路径不同,更新路径(用于动态路由)
  105. if (existingTab.path !== path) {
  106. existingTab.path = path;
  107. }
  108. activeTabId.value = existingTab.id;
  109. return existingTab;
  110. }
  111. // 获取页面配置(支持动态路由)
  112. let config = PAGE_CONFIG[path];
  113. if (!config && path.startsWith('/analytics/platform-detail/')) {
  114. // 从路径中提取平台名称
  115. const platform = path.split('/').pop() || '';
  116. const platformNames: Record<string, string> = {
  117. 'douyin': '抖音',
  118. 'xiaohongshu': '小红书',
  119. 'baijiahao': '百家号',
  120. 'weixin_video': '视频号',
  121. };
  122. config = {
  123. title: `${platformNames[platform] || platform}平台数据详情`,
  124. icon: 'TrendCharts'
  125. };
  126. }
  127. const finalConfig = config || { title: '未知页面', icon: 'Document' };
  128. const tab = addTab({
  129. title: finalConfig.title,
  130. type: 'page',
  131. path,
  132. component: component ? markRaw(component) : undefined,
  133. icon: finalConfig.icon,
  134. // 首页不可关闭
  135. closable: path !== '/',
  136. });
  137. return tab;
  138. }
  139. // 打开浏览器标签页
  140. function openBrowserTab(platform: string, title?: string, groupId?: number, url?: string, cookieData?: string, isAdminMode?: boolean): Tab {
  141. const tab = addTab({
  142. title: title || `${platform} 浏览器`,
  143. type: 'browser',
  144. browserData: {
  145. platform,
  146. status: 'loading',
  147. groupId,
  148. url, // 可选的初始 URL
  149. cookieData, // 可选的预设 Cookie
  150. isAdminMode, // 是否是管理后台模式
  151. },
  152. icon: 'Monitor',
  153. });
  154. return tab;
  155. }
  156. // 更新标签页
  157. function updateTab(id: string, updates: Partial<Tab>) {
  158. const index = tabs.value.findIndex(tab => tab.id === id);
  159. if (index !== -1) {
  160. tabs.value[index] = { ...tabs.value[index], ...updates };
  161. }
  162. }
  163. // 更新浏览器标签页数据
  164. function updateBrowserTab(id: string, browserData: Partial<Tab['browserData']>) {
  165. const tab = tabs.value.find(t => t.id === id);
  166. if (tab && tab.browserData) {
  167. tab.browserData = { ...tab.browserData, ...browserData };
  168. }
  169. }
  170. // 关闭标签页
  171. function closeTab(id: string) {
  172. const index = tabs.value.findIndex(tab => tab.id === id);
  173. if (index === -1) return;
  174. const tab = tabs.value[index];
  175. if (!tab.closable) return;
  176. tabs.value.splice(index, 1);
  177. // 如果关闭的是当前激活的标签页,激活相邻的标签页
  178. if (activeTabId.value === id) {
  179. if (tabs.value.length > 0) {
  180. // 激活前一个或后一个标签页
  181. const newIndex = Math.min(index, tabs.value.length - 1);
  182. activeTabId.value = tabs.value[newIndex].id;
  183. } else {
  184. activeTabId.value = '';
  185. }
  186. }
  187. }
  188. // 关闭所有标签页
  189. function closeAllTabs() {
  190. tabs.value = tabs.value.filter(tab => !tab.closable);
  191. if (tabs.value.length > 0) {
  192. activeTabId.value = tabs.value[0].id;
  193. } else {
  194. activeTabId.value = '';
  195. }
  196. }
  197. // 关闭其他标签页
  198. function closeOtherTabs(id: string) {
  199. tabs.value = tabs.value.filter(tab => tab.id === id || !tab.closable);
  200. activeTabId.value = id;
  201. }
  202. // 关闭右侧标签页
  203. function closeRightTabs(id: string) {
  204. const index = tabs.value.findIndex(tab => tab.id === id);
  205. if (index === -1) return;
  206. tabs.value = tabs.value.filter((tab, i) => i <= index || !tab.closable);
  207. // 如果当前激活的标签页被关闭了,激活指定的标签页
  208. if (!tabs.value.find(t => t.id === activeTabId.value)) {
  209. activeTabId.value = id;
  210. }
  211. }
  212. // 激活标签页
  213. function activateTab(id: string) {
  214. if (tabs.value.find(tab => tab.id === id)) {
  215. activeTabId.value = id;
  216. }
  217. }
  218. // 通过路径激活标签页
  219. function activateTabByPath(path: string): boolean {
  220. const tab = tabs.value.find(t => t.type === 'page' && t.path === path);
  221. if (tab) {
  222. activeTabId.value = tab.id;
  223. return true;
  224. }
  225. return false;
  226. }
  227. // 通过浏览器会话ID查找标签页
  228. function findTabBySessionId(sessionId: string): Tab | undefined {
  229. return tabs.value.find(
  230. tab => tab.type === 'browser' && tab.browserData?.sessionId === sessionId
  231. );
  232. }
  233. // 获取当前激活的页面路径
  234. function getActivePagePath(): string | undefined {
  235. const tab = activeTab.value;
  236. if (tab?.type === 'page') {
  237. return tab.path;
  238. }
  239. return undefined;
  240. }
  241. // 触发账号列表刷新
  242. function triggerAccountRefresh() {
  243. accountRefreshTrigger.value++;
  244. }
  245. // 触发标签页刷新
  246. function triggerTabRefresh(tabId: string) {
  247. tabRefreshTrigger.value[tabId] = (tabRefreshTrigger.value[tabId] || 0) + 1;
  248. }
  249. // 获取标签页刷新触发器
  250. function getTabRefreshTrigger(tabId: string) {
  251. return computed(() => tabRefreshTrigger.value[tabId] || 0);
  252. }
  253. return {
  254. // 状态
  255. tabs,
  256. activeTabId,
  257. accountRefreshTrigger,
  258. tabRefreshTrigger,
  259. // 计算属性
  260. activeTab,
  261. browserTabs,
  262. pageTabs,
  263. // 方法
  264. addTab,
  265. openPageTab,
  266. openBrowserTab,
  267. updateTab,
  268. updateBrowserTab,
  269. closeTab,
  270. closeAllTabs,
  271. closeOtherTabs,
  272. closeRightTabs,
  273. activateTab,
  274. activateTabByPath,
  275. findTabBySessionId,
  276. getActivePagePath,
  277. triggerAccountRefresh,
  278. triggerTabRefresh,
  279. getTabRefreshTrigger,
  280. };
  281. });