feat(workbench): implement workbench window management and navigation state
Deployment Config CI / Deployment Config (push) Successful in 24s
Desktop Client Build / Resolve Build Metadata (push) Successful in 39s
Frontend CI / Frontend (push) Successful in 3m3s
Backend CI / Backend (push) Successful in 16m18s
Desktop Client Build / Build Desktop Client (push) Successful in 24m1s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 35s
Deployment Config CI / Deployment Config (push) Successful in 24s
Desktop Client Build / Resolve Build Metadata (push) Successful in 39s
Frontend CI / Frontend (push) Successful in 3m3s
Backend CI / Backend (push) Successful in 16m18s
Desktop Client Build / Build Desktop Client (push) Successful in 24m1s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 35s
This commit is contained in:
@@ -72,6 +72,7 @@ import {
|
||||
} from './session-registry'
|
||||
import { upsertDesktopAccount } from './transport/api-client'
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
import { openWorkbenchWindow } from './workbench-window'
|
||||
|
||||
interface DetectedAccount {
|
||||
platformUid: string
|
||||
@@ -4559,7 +4560,11 @@ export async function openPublishAccountConsole(account: PublishAccountIdentity)
|
||||
throw new Error(`desktop_account_session_expired:${account.platform}`)
|
||||
}
|
||||
}
|
||||
const window = createBoundWindow(`${definition.label} 创作台`, consoleURL, handle.session)
|
||||
const window = await openWorkbenchWindow({
|
||||
title: `${definition.label} 创作台`,
|
||||
targetURL: consoleURL,
|
||||
session: handle.session,
|
||||
})
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ export function initTray(onOpen: () => void): ElectronTray {
|
||||
updateTrayIssueIndicator(0)
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: 'Open', click: () => onOpen() },
|
||||
{ label: '打开省心推', click: () => onOpen() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
{ label: '退出省心推', click: () => app.quit() },
|
||||
]),
|
||||
)
|
||||
tray.on('click', () => onOpen())
|
||||
|
||||
@@ -0,0 +1,807 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import type { Session, WebContents } from 'electron/main'
|
||||
import { BrowserWindow, WebContentsView, ipcMain, nativeTheme } from 'electron/main'
|
||||
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
|
||||
interface WorkbenchWindowOptions {
|
||||
title: string
|
||||
targetURL: string
|
||||
session: Session
|
||||
}
|
||||
|
||||
interface WorkbenchNavigationState {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
loading: boolean
|
||||
url: string
|
||||
title: string
|
||||
host: string
|
||||
secure: boolean
|
||||
}
|
||||
|
||||
interface WorkbenchWindowRecord {
|
||||
shellContentsId: number
|
||||
window: BrowserWindow
|
||||
view: WebContentsView
|
||||
title: string
|
||||
initialURL: string
|
||||
activeTargetURL: string
|
||||
pageTitle: string
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const TOOLBAR_HEIGHT = 46
|
||||
const workbenchWindows = new Map<number, WorkbenchWindowRecord>()
|
||||
let handlersRegistered = false
|
||||
|
||||
function preloadPath(): string {
|
||||
return join(__dirname, '../preload/bridge.cjs')
|
||||
}
|
||||
|
||||
function titleBarStyle(): 'hidden' | 'hiddenInset' {
|
||||
return process.platform === 'darwin' ? 'hiddenInset' : 'hidden'
|
||||
}
|
||||
|
||||
function titleBarOverlay(): { color: string; symbolColor: string; height: number } | undefined {
|
||||
if (process.platform === 'darwin') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
color: nativeTheme.shouldUseDarkColors ? '#15191a' : '#fbfcfd',
|
||||
symbolColor: nativeTheme.shouldUseDarkColors ? '#f8fafc' : '#111827',
|
||||
height: TOOLBAR_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function hostFromURL(rawURL: string): string {
|
||||
try {
|
||||
return new URL(rawURL).host
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function isRemoteHTTPURL(rawURL: string): boolean {
|
||||
return /^https?:\/\//i.test(rawURL)
|
||||
}
|
||||
|
||||
function isLocalWorkbenchPage(rawURL: string): boolean {
|
||||
return rawURL.startsWith('data:text/html')
|
||||
}
|
||||
|
||||
function workbenchShellHTML(title: string, targetURL: string): string {
|
||||
const initialHost = hostFromURL(targetURL)
|
||||
const platformClass = `platform-${process.platform}`
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--toolbar-height: ${TOOLBAR_HEIGHT}px;
|
||||
--surface: #fbfcfd;
|
||||
--surface-elevated: rgba(255, 255, 255, 0.82);
|
||||
--surface-control: #eef2f6;
|
||||
--surface-control-hover: #e3e9f1;
|
||||
--border: #dfe5ed;
|
||||
--border-soft: rgba(148, 163, 184, 0.28);
|
||||
--text: #1f2933;
|
||||
--muted: #64717f;
|
||||
--muted-soft: #94a3b8;
|
||||
--accent: #2f6fed;
|
||||
font-family:
|
||||
"SF Pro Text",
|
||||
"SF Pro Display",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.workbench-toolbar {
|
||||
position: fixed;
|
||||
inset: 0 0 auto 0;
|
||||
height: var(--toolbar-height);
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.9)),
|
||||
var(--surface);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.platform-darwin .workbench-toolbar {
|
||||
padding-left: 82px;
|
||||
}
|
||||
|
||||
.platform-win32 .workbench-toolbar {
|
||||
padding-right: 148px;
|
||||
}
|
||||
|
||||
.nav-cluster {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
height: 30px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: rgba(241, 245, 249, 0.76);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(15, 23, 42, 0.05),
|
||||
0 1px 0 rgba(255, 255, 255, 0.78) inset;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
width: 25px;
|
||||
height: 24px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
color 150ms ease,
|
||||
transform 120ms ease,
|
||||
opacity 150ms ease;
|
||||
}
|
||||
|
||||
.nav-button:hover:not(:disabled) {
|
||||
background: var(--surface-control-hover);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.nav-button:active:not(:disabled) {
|
||||
transform: translateY(1px) scale(0.98);
|
||||
}
|
||||
|
||||
.nav-button:disabled {
|
||||
cursor: default;
|
||||
color: var(--muted-soft);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.nav-button svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: block;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.title-strip {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 30px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
min-width: 0;
|
||||
max-width: min(46vw, 620px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.host-pill {
|
||||
min-width: 0;
|
||||
max-width: 260px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-elevated);
|
||||
color: var(--muted);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.72) inset;
|
||||
}
|
||||
|
||||
.host-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
background: #22a06b;
|
||||
box-shadow: 0 0 0 3px rgba(34, 160, 107, 0.12);
|
||||
}
|
||||
|
||||
.host {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.loading-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 140ms ease;
|
||||
}
|
||||
|
||||
.loading-line.is-loading {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-line::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 38%;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||
transform: translateX(-100%);
|
||||
animation: loading-sweep 1.05s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes loading-sweep {
|
||||
to {
|
||||
transform: translateX(270%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.workbench-toolbar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.platform-darwin .workbench-toolbar {
|
||||
padding-left: 78px;
|
||||
}
|
||||
|
||||
.host-pill {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
max-width: 48vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--surface: #15191a;
|
||||
--surface-elevated: rgba(31, 37, 41, 0.82);
|
||||
--surface-control: #20272c;
|
||||
--surface-control-hover: #293139;
|
||||
--border: #293138;
|
||||
--border-soft: rgba(148, 163, 184, 0.18);
|
||||
--text: #edf2f7;
|
||||
--muted: #a6b0bb;
|
||||
--muted-soft: #66717e;
|
||||
--accent: #6aa2ff;
|
||||
}
|
||||
|
||||
.workbench-toolbar {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(24, 29, 32, 0.94), rgba(20, 24, 27, 0.92)),
|
||||
var(--surface);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset;
|
||||
}
|
||||
|
||||
.nav-cluster {
|
||||
background: rgba(30, 37, 42, 0.76);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.nav-button:hover:not(:disabled) {
|
||||
background: var(--surface-control-hover);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.host-pill {
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="${platformClass}">
|
||||
<header class="workbench-toolbar">
|
||||
<nav class="nav-cluster" aria-label="工作台导航">
|
||||
<button class="nav-button" id="backButton" type="button" title="后退" aria-label="后退" disabled>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18 9 12l6-6" /></svg>
|
||||
</button>
|
||||
<button class="nav-button" id="forwardButton" type="button" title="前进" aria-label="前进" disabled>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
|
||||
</button>
|
||||
<button class="nav-button" id="reloadButton" type="button" title="刷新" aria-label="刷新">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6v5h-5" /><path d="M4 18v-5h5" /><path d="M18.4 9A7 7 0 0 0 6.8 6.4L4 9" /><path d="M5.6 15a7 7 0 0 0 11.6 2.6L20 15" /></svg>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="title-strip">
|
||||
<strong class="page-title" id="pageTitle">${escapeHtml(title)}</strong>
|
||||
<span class="host-pill" title="${escapeHtml(initialHost)}">
|
||||
<span class="host-dot" aria-hidden="true"></span>
|
||||
<span class="host" id="hostLabel">${escapeHtml(initialHost)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="toolbar-spacer" aria-hidden="true"></div>
|
||||
<div class="loading-line" id="loadingLine" aria-hidden="true"></div>
|
||||
</header>
|
||||
<script>
|
||||
(() => {
|
||||
const bridge = window.desktopBridge && window.desktopBridge.workbenchNavigation;
|
||||
const backButton = document.getElementById('backButton');
|
||||
const forwardButton = document.getElementById('forwardButton');
|
||||
const reloadButton = document.getElementById('reloadButton');
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
const hostLabel = document.getElementById('hostLabel');
|
||||
const loadingLine = document.getElementById('loadingLine');
|
||||
|
||||
function applyState(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
backButton.disabled = !state.canGoBack;
|
||||
forwardButton.disabled = !state.canGoForward;
|
||||
pageTitle.textContent = state.title || ${JSON.stringify(title)};
|
||||
hostLabel.textContent = state.host || '';
|
||||
hostLabel.parentElement.title = state.url || state.host || '';
|
||||
loadingLine.classList.toggle('is-loading', Boolean(state.loading));
|
||||
}
|
||||
|
||||
if (!bridge) {
|
||||
reloadButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
backButton.addEventListener('click', () => {
|
||||
void bridge.goBack();
|
||||
});
|
||||
forwardButton.addEventListener('click', () => {
|
||||
void bridge.goForward();
|
||||
});
|
||||
reloadButton.addEventListener('click', () => {
|
||||
void bridge.reload();
|
||||
});
|
||||
|
||||
bridge.onStateChanged(applyState);
|
||||
void bridge.getState().then(applyState).catch(() => undefined);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
function workbenchShellDataURL(title: string, targetURL: string): string {
|
||||
return `data:text/html;charset=UTF-8,${encodeURIComponent(workbenchShellHTML(title, targetURL))}`
|
||||
}
|
||||
|
||||
function workbenchErrorDataURL(title: string, targetURL: string, message: string): string {
|
||||
const host = hostFromURL(targetURL)
|
||||
const html = `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${escapeHtml(title)} - 加载失败</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family:
|
||||
"SF Pro Text",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background: #f6f8fb;
|
||||
color: #17202a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
main {
|
||||
width: min(620px, 100%);
|
||||
padding: 26px;
|
||||
border: 1px solid #dbe3ec;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
p {
|
||||
margin: 12px 0 0;
|
||||
color: #536170;
|
||||
line-height: 1.7;
|
||||
}
|
||||
code {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
padding: 11px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root,
|
||||
body {
|
||||
background: #111827;
|
||||
color: #f8fafc;
|
||||
}
|
||||
main {
|
||||
border-color: #2f3948;
|
||||
background: #182130;
|
||||
box-shadow: none;
|
||||
}
|
||||
p {
|
||||
color: #b6bfcb;
|
||||
}
|
||||
code {
|
||||
background: #111827;
|
||||
color: #d8e0ea;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>${escapeHtml(title)}加载失败</h1>
|
||||
<p>远程工作台暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p>
|
||||
<code>${escapeHtml(host || targetURL)}
|
||||
|
||||
${escapeHtml(message)}</code>
|
||||
</main>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`
|
||||
}
|
||||
|
||||
function stateForRecord(record: WorkbenchWindowRecord): WorkbenchNavigationState {
|
||||
const contents = record.view.webContents
|
||||
const currentURL = contents.isDestroyed() ? '' : contents.getURL()
|
||||
const displayURL = isLocalWorkbenchPage(currentURL) ? record.activeTargetURL : currentURL
|
||||
return {
|
||||
canGoBack: !contents.isDestroyed() && contents.canGoBack(),
|
||||
canGoForward: !contents.isDestroyed() && contents.canGoForward(),
|
||||
loading: record.loading || (!contents.isDestroyed() && contents.isLoading()),
|
||||
url: displayURL,
|
||||
title: record.pageTitle || record.title,
|
||||
host: hostFromURL(displayURL),
|
||||
secure: displayURL.startsWith('https://'),
|
||||
}
|
||||
}
|
||||
|
||||
function sendNavigationState(record: WorkbenchWindowRecord): void {
|
||||
if (record.window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
record.window.webContents.send('desktop:workbench-navigation-state', stateForRecord(record))
|
||||
}
|
||||
|
||||
function getRecordForSender(sender: WebContents): WorkbenchWindowRecord | null {
|
||||
const record = workbenchWindows.get(sender.id)
|
||||
if (!record || record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
function defaultNavigationState(): WorkbenchNavigationState {
|
||||
return {
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loading: false,
|
||||
url: '',
|
||||
title: '',
|
||||
host: '',
|
||||
secure: false,
|
||||
}
|
||||
}
|
||||
|
||||
function registerWorkbenchNavigationHandlers(): void {
|
||||
if (handlersRegistered) {
|
||||
return
|
||||
}
|
||||
handlersRegistered = true
|
||||
|
||||
ipcMain.handle('desktop:workbench-navigation:get-state', (event) => {
|
||||
const record = getRecordForSender(event.sender)
|
||||
return record ? stateForRecord(record) : defaultNavigationState()
|
||||
})
|
||||
|
||||
ipcMain.handle('desktop:workbench-navigation:go-back', (event) => {
|
||||
const record = getRecordForSender(event.sender)
|
||||
if (record?.view.webContents.canGoBack()) {
|
||||
record.view.webContents.goBack()
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('desktop:workbench-navigation:go-forward', (event) => {
|
||||
const record = getRecordForSender(event.sender)
|
||||
if (record?.view.webContents.canGoForward()) {
|
||||
record.view.webContents.goForward()
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('desktop:workbench-navigation:reload', (event) => {
|
||||
const record = getRecordForSender(event.sender)
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentURL = record.view.webContents.getURL()
|
||||
if (isLocalWorkbenchPage(currentURL)) {
|
||||
void navigateWorkbenchRemote(record, record.activeTargetURL || record.initialURL)
|
||||
} else {
|
||||
record.view.webContents.reload()
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
function layoutWorkbenchView(record: WorkbenchWindowRecord): void {
|
||||
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const [width, height] = record.window.getContentSize()
|
||||
record.view.setBounds({
|
||||
x: 0,
|
||||
y: TOOLBAR_HEIGHT,
|
||||
width: Math.max(0, width),
|
||||
height: Math.max(0, height - TOOLBAR_HEIGHT),
|
||||
})
|
||||
}
|
||||
|
||||
function attachWorkbenchViewEvents(record: WorkbenchWindowRecord): void {
|
||||
const { webContents } = record.view
|
||||
|
||||
webContents.on('did-start-loading', () => {
|
||||
record.loading = true
|
||||
sendNavigationState(record)
|
||||
})
|
||||
|
||||
webContents.on('did-stop-loading', () => {
|
||||
record.loading = false
|
||||
sendNavigationState(record)
|
||||
})
|
||||
|
||||
webContents.on('did-start-navigation', (_event, url, isInPlace, isMainFrame) => {
|
||||
if (isMainFrame && isRemoteHTTPURL(url)) {
|
||||
record.activeTargetURL = url
|
||||
record.loading = !isInPlace
|
||||
sendNavigationState(record)
|
||||
}
|
||||
})
|
||||
|
||||
webContents.on('did-navigate', (_event, url) => {
|
||||
if (isRemoteHTTPURL(url)) {
|
||||
record.activeTargetURL = url
|
||||
}
|
||||
sendNavigationState(record)
|
||||
})
|
||||
|
||||
webContents.on('did-navigate-in-page', (_event, url) => {
|
||||
if (isRemoteHTTPURL(url)) {
|
||||
record.activeTargetURL = url
|
||||
}
|
||||
sendNavigationState(record)
|
||||
})
|
||||
|
||||
webContents.on('page-title-updated', (_event, pageTitle) => {
|
||||
record.pageTitle = pageTitle || record.title
|
||||
sendNavigationState(record)
|
||||
})
|
||||
|
||||
webContents.on(
|
||||
'did-fail-load',
|
||||
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (!isMainFrame || errorCode === -3 || errorCode === -20) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetURL = validatedURL || record.activeTargetURL || record.initialURL
|
||||
console.warn('[desktop-workbench] workbench page load failed', {
|
||||
title: record.title,
|
||||
targetURL,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
})
|
||||
void showWorkbenchError(record, targetURL, `${errorCode}: ${errorDescription}`)
|
||||
},
|
||||
)
|
||||
|
||||
webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (isRemoteHTTPURL(url)) {
|
||||
setImmediate(() => {
|
||||
void navigateWorkbenchRemote(record, url)
|
||||
})
|
||||
}
|
||||
return { action: 'deny' }
|
||||
})
|
||||
}
|
||||
|
||||
async function showWorkbenchError(
|
||||
record: WorkbenchWindowRecord,
|
||||
targetURL: string,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
record.loading = false
|
||||
record.activeTargetURL = targetURL
|
||||
record.pageTitle = `${record.title}加载失败`
|
||||
await record.view.webContents.loadURL(workbenchErrorDataURL(record.title, targetURL, message))
|
||||
sendNavigationState(record)
|
||||
}
|
||||
|
||||
async function navigateWorkbenchRemote(
|
||||
record: WorkbenchWindowRecord,
|
||||
targetURL: string,
|
||||
): Promise<void> {
|
||||
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
record.activeTargetURL = targetURL
|
||||
record.loading = true
|
||||
sendNavigationState(record)
|
||||
|
||||
try {
|
||||
await record.view.webContents.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (/ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i.test(message)) {
|
||||
return
|
||||
}
|
||||
|
||||
await showWorkbenchError(record, targetURL, message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise<BrowserWindow> {
|
||||
registerWorkbenchNavigationHandlers()
|
||||
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1320,
|
||||
height: 900,
|
||||
minWidth: 1100,
|
||||
minHeight: 760,
|
||||
title: options.title,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: titleBarStyle(),
|
||||
titleBarOverlay: titleBarOverlay(),
|
||||
trafficLightPosition: process.platform === 'darwin' ? { x: 14, y: 16 } : undefined,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#15191a' : '#fbfcfd',
|
||||
webPreferences: {
|
||||
preload: preloadPath(),
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
const view = new WebContentsView({
|
||||
webPreferences: {
|
||||
session: options.session,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
|
||||
view.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||
window.contentView.addChildView(view)
|
||||
|
||||
const record: WorkbenchWindowRecord = {
|
||||
shellContentsId: window.webContents.id,
|
||||
window,
|
||||
view,
|
||||
title: options.title,
|
||||
initialURL: options.targetURL,
|
||||
activeTargetURL: options.targetURL,
|
||||
pageTitle: options.title,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
workbenchWindows.set(record.shellContentsId, record)
|
||||
attachWorkbenchViewEvents(record)
|
||||
|
||||
window.on('resize', () => {
|
||||
layoutWorkbenchView(record)
|
||||
})
|
||||
window.on('closed', () => {
|
||||
workbenchWindows.delete(record.shellContentsId)
|
||||
if (!record.view.webContents.isDestroyed()) {
|
||||
record.view.webContents.close()
|
||||
}
|
||||
})
|
||||
|
||||
await window.webContents.loadURL(workbenchShellDataURL(options.title, options.targetURL))
|
||||
layoutWorkbenchView(record)
|
||||
sendNavigationState(record)
|
||||
void navigateWorkbenchRemote(record, options.targetURL)
|
||||
|
||||
return window
|
||||
}
|
||||
@@ -38,6 +38,16 @@ interface DesktopLoginCredentials {
|
||||
password: string
|
||||
}
|
||||
|
||||
interface WorkbenchNavigationState {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
loading: boolean
|
||||
url: string
|
||||
title: string
|
||||
host: string
|
||||
secure: boolean
|
||||
}
|
||||
|
||||
const rendererProxyRequestChannel = 'desktop:renderer-devtools-proxy:request'
|
||||
const rendererProxyResponseChannel = 'desktop:renderer-devtools-proxy:response'
|
||||
|
||||
@@ -196,6 +206,25 @@ const desktopBridge = {
|
||||
}
|
||||
},
|
||||
},
|
||||
workbenchNavigation: {
|
||||
getState: () =>
|
||||
ipcRenderer.invoke(
|
||||
'desktop:workbench-navigation:get-state',
|
||||
) as Promise<WorkbenchNavigationState>,
|
||||
goBack: () => ipcRenderer.invoke('desktop:workbench-navigation:go-back') as Promise<null>,
|
||||
goForward: () =>
|
||||
ipcRenderer.invoke('desktop:workbench-navigation:go-forward') as Promise<null>,
|
||||
reload: () => ipcRenderer.invoke('desktop:workbench-navigation:reload') as Promise<null>,
|
||||
onStateChanged: (listener: (state: WorkbenchNavigationState) => void) => {
|
||||
const wrapped = (_event: IpcRendererEvent, payload: WorkbenchNavigationState) => {
|
||||
listener(payload)
|
||||
}
|
||||
ipcRenderer.on('desktop:workbench-navigation-state', wrapped)
|
||||
return () => {
|
||||
ipcRenderer.off('desktop:workbench-navigation-state', wrapped)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('desktopBridge', desktopBridge)
|
||||
|
||||
+17
@@ -22,6 +22,16 @@ declare global {
|
||||
password: string
|
||||
}
|
||||
|
||||
interface WorkbenchNavigationState {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
loading: boolean
|
||||
url: string
|
||||
title: string
|
||||
host: string
|
||||
secure: boolean
|
||||
}
|
||||
|
||||
interface Window {
|
||||
desktopBridge: {
|
||||
app: {
|
||||
@@ -85,6 +95,13 @@ declare global {
|
||||
): () => void
|
||||
onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void
|
||||
}
|
||||
workbenchNavigation: {
|
||||
getState(): Promise<WorkbenchNavigationState>
|
||||
goBack(): Promise<null>
|
||||
goForward(): Promise<null>
|
||||
reload(): Promise<null>
|
||||
onStateChanged(listener: (state: WorkbenchNavigationState) => void): () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -151,14 +151,14 @@ qdrant:
|
||||
timeout: 15s
|
||||
|
||||
object_storage:
|
||||
provider: minio
|
||||
endpoint: minio:9000
|
||||
access_key: minioadmin
|
||||
secret_key: minioadmin
|
||||
bucket: geo-private
|
||||
use_ssl: false
|
||||
provider: aliyun
|
||||
endpoint: https://oss-cn-shanghai.aliyuncs.com
|
||||
access_key: LTAI5tAEj8euR8B1gXzMoG84
|
||||
secret_key: lJPrKo9ViyHJE8UWIMmMCY0B6Je8v2
|
||||
bucket: shengxintui
|
||||
use_ssl: true
|
||||
public_base_url: ""
|
||||
region: ""
|
||||
region: cn-shanghai
|
||||
|
||||
cache:
|
||||
driver: redis
|
||||
|
||||
Reference in New Issue
Block a user