feat: 标签页拖拽创建新窗口时窗口定位优化及 favicon 验证

- 优化拖拽标签创建新窗口时的位置计算,使用 setPosition 确保窗口出现在鼠标位置
  - 重构 createWebTabWindowInstance 函数,仅在明确指定 x/y 时设置窗口坐标
  - 新增 fetchFaviconAsBase64 工具函数,在主进程验证 favicon 并转为 base64
  - favicon 验证后再保存和传递给前端,确保拖拽后 icon 状态与原窗口一致
  - 简化前端 favicon 处理逻辑,移除重复的图片验证代码
This commit is contained in:
kuaifan 2026-01-09 13:58:22 +00:00
parent 9d62ec1ec1
commit 089f219280
16 changed files with 1138 additions and 326 deletions

1056
electron/electron.js vendored

File diff suppressed because it is too large Load Diff

90
electron/lib/utils.js vendored
View File

@ -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

View File

@ -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));
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View 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

View 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="#C7C7C7"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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

View 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="#000000"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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

View 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

View 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

View 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="#000000"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

View File

@ -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})` : ''
},
/**