feat: 标签页拖拽创建新窗口时窗口定位优化及 favicon 验证
- 优化拖拽标签创建新窗口时的位置计算,使用 setPosition 确保窗口出现在鼠标位置 - 重构 createWebTabWindowInstance 函数,仅在明确指定 x/y 时设置窗口坐标 - 新增 fetchFaviconAsBase64 工具函数,在主进程验证 favicon 并转为 base64 - favicon 验证后再保存和传递给前端,确保拖拽后 icon 状态与原窗口一致 - 简化前端 favicon 处理逻辑,移除重复的图片验证代码
1056
electron/electron.js
vendored
90
electron/lib/utils.js
vendored
@ -5,7 +5,7 @@ const dayjs = require("dayjs");
|
||||
const http = require('http')
|
||||
const https = require('https')
|
||||
const crypto = require('crypto')
|
||||
const {shell, dialog, session, Notification, nativeTheme} = require("electron");
|
||||
const {shell, dialog, session, net, Notification, nativeTheme} = require("electron");
|
||||
const loger = require("electron-log");
|
||||
const Store = require("electron-store");
|
||||
const store = new Store();
|
||||
@ -642,6 +642,94 @@ const utils = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取并验证 favicon,转换为 base64
|
||||
* @param {string} faviconUrl - favicon 的 URL
|
||||
* @param {number} timeout - 超时时间(毫秒),默认 5000
|
||||
* @returns {Promise<string|null>} - 成功返回 base64 data URL,失败返回 null
|
||||
*/
|
||||
async fetchFaviconAsBase64(faviconUrl, timeout = 5000) {
|
||||
if (!faviconUrl || typeof faviconUrl !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果已经是 base64,直接返回
|
||||
if (faviconUrl.startsWith('data:')) {
|
||||
return faviconUrl;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const request = net.request(faviconUrl);
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
request.abort();
|
||||
resolve(null);
|
||||
}, timeout);
|
||||
|
||||
const chunks = [];
|
||||
|
||||
request.on('response', (response) => {
|
||||
const contentType = response.headers['content-type'];
|
||||
// 验证是否为图片类型
|
||||
const isImage = contentType && (
|
||||
contentType.includes('image/') ||
|
||||
contentType.includes('icon')
|
||||
);
|
||||
|
||||
if (response.statusCode !== 200 || !isImage) {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
clearTimeout(timeoutId);
|
||||
try {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
// 验证图片数据有效(至少有一些字节)
|
||||
if (buffer.length < 10) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
// 获取正确的 MIME 类型
|
||||
let mimeType = 'image/png';
|
||||
if (contentType) {
|
||||
const match = contentType.match(/^([^;]+)/);
|
||||
if (match) {
|
||||
mimeType = match[1].trim();
|
||||
}
|
||||
}
|
||||
const base64 = buffer.toString('base64');
|
||||
resolve(`data:${mimeType};base64,${base64}`);
|
||||
} catch (e) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
request.end();
|
||||
} catch (e) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否是本地URL
|
||||
* @param url
|
||||
|
||||
90
electron/render/tabs/assets/css/style.css
vendored
@ -117,8 +117,8 @@ body {
|
||||
background: var(--tab-active-background);
|
||||
}
|
||||
|
||||
.nav-tabs li.active .tab-icon.background {
|
||||
background-image: url(../image/link_normal_selected_icon.png);
|
||||
.nav-tabs li.active .tab-icon::before {
|
||||
background-image: var(--tab-icon-image, url(../image/earth/light_selected.svg));
|
||||
}
|
||||
|
||||
|
||||
@ -152,8 +152,10 @@ body {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: cover;
|
||||
background-image: url(../image/link_normal_selected_icon.png);
|
||||
background-size: 94%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../image/link/light_selected.svg);
|
||||
}
|
||||
|
||||
/* 图标 */
|
||||
@ -162,20 +164,35 @@ body {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: var(--tab-icon-image, url(../image/earth/light.svg));
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.tab-icon.background {
|
||||
background-image: url(../image/link_normal_icon.png);
|
||||
.tab-icon.loading::before {
|
||||
transform: scale(0.75);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.tab-icon.loading {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.tab-icon .tab-icon-loading {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.tab-icon.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #eeeeee;
|
||||
border-bottom-color: #84C56A;
|
||||
border-radius: 50%;
|
||||
@ -184,10 +201,6 @@ body {
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.tab-icon:not(.loading) .tab-icon-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-icon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@ -299,8 +312,8 @@ body.darwin.full-screen .nav {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.sortable-fallback .tab-icon.background {
|
||||
background-image: url(../image/link_normal_selected_icon.png);
|
||||
.sortable-fallback .tab-icon::before {
|
||||
background-image: var(--tab-icon-image, url(../image/earth/light_selected.svg));
|
||||
}
|
||||
|
||||
.sortable-fallback .tab-title {
|
||||
@ -338,6 +351,31 @@ body.darwin.full-screen .nav {
|
||||
transform: translate(50%, -50%) scale(0.9) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* 拖出窗口时的视觉反馈 */
|
||||
.sortable-fallback.detaching {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
/* 拖入目标窗口时的视觉反馈 */
|
||||
.nav-tabs.drag-target {
|
||||
background: rgba(132, 197, 106, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-tabs.drag-target::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 2px dashed #84C56A;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 暗黑模式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@ -348,16 +386,16 @@ body.darwin.full-screen .nav {
|
||||
--tab-close-color: #E3E3E3;
|
||||
}
|
||||
|
||||
.nav-tabs li.active .tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
.nav-tabs li.active .tab-icon::before {
|
||||
background-image: var(--tab-icon-image, url(../image/earth/dark_selected.svg));
|
||||
}
|
||||
|
||||
.nav-browser span {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
background-image: url(../image/link/dark_selected.svg);
|
||||
}
|
||||
|
||||
.tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_icon.png);
|
||||
.tab-icon::before {
|
||||
background-image: var(--tab-icon-image, url(../image/earth/dark.svg));
|
||||
}
|
||||
|
||||
/* 暗黑模式下 fallback 样式 */
|
||||
@ -365,7 +403,7 @@ body.darwin.full-screen .nav {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.sortable-fallback .tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
.sortable-fallback .tab-icon::before {
|
||||
background-image: var(--tab-icon-image, url(../image/earth/dark_selected.svg));
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
1
electron/render/tabs/assets/image/earth/dark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 2.06850072C230.79961665 2.06850072 2.06850072 230.79961665 2.06850072 512s228.73111593 509.93149928 509.93149928 509.93149928 509.93149928-228.73111593 509.93149928-509.93149928S793.20038335 2.06850072 512 2.06850072z m246.91419965 572.5993967c-110.84300484 0-139.1576223 60.18533617-143.31758979 110.70881233-2.68385 32.2732962-1.7445025 53.34151868-1.00644375 70.3168699 1.07354 24.55722746 1.61030999 34.6887612-10.06443749 59.98404742-5.36769999 11.80893999-32.1391037 14.96246373-55.68988742 17.71340998-33.27973996 3.89158249-74.74522239 8.78960875-87.35931738 45.49125743-7.24639499 21.20241496-4.22706374 50.05380243 9.39347499 87.56060613a456.25449935 456.25449935 0 0 1-313.20529457-167.13675852c36.7016487-14.09021248 66.89496116-34.9571462 90.11026363-62.19822365 39.05001745-45.82673868 41.33128995-92.59282487 43.14288869-130.16672482 1.61030999-33.07845121 3.28771625-52.60345992 19.32371997-68.84075241 9.79605249-9.93024499 22.67853247-14.35859749 41.86805994-14.35859747 18.51856497 0 39.92226869 4.1599675 62.53370491 8.58831998 25.69786372 4.96512249 52.20088243 10.13153374 78.77099739 10.13153373 46.56479743 0 82.19290613-16.37148498 108.89721361-50.05380242 66.35819116-83.73611987 23.88626496-145.06209229-4.22706376-185.58822724-12.94957623-18.65275748-25.16109371-36.2990712-24.35593871-50.32218742 1.20773249-21.60499247 10.13153374-36.63455245 20.39725997-54.01248118 15.49923373-26.16753746 34.75585745-58.64212242 17.44502498-110.44042734a109.36688734 109.36688734 0 0 0-14.69407873-27.91203997 453.43645686 453.43645686 0 0 1 371.17645448 448.00166063c0 51.79830492-8.78960875 102.38887735-26.16753747 151.23494727-50.45637993-58.10535241-113.05718108-88.56704987-183.17276224-88.56704988z" fill="#747C87"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 2.06850072C230.79961665 2.06850072 2.06850072 230.79961665 2.06850072 512s228.73111593 509.93149928 509.93149928 509.93149928 509.93149928-228.73111593 509.93149928-509.93149928S793.20038335 2.06850072 512 2.06850072z m246.91419965 572.5993967c-110.84300484 0-139.1576223 60.18533617-143.31758979 110.70881233-2.68385 32.2732962-1.7445025 53.34151868-1.00644375 70.3168699 1.07354 24.55722746 1.61030999 34.6887612-10.06443749 59.98404742-5.36769999 11.80893999-32.1391037 14.96246373-55.68988742 17.71340998-33.27973996 3.89158249-74.74522239 8.78960875-87.35931738 45.49125743-7.24639499 21.20241496-4.22706374 50.05380243 9.39347499 87.56060613a456.25449935 456.25449935 0 0 1-313.20529457-167.13675852c36.7016487-14.09021248 66.89496116-34.9571462 90.11026363-62.19822365 39.05001745-45.82673868 41.33128995-92.59282487 43.14288869-130.16672482 1.61030999-33.07845121 3.28771625-52.60345992 19.32371997-68.84075241 9.79605249-9.93024499 22.67853247-14.35859749 41.86805994-14.35859747 18.51856497 0 39.92226869 4.1599675 62.53370491 8.58831998 25.69786372 4.96512249 52.20088243 10.13153374 78.77099739 10.13153373 46.56479743 0 82.19290613-16.37148498 108.89721361-50.05380242 66.35819116-83.73611987 23.88626496-145.06209229-4.22706376-185.58822724-12.94957623-18.65275748-25.16109371-36.2990712-24.35593871-50.32218742 1.20773249-21.60499247 10.13153374-36.63455245 20.39725997-54.01248118 15.49923373-26.16753746 34.75585745-58.64212242 17.44502498-110.44042734a109.36688734 109.36688734 0 0 0-14.69407873-27.91203997 453.43645686 453.43645686 0 0 1 371.17645448 448.00166063c0 51.79830492-8.78960875 102.38887735-26.16753747 151.23494727-50.45637993-58.10535241-113.05718108-88.56704987-183.17276224-88.56704988z" fill="#C7C7C7"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
electron/render/tabs/assets/image/earth/light.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 2.06850072C230.79961665 2.06850072 2.06850072 230.79961665 2.06850072 512s228.73111593 509.93149928 509.93149928 509.93149928 509.93149928-228.73111593 509.93149928-509.93149928S793.20038335 2.06850072 512 2.06850072z m246.91419965 572.5993967c-110.84300484 0-139.1576223 60.18533617-143.31758979 110.70881233-2.68385 32.2732962-1.7445025 53.34151868-1.00644375 70.3168699 1.07354 24.55722746 1.61030999 34.6887612-10.06443749 59.98404742-5.36769999 11.80893999-32.1391037 14.96246373-55.68988742 17.71340998-33.27973996 3.89158249-74.74522239 8.78960875-87.35931738 45.49125743-7.24639499 21.20241496-4.22706374 50.05380243 9.39347499 87.56060613a456.25449935 456.25449935 0 0 1-313.20529457-167.13675852c36.7016487-14.09021248 66.89496116-34.9571462 90.11026363-62.19822365 39.05001745-45.82673868 41.33128995-92.59282487 43.14288869-130.16672482 1.61030999-33.07845121 3.28771625-52.60345992 19.32371997-68.84075241 9.79605249-9.93024499 22.67853247-14.35859749 41.86805994-14.35859747 18.51856497 0 39.92226869 4.1599675 62.53370491 8.58831998 25.69786372 4.96512249 52.20088243 10.13153374 78.77099739 10.13153373 46.56479743 0 82.19290613-16.37148498 108.89721361-50.05380242 66.35819116-83.73611987 23.88626496-145.06209229-4.22706376-185.58822724-12.94957623-18.65275748-25.16109371-36.2990712-24.35593871-50.32218742 1.20773249-21.60499247 10.13153374-36.63455245 20.39725997-54.01248118 15.49923373-26.16753746 34.75585745-58.64212242 17.44502498-110.44042734a109.36688734 109.36688734 0 0 0-14.69407873-27.91203997 453.43645686 453.43645686 0 0 1 371.17645448 448.00166063c0 51.79830492-8.78960875 102.38887735-26.16753747 151.23494727-50.45637993-58.10535241-113.05718108-88.56704987-183.17276224-88.56704988z" fill="#747C87"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 2.06850072C230.79961665 2.06850072 2.06850072 230.79961665 2.06850072 512s228.73111593 509.93149928 509.93149928 509.93149928 509.93149928-228.73111593 509.93149928-509.93149928S793.20038335 2.06850072 512 2.06850072z m246.91419965 572.5993967c-110.84300484 0-139.1576223 60.18533617-143.31758979 110.70881233-2.68385 32.2732962-1.7445025 53.34151868-1.00644375 70.3168699 1.07354 24.55722746 1.61030999 34.6887612-10.06443749 59.98404742-5.36769999 11.80893999-32.1391037 14.96246373-55.68988742 17.71340998-33.27973996 3.89158249-74.74522239 8.78960875-87.35931738 45.49125743-7.24639499 21.20241496-4.22706374 50.05380243 9.39347499 87.56060613a456.25449935 456.25449935 0 0 1-313.20529457-167.13675852c36.7016487-14.09021248 66.89496116-34.9571462 90.11026363-62.19822365 39.05001745-45.82673868 41.33128995-92.59282487 43.14288869-130.16672482 1.61030999-33.07845121 3.28771625-52.60345992 19.32371997-68.84075241 9.79605249-9.93024499 22.67853247-14.35859749 41.86805994-14.35859747 18.51856497 0 39.92226869 4.1599675 62.53370491 8.58831998 25.69786372 4.96512249 52.20088243 10.13153374 78.77099739 10.13153373 46.56479743 0 82.19290613-16.37148498 108.89721361-50.05380242 66.35819116-83.73611987 23.88626496-145.06209229-4.22706376-185.58822724-12.94957623-18.65275748-25.16109371-36.2990712-24.35593871-50.32218742 1.20773249-21.60499247 10.13153374-36.63455245 20.39725997-54.01248118 15.49923373-26.16753746 34.75585745-58.64212242 17.44502498-110.44042734a109.36688734 109.36688734 0 0 0-14.69407873-27.91203997 453.43645686 453.43645686 0 0 1 371.17645448 448.00166063c0 51.79830492-8.78960875 102.38887735-26.16753747 151.23494727-50.45637993-58.10535241-113.05718108-88.56704987-183.17276224-88.56704988z" fill="#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
electron/render/tabs/assets/image/link/dark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 4.63280312c280.21890262 0 507.36719688 227.14829428 507.36719688 507.36719688 0 275.50038771-219.53778634 499.6552147-493.160915 507.16424961L512 1019.36719688l-14.20628188-0.20294727C224.17058942 1011.6552147 4.63280312 787.50038771 4.63280312 512 4.63280312 231.78109738 231.78109738 4.63280312 512 4.63280312zM287.99738314 550.10327686H82.41219433a431.51580062 431.51580062 0 0 0 343.23390844 384.4828615 756.12933254 756.12933254 0 0 1-135.72072467-362.56459863l-1.92799496-21.91826287z m653.59042253 0h-205.48371458a756.12933254 756.12933254 0 0 1-137.64872079 384.58433456 431.6680108 431.6680108 0 0 0 341.76254393-370.98689449l1.36989144-13.59744007z m-281.79174131-0.05073713H364.25467278a680.02425307 680.02425307 0 0 0 126.84179833 345.41558778l12.43049712 16.64164326 8.47303177 10.65471113 8.47303177-10.65471113a679.77056988 679.77056988 0 0 0 137.24282747-339.32718022l2.08020512-22.73005082z m-234.25143466-460.68941402l-2.02946915 0.45663048a431.51580062 431.51580062 0 0 0-341.10296622 384.12770408h205.48371458a756.12933254 756.12933254 0 0 1 137.64872079-384.58433456zM512 101.184781l-8.47303177 10.65471112a679.3646765 679.3646765 0 0 0-139.37376968 362.15870525h295.59212867a680.02425307 680.02425307 0 0 0-126.84179833-345.46632488l-12.43049712-16.64164326L512 101.184781z m86.35389723-11.77091936l2.79051878 3.80525397a757.24554076 757.24554076 0 0 1 134.85820085 380.72834466h205.58518881a431.56653774 431.56653774 0 0 0-330.09309885-381.69234153l-13.14080959-2.8412571z" fill="#747C87"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
electron/render/tabs/assets/image/link/dark_selected.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 4.63280312c280.21890262 0 507.36719688 227.14829428 507.36719688 507.36719688 0 275.50038771-219.53778634 499.6552147-493.160915 507.16424961L512 1019.36719688l-14.20628188-0.20294727C224.17058942 1011.6552147 4.63280312 787.50038771 4.63280312 512 4.63280312 231.78109738 231.78109738 4.63280312 512 4.63280312zM287.99738314 550.10327686H82.41219433a431.51580062 431.51580062 0 0 0 343.23390844 384.4828615 756.12933254 756.12933254 0 0 1-135.72072467-362.56459863l-1.92799496-21.91826287z m653.59042253 0h-205.48371458a756.12933254 756.12933254 0 0 1-137.64872079 384.58433456 431.6680108 431.6680108 0 0 0 341.76254393-370.98689449l1.36989144-13.59744007z m-281.79174131-0.05073713H364.25467278a680.02425307 680.02425307 0 0 0 126.84179833 345.41558778l12.43049712 16.64164326 8.47303177 10.65471113 8.47303177-10.65471113a679.77056988 679.77056988 0 0 0 137.24282747-339.32718022l2.08020512-22.73005082z m-234.25143466-460.68941402l-2.02946915 0.45663048a431.51580062 431.51580062 0 0 0-341.10296622 384.12770408h205.48371458a756.12933254 756.12933254 0 0 1 137.64872079-384.58433456zM512 101.184781l-8.47303177 10.65471112a679.3646765 679.3646765 0 0 0-139.37376968 362.15870525h295.59212867a680.02425307 680.02425307 0 0 0-126.84179833-345.46632488l-12.43049712-16.64164326L512 101.184781z m86.35389723-11.77091936l2.79051878 3.80525397a757.24554076 757.24554076 0 0 1 134.85820085 380.72834466h205.58518881a431.56653774 431.56653774 0 0 0-330.09309885-381.69234153l-13.14080959-2.8412571z" fill="#C7C7C7"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
electron/render/tabs/assets/image/link/light.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 4.63280312c280.21890262 0 507.36719688 227.14829428 507.36719688 507.36719688 0 275.50038771-219.53778634 499.6552147-493.160915 507.16424961L512 1019.36719688l-14.20628188-0.20294727C224.17058942 1011.6552147 4.63280312 787.50038771 4.63280312 512 4.63280312 231.78109738 231.78109738 4.63280312 512 4.63280312zM287.99738314 550.10327686H82.41219433a431.51580062 431.51580062 0 0 0 343.23390844 384.4828615 756.12933254 756.12933254 0 0 1-135.72072467-362.56459863l-1.92799496-21.91826287z m653.59042253 0h-205.48371458a756.12933254 756.12933254 0 0 1-137.64872079 384.58433456 431.6680108 431.6680108 0 0 0 341.76254393-370.98689449l1.36989144-13.59744007z m-281.79174131-0.05073713H364.25467278a680.02425307 680.02425307 0 0 0 126.84179833 345.41558778l12.43049712 16.64164326 8.47303177 10.65471113 8.47303177-10.65471113a679.77056988 679.77056988 0 0 0 137.24282747-339.32718022l2.08020512-22.73005082z m-234.25143466-460.68941402l-2.02946915 0.45663048a431.51580062 431.51580062 0 0 0-341.10296622 384.12770408h205.48371458a756.12933254 756.12933254 0 0 1 137.64872079-384.58433456zM512 101.184781l-8.47303177 10.65471112a679.3646765 679.3646765 0 0 0-139.37376968 362.15870525h295.59212867a680.02425307 680.02425307 0 0 0-126.84179833-345.46632488l-12.43049712-16.64164326L512 101.184781z m86.35389723-11.77091936l2.79051878 3.80525397a757.24554076 757.24554076 0 0 1 134.85820085 380.72834466h205.58518881a431.56653774 431.56653774 0 0 0-330.09309885-381.69234153l-13.14080959-2.8412571z" fill="#747C87"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M512 4.63280312c280.21890262 0 507.36719688 227.14829428 507.36719688 507.36719688 0 275.50038771-219.53778634 499.6552147-493.160915 507.16424961L512 1019.36719688l-14.20628188-0.20294727C224.17058942 1011.6552147 4.63280312 787.50038771 4.63280312 512 4.63280312 231.78109738 231.78109738 4.63280312 512 4.63280312zM287.99738314 550.10327686H82.41219433a431.51580062 431.51580062 0 0 0 343.23390844 384.4828615 756.12933254 756.12933254 0 0 1-135.72072467-362.56459863l-1.92799496-21.91826287z m653.59042253 0h-205.48371458a756.12933254 756.12933254 0 0 1-137.64872079 384.58433456 431.6680108 431.6680108 0 0 0 341.76254393-370.98689449l1.36989144-13.59744007z m-281.79174131-0.05073713H364.25467278a680.02425307 680.02425307 0 0 0 126.84179833 345.41558778l12.43049712 16.64164326 8.47303177 10.65471113 8.47303177-10.65471113a679.77056988 679.77056988 0 0 0 137.24282747-339.32718022l2.08020512-22.73005082z m-234.25143466-460.68941402l-2.02946915 0.45663048a431.51580062 431.51580062 0 0 0-341.10296622 384.12770408h205.48371458a756.12933254 756.12933254 0 0 1 137.64872079-384.58433456zM512 101.184781l-8.47303177 10.65471112a679.3646765 679.3646765 0 0 0-139.37376968 362.15870525h295.59212867a680.02425307 680.02425307 0 0 0-126.84179833-345.46632488l-12.43049712-16.64164326L512 101.184781z m86.35389723-11.77091936l2.79051878 3.80525397a757.24554076 757.24554076 0 0 1 134.85820085 380.72834466h205.58518881a431.56653774 431.56653774 0 0 0-330.09309885-381.69234153l-13.14080959-2.8412571z" fill="#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 914 B |
@ -24,10 +24,7 @@
|
||||
</div>
|
||||
<ul class="nav-tabs">
|
||||
<li v-for="item in tabs" :key="item.id" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)">
|
||||
<div v-if="item.state === 'loading'" class="tab-icon loading">
|
||||
<div class="tab-icon-loading"></div>
|
||||
</div>
|
||||
<div v-else class="tab-icon background" :style="iconStyle(item)"></div>
|
||||
<div :class="['tab-icon', item.state === 'loading' ? 'loading' : null]" :style="iconStyle(item)"></div>
|
||||
<div class="tab-title" :title="item.title">{{tabTitle(item)}}</div>
|
||||
<div class="tab-close" @click.stop="onClose(item)"></div>
|
||||
</li>
|
||||
@ -42,6 +39,9 @@
|
||||
const App = {
|
||||
data() {
|
||||
return {
|
||||
// 当前窗口ID
|
||||
windowId: null,
|
||||
|
||||
// 当前激活的标签页ID
|
||||
activeId: 0,
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
// 停止定时器
|
||||
stopTimer: null,
|
||||
|
||||
|
||||
// 是否可以后退
|
||||
canGoBack: false,
|
||||
|
||||
@ -59,12 +59,28 @@
|
||||
|
||||
// 是否正在拖拽(用于跳过 watch 同步)
|
||||
isDragging: false,
|
||||
|
||||
// 拖拽状态
|
||||
dragState: {
|
||||
tabId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startScreenX: 0,
|
||||
startScreenY: 0,
|
||||
},
|
||||
|
||||
// 其他窗口信息(用于拖入检测)
|
||||
otherWindows: [],
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
document.body.classList.add(window.process.platform)
|
||||
},
|
||||
mounted() {
|
||||
// 从 URL 参数获取窗口ID
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.windowId = parseInt(urlParams.get('windowId')) || null;
|
||||
|
||||
// 初始化 Sortable 拖拽排序
|
||||
this.initSortable()
|
||||
|
||||
@ -73,17 +89,23 @@
|
||||
switch (event) {
|
||||
// 创建标签页
|
||||
case 'create':
|
||||
const newTab = Object.assign({
|
||||
// 检查是否已存在该标签
|
||||
if (this.tabs.some(t => t.id === id)) {
|
||||
break
|
||||
}
|
||||
const newTab = {
|
||||
id,
|
||||
title: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
state: 'loading'
|
||||
}, detail)
|
||||
title: detail.title || '',
|
||||
url: detail.url || '',
|
||||
favicon: detail.favicon || '',
|
||||
state: detail.state || 'loading'
|
||||
}
|
||||
|
||||
// 确定插入位置
|
||||
let insertIndex = this.tabs.length
|
||||
if (detail.afterId) {
|
||||
if (typeof detail.insertIndex === 'number') {
|
||||
insertIndex = Math.max(0, Math.min(detail.insertIndex, this.tabs.length))
|
||||
} else if (detail.afterId) {
|
||||
const afterIndex = this.tabs.findIndex(item => item.id === detail.afterId)
|
||||
if (afterIndex > -1) {
|
||||
insertIndex = afterIndex + 1
|
||||
@ -125,13 +147,7 @@
|
||||
case 'favicon':
|
||||
const faviconItem = this.tabs.find(item => item.id === id)
|
||||
if (faviconItem) {
|
||||
faviconItem.icon = detail.favicons[detail.favicons.length - 1]
|
||||
//
|
||||
const img = new Image();
|
||||
img.onerror = () => {
|
||||
faviconItem.icon = ''
|
||||
};
|
||||
img.src = faviconItem.icon
|
||||
faviconItem.favicon = detail.favicon || ''
|
||||
}
|
||||
break
|
||||
|
||||
@ -263,14 +279,58 @@
|
||||
onStart: (evt) => {
|
||||
this.isDragging = true
|
||||
const item = this.tabs[evt.oldIndex]
|
||||
if (item && this.activeId !== item.id) {
|
||||
this.sendMessage('webTabActivate', item.id)
|
||||
if (item) {
|
||||
// 记录拖拽起始状态
|
||||
this.dragState.tabId = item.id
|
||||
this.dragState.startX = evt.originalEvent.clientX
|
||||
this.dragState.startY = evt.originalEvent.clientY
|
||||
this.dragState.startScreenX = evt.originalEvent.screenX
|
||||
this.dragState.startScreenY = evt.originalEvent.screenY
|
||||
|
||||
if (this.activeId !== item.id) {
|
||||
this.sendMessage('webTabActivate', {windowId: this.windowId, tabId: item.id})
|
||||
}
|
||||
|
||||
// 获取所有窗口信息(用于拖入检测)
|
||||
this.fetchAllWindowsInfo()
|
||||
}
|
||||
},
|
||||
// 拖拽移动时检测边界
|
||||
onMove: (evt, originalEvent) => {
|
||||
if (!originalEvent) return true
|
||||
this.checkDragBoundary(originalEvent)
|
||||
return true
|
||||
},
|
||||
// 拖拽结束回调
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt
|
||||
const originalEvent = evt.originalEvent
|
||||
|
||||
// 检查是否拖出了窗口边界
|
||||
if (this.dragState.tabId && originalEvent) {
|
||||
const shouldDetach = this.checkDetachCondition(originalEvent)
|
||||
const targetWindow = this.findTargetWindow(originalEvent.screenX, originalEvent.screenY)
|
||||
|
||||
if (targetWindow && targetWindow.windowId !== this.windowId) {
|
||||
// 拖入其他窗口
|
||||
this.attachToWindow(this.dragState.tabId, targetWindow.windowId, originalEvent.screenX)
|
||||
this.resetDragState()
|
||||
this.$nextTick(() => {
|
||||
this.isDragging = false
|
||||
})
|
||||
return
|
||||
} else if (shouldDetach) {
|
||||
// 拖出创建新窗口
|
||||
this.detachTab(this.dragState.tabId, originalEvent.screenX, originalEvent.screenY)
|
||||
this.resetDragState()
|
||||
this.$nextTick(() => {
|
||||
this.isDragging = false
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 正常的窗口内排序
|
||||
if (oldIndex !== newIndex) {
|
||||
// 先将 DOM 恢复到原始位置,让 Vue 来控制渲染
|
||||
const parent = evt.from
|
||||
@ -286,9 +346,11 @@
|
||||
this.tabs.splice(newIndex, 0, item)
|
||||
|
||||
// 通知主进程同步顺序
|
||||
this.sendMessage('webTabReorder', this.tabs.map(t => t.id))
|
||||
this.sendMessage('webTabReorder', {windowId: this.windowId, newOrder: this.tabs.map(t => t.id)})
|
||||
}
|
||||
|
||||
this.resetDragState()
|
||||
|
||||
// 延迟重置标志,确保 Vue 渲染完成
|
||||
this.$nextTick(() => {
|
||||
this.isDragging = false
|
||||
@ -297,12 +359,120 @@
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有窗口信息
|
||||
*/
|
||||
async fetchAllWindowsInfo() {
|
||||
try {
|
||||
const windows = await electron?.sendAsync('webTabGetAllWindows')
|
||||
this.otherWindows = (windows || []).filter(w => w.windowId !== this.windowId)
|
||||
} catch (e) {
|
||||
this.otherWindows = []
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查拖拽边界
|
||||
*/
|
||||
checkDragBoundary(event) {
|
||||
// 实时视觉反馈
|
||||
const fallbackEl = document.querySelector('.sortable-fallback')
|
||||
if (fallbackEl) {
|
||||
const isOutside = this.checkDetachCondition(event)
|
||||
fallbackEl.classList.toggle('detaching', isOutside)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否满足分离条件
|
||||
*/
|
||||
checkDetachCondition(event) {
|
||||
// 检查鼠标是否超出窗口边界
|
||||
const windowBounds = {
|
||||
x: window.screenX,
|
||||
y: window.screenY,
|
||||
width: window.outerWidth,
|
||||
height: window.outerHeight
|
||||
}
|
||||
|
||||
const screenX = event.screenX
|
||||
const screenY = event.screenY
|
||||
|
||||
// 检查是否在窗口范围外(左右上)
|
||||
const isOutsideHorizontal = screenX < windowBounds.x ||
|
||||
screenX > windowBounds.x + windowBounds.width
|
||||
const isOutsideTop = screenY < windowBounds.y
|
||||
const isOutsideBottom = screenY > windowBounds.y + windowBounds.height
|
||||
|
||||
// 检查垂直移动距离(用于向下拖动检测)
|
||||
const verticalMove = Math.abs(screenY - this.dragState.startScreenY)
|
||||
const tabBarHeight = 40
|
||||
const detachThreshold = tabBarHeight + 20 // 超过标签栏高度 + 20px 触发分离
|
||||
|
||||
// 向下拖动超过阈值也触发分离
|
||||
const isDownwardDetach = screenY > this.dragState.startScreenY + detachThreshold
|
||||
|
||||
return isOutsideHorizontal || isOutsideTop || isOutsideBottom || isDownwardDetach
|
||||
},
|
||||
|
||||
/**
|
||||
* 查找目标窗口(用于拖入)
|
||||
*/
|
||||
findTargetWindow(screenX, screenY) {
|
||||
for (const win of this.otherWindows) {
|
||||
const tb = win.tabBarBounds
|
||||
// 检查是否在目标窗口的标签栏区域内
|
||||
if (screenX >= tb.x && screenX <= tb.x + tb.width &&
|
||||
screenY >= tb.y && screenY <= tb.y + tb.height) {
|
||||
return win
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
* 分离标签到新窗口
|
||||
*/
|
||||
detachTab(tabId, screenX, screenY) {
|
||||
this.sendMessage('webTabDetach', {
|
||||
windowId: this.windowId,
|
||||
tabId: tabId,
|
||||
screenX: screenX,
|
||||
screenY: screenY
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 将标签附加到目标窗口
|
||||
*/
|
||||
attachToWindow(tabId, targetWindowId, screenX) {
|
||||
// 计算插入位置(可以根据 screenX 确定位置,这里简化处理)
|
||||
this.sendMessage('webTabAttach', {
|
||||
sourceWindowId: this.windowId,
|
||||
tabId: tabId,
|
||||
targetWindowId: targetWindowId,
|
||||
insertIndex: null // 追加到末尾
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置拖拽状态
|
||||
*/
|
||||
resetDragState() {
|
||||
this.dragState.tabId = null
|
||||
this.dragState.startX = 0
|
||||
this.dragState.startY = 0
|
||||
this.dragState.startScreenX = 0
|
||||
this.dragState.startScreenY = 0
|
||||
this.otherWindows = []
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换标签页
|
||||
* @param item
|
||||
*/
|
||||
onSwitch(item) {
|
||||
this.sendMessage('webTabActivate', item.id)
|
||||
this.sendMessage('webTabActivate', {windowId: this.windowId, tabId: item.id})
|
||||
},
|
||||
|
||||
/**
|
||||
@ -310,7 +480,7 @@
|
||||
* @param item
|
||||
*/
|
||||
onClose(item) {
|
||||
this.sendMessage('webTabClose', item.id);
|
||||
this.sendMessage('webTabClose', {windowId: this.windowId, tabId: item.id});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -326,7 +496,7 @@
|
||||
* @returns {string}
|
||||
*/
|
||||
iconStyle(item) {
|
||||
return item.icon ? `background-image: url(${item.icon})` : ''
|
||||
return item.favicon ? `--tab-icon-image: url(${item.favicon})` : ''
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||