You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
707 lines
25 KiB
707 lines
25 KiB
<!doctype html>
|
|
<!--
|
|
dsh-launcher UI redesign — high-fidelity prototype.
|
|
Drop-in replacement for /frontend/dist/ in the Wails app (1040×1280, dark mode).
|
|
Bound methods: CheckEnv / Start / Stop / ShowWindow / HideWindow / GetStatus.
|
|
All buttons are wired with onclick stubs that match the real Wails bindings.
|
|
-->
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>dsh-launcher</title>
|
|
<style>
|
|
/* ─── tokens — dark modern-minimal, bound verbatim from direction lib ─── */
|
|
:root {
|
|
--bg: oklch(17% 0.012 260);
|
|
--surface: oklch(21% 0.013 260);
|
|
--fg: oklch(96% 0.005 260);
|
|
--muted: oklch(66% 0.012 260);
|
|
--border: oklch(28% 0.012 260);
|
|
--accent: oklch(72% 0.14 220); /* cool cyan — dev-tool feel */
|
|
|
|
--accent-soft: color-mix(in oklch, var(--accent) 16%, transparent);
|
|
--accent-fg-on: oklch(15% 0.02 220);
|
|
--fg-soft: color-mix(in oklch, var(--fg) 5%, transparent);
|
|
|
|
/* status hues — derived, not new raw colors */
|
|
--ok: oklch(74% 0.14 155); /* green */
|
|
--warn: oklch(78% 0.14 75); /* amber */
|
|
--err: oklch(68% 0.18 25); /* red */
|
|
--ok-soft: color-mix(in oklch, var(--ok) 14%, transparent);
|
|
--warn-soft: color-mix(in oklch, var(--warn) 14%, transparent);
|
|
--err-soft: color-mix(in oklch, var(--err) 14%, transparent);
|
|
|
|
/* type — system-first (no Google fonts), mono for numerics */
|
|
--font-display: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
|
|
--font-body: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
|
--font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace;
|
|
|
|
/* scale — desktop window, not marketing page */
|
|
--fs-h1: 22px;
|
|
--fs-h2: 14px;
|
|
--fs-h3: 13px;
|
|
--fs-body: 14px;
|
|
--fs-meta: 12px;
|
|
--fs-mono: 12px;
|
|
|
|
/* spacing — tight on purpose, this is a tool not a brochure */
|
|
--gap-xs: 6px;
|
|
--gap-sm: 10px;
|
|
--gap-md: 16px;
|
|
--gap-lg: 24px;
|
|
--gap-xl: 36px;
|
|
|
|
--radius: 8px;
|
|
--radius-lg: 12px;
|
|
|
|
--win-w: 1040px;
|
|
--win-h: 1280px;
|
|
}
|
|
|
|
/* ─── reset & base ─────────────────────────────────────────────── */
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html { -webkit-text-size-adjust: 100%; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: var(--font-body);
|
|
font-size: var(--fs-body);
|
|
line-height: 1.5;
|
|
text-rendering: optimizeLegibility;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
button { font: inherit; cursor: pointer; border: 0; background: none; color: inherit; }
|
|
button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
|
|
p { margin: 0; }
|
|
|
|
/* ─── shell — Wails window frame ──────────────────────────────── */
|
|
.shell {
|
|
max-width: var(--win-w);
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* draggable region for Wails macOS titlebar */
|
|
-webkit-app-region: drag;
|
|
}
|
|
.shell > * { -webkit-app-region: no-drag; }
|
|
|
|
/* ─── titlebar (macOS) ────────────────────────────────────────── */
|
|
.titlebar {
|
|
height: 38px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: color-mix(in oklch, var(--bg) 90%, transparent);
|
|
-webkit-app-region: drag;
|
|
position: relative;
|
|
}
|
|
.titlebar-title {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
letter-spacing: 0.04em;
|
|
color: var(--muted);
|
|
}
|
|
.titlebar-actions {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex;
|
|
gap: 4px;
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
.titlebar-btn {
|
|
width: 28px;
|
|
height: 22px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 6px;
|
|
color: var(--muted);
|
|
transition: background 0.15s ease, color 0.15s ease;
|
|
}
|
|
.titlebar-btn:hover { background: var(--fg-soft); color: var(--fg); }
|
|
.titlebar-btn svg { width: 12px; height: 12px; }
|
|
|
|
/* ─── main content layout ─────────────────────────────────────── */
|
|
.main {
|
|
padding: 28px 36px 40px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 28px;
|
|
}
|
|
|
|
/* ─── hero — "what this app does" + big start button ─────────── */
|
|
.hero {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
align-items: center;
|
|
column-gap: 28px;
|
|
padding: 22px 28px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.hero-fish {
|
|
width: 96px;
|
|
height: 96px;
|
|
border-radius: 16px;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
/* 源图 256×256 → 96px 显示,浏览器缩小渲染比原生 96 清晰 */
|
|
image-rendering: -webkit-optimize-contrast;
|
|
box-shadow: 0 0 0 1px var(--border), 0 4px 16px -8px var(--accent-soft);
|
|
}
|
|
/* subtle accent glow on the active card */
|
|
.hero::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(60% 120% at 100% 0%, var(--accent-soft), transparent 60%);
|
|
pointer-events: none;
|
|
}
|
|
.hero > * { position: relative; }
|
|
.hero-text { display: flex; flex-direction: column; gap: 8px; min-width: 0; justify-self: start; margin-left: 32px; }
|
|
.hero-eyebrow {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
}
|
|
.hero h1 {
|
|
font-family: var(--font-display);
|
|
font-size: var(--fs-h1);
|
|
line-height: 1.25;
|
|
color: var(--fg);
|
|
}
|
|
.hero-sub {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* ─── status pill (idle / live) ───────────────────────────────── */
|
|
.status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 5px 10px 5px 8px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.04em;
|
|
color: var(--muted);
|
|
width: fit-content;
|
|
}
|
|
.status .dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
}
|
|
.status[data-state="idle"] .dot { background: var(--muted); }
|
|
.status[data-state="running"] .dot {
|
|
background: var(--ok);
|
|
box-shadow: 0 0 0 3px var(--ok-soft);
|
|
animation: pulse 2.4s ease-in-out infinite;
|
|
}
|
|
.status[data-state="error"] .dot { background: var(--err); }
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.45; }
|
|
}
|
|
|
|
/* ─── primary action button ──────────────────────────────────── */
|
|
.start-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 22px;
|
|
border-radius: var(--radius);
|
|
background: var(--accent);
|
|
color: var(--accent-fg-on);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.005em;
|
|
transition: transform 0.05s ease, background 0.15s ease, box-shadow 0.15s ease;
|
|
box-shadow: 0 0 0 1px var(--accent), 0 6px 24px -8px var(--accent-soft);
|
|
white-space: nowrap;
|
|
}
|
|
.start-btn:hover { background: color-mix(in oklch, var(--accent) 90%, white); }
|
|
.start-btn:active { transform: translateY(1px); }
|
|
.start-btn[disabled] {
|
|
background: var(--surface);
|
|
color: var(--muted);
|
|
box-shadow: 0 0 0 1px var(--border);
|
|
cursor: not-allowed;
|
|
}
|
|
.start-btn .kbd {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: color-mix(in oklch, var(--accent-fg-on) 18%, transparent);
|
|
}
|
|
.start-btn .icon { width: 16px; height: 16px; }
|
|
|
|
.stop-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 22px;
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
color: var(--fg);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
border: 1px solid var(--border);
|
|
transition: border-color 0.15s ease, color 0.15s ease;
|
|
}
|
|
.stop-btn:hover { border-color: var(--err); color: var(--err); }
|
|
.stop-btn .icon { width: 14px; height: 14px; }
|
|
|
|
.row-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
|
|
/* ─── env check panel — 2-col grid of small status rows ──────── */
|
|
.panel {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: 20px 24px;
|
|
}
|
|
.panel-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
.panel-title {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.panel-meta {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--fs-mono);
|
|
color: var(--muted);
|
|
}
|
|
.panel-action {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
transition: background 0.15s ease, color 0.15s ease;
|
|
}
|
|
.panel-action:hover { background: var(--fg-soft); color: var(--fg); }
|
|
|
|
.env-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1px;
|
|
background: var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
.env-cell {
|
|
background: var(--surface);
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
.env-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--fg-soft);
|
|
color: var(--muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.env-icon svg { width: 14px; height: 14px; }
|
|
.env-icon[data-ok="true"] { background: var(--ok-soft); color: var(--ok); }
|
|
.env-icon[data-ok="false"] { background: var(--err-soft); color: var(--err); }
|
|
|
|
.env-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
|
|
.env-name {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--fg);
|
|
}
|
|
.env-value {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--fs-mono);
|
|
color: var(--muted);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ─── issues / warnings list ──────────────────────────────────── */
|
|
.issue-list {
|
|
margin-top: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.issue {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
padding: 8px 12px;
|
|
background: var(--warn-soft);
|
|
border: 1px solid color-mix(in oklch, var(--warn) 30%, transparent);
|
|
border-radius: 8px;
|
|
font-size: 12.5px;
|
|
color: var(--fg);
|
|
line-height: 1.45;
|
|
}
|
|
.issue .icon {
|
|
flex-shrink: 0;
|
|
color: var(--warn);
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-top: 1px;
|
|
}
|
|
|
|
/* ─── session info (when running) ─────────────────────────────── */
|
|
.session {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 1px;
|
|
background: var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
.session-cell {
|
|
background: var(--surface);
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.session-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.session-value {
|
|
font-family: var(--font-mono);
|
|
font-size: 14px;
|
|
color: var(--fg);
|
|
font-variant-numeric: tabular-nums;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ─── log / output panel ─────────────────────────────────────── */
|
|
.log {
|
|
background: oklch(13% 0.012 260);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
}
|
|
.log-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--surface);
|
|
}
|
|
.log-head .panel-title { margin: 0; }
|
|
.log-head-actions { display: flex; gap: 4px; }
|
|
.log-head-actions button {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 5px;
|
|
color: var(--muted);
|
|
display: grid;
|
|
place-items: center;
|
|
transition: background 0.15s ease, color 0.15s ease;
|
|
}
|
|
.log-head-actions button:hover { background: var(--fg-soft); color: var(--fg); }
|
|
.log-head-actions svg { width: 12px; height: 12px; }
|
|
|
|
.log-body {
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
padding: 12px 16px;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--fs-mono);
|
|
line-height: 1.65;
|
|
color: oklch(82% 0.008 260);
|
|
}
|
|
.log-body::-webkit-scrollbar { width: 8px; }
|
|
.log-body::-webkit-scrollbar-track { background: transparent; }
|
|
.log-body::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 4px;
|
|
}
|
|
.log-line {
|
|
display: flex;
|
|
gap: 10px;
|
|
padding: 2px 0;
|
|
}
|
|
.log-ts {
|
|
color: var(--muted);
|
|
flex-shrink: 0;
|
|
opacity: 0.6;
|
|
}
|
|
.log-msg { flex: 1; word-break: break-all; }
|
|
.log-line[data-kind="ok"] .log-msg { color: oklch(85% 0.12 155); }
|
|
.log-line[data-kind="err"] .log-msg { color: oklch(82% 0.16 25); }
|
|
.log-line[data-kind="info"] .log-msg { color: oklch(82% 0.08 220); }
|
|
.log-empty {
|
|
color: var(--muted);
|
|
font-style: italic;
|
|
padding: 24px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ─── footer ─────────────────────────────────────────────────── */
|
|
.foot {
|
|
padding: 16px 0 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.foot-links { display: flex; gap: 16px; }
|
|
.foot-links a { color: var(--muted); transition: color 0.15s ease; }
|
|
.foot-links a:hover { color: var(--fg); }
|
|
.foot-shortcut {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.foot-shortcut .kbd {
|
|
padding: 1px 5px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
font-size: 10.5px;
|
|
color: var(--fg);
|
|
background: var(--surface);
|
|
}
|
|
|
|
/* ─── responsive — narrow window (sidebar collapses) ─────────── */
|
|
@media (max-width: 920px) {
|
|
.main { padding: 20px 20px 32px; }
|
|
.hero { grid-template-columns: 1fr; row-gap: 18px; }
|
|
.hero-fish { justify-self: start; }
|
|
.env-grid { grid-template-columns: 1fr; }
|
|
.session { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell" data-od-id="shell">
|
|
<!-- ─── macOS titlebar ──────────────────────────────────────────── -->
|
|
<div class="titlebar" data-od-id="titlebar">
|
|
<span class="titlebar-title">dsh-launcher</span>
|
|
<div class="titlebar-actions">
|
|
<button class="titlebar-btn" aria-label="Hide window" onclick="HideWindow()">
|
|
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6h8"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── main content ────────────────────────────────────────────── -->
|
|
<div class="main" data-od-id="main">
|
|
|
|
<!-- HERO: title + status + start/stop -->
|
|
<section class="hero" data-od-id="hero">
|
|
<img class="hero-fish" src="assets/appicon.png" alt="" data-od-id="hero-fish" />
|
|
<div class="hero-text">
|
|
<span class="hero-eyebrow">本地启动器</span>
|
|
<h1>dsh web UI</h1>
|
|
<p class="hero-sub">检查环境 · 启动本地服务<br/>自动打开浏览器,启动后窗口隐藏到状态栏。</p>
|
|
<span class="status" id="status-pill" data-state="idle">
|
|
<span class="dot"></span>
|
|
<span id="status-text">未运行</span>
|
|
</span>
|
|
</div>
|
|
<div class="row-actions">
|
|
<button class="start-btn" id="btn-start" onclick="Start()">
|
|
<svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M4 3.5v9l8-4.5-8-4.5z"/></svg>
|
|
启动 dsh web
|
|
<span class="kbd">⌘ ⏎</span>
|
|
</button>
|
|
<button class="stop-btn" id="btn-stop" disabled onclick="Stop()">
|
|
<svg class="icon" viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="8" height="8" rx="1"/></svg>
|
|
停止
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ENVIRONMENT: 4 cells, dense -->
|
|
<section class="panel" data-od-id="env">
|
|
<div class="panel-head">
|
|
<span class="panel-title">环境检查</span>
|
|
<button class="panel-action" onclick="CheckEnv()">重新检查</button>
|
|
</div>
|
|
<div class="env-grid">
|
|
<div class="env-cell" data-od-id="env-node">
|
|
<div class="env-icon" data-ok="true" id="env-node-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
<path d="M12 3 4 7v10l8 4 8-4V7l-8-4z"/>
|
|
<path d="m4 7 8 4 8-4M12 11v10"/>
|
|
</svg>
|
|
</div>
|
|
<div class="env-body">
|
|
<span class="env-name">Node.js</span>
|
|
<span class="env-value" id="env-node-val">v20.14.0 · /opt/homebrew/bin/node</span>
|
|
</div>
|
|
</div>
|
|
<div class="env-cell" data-od-id="env-dsh">
|
|
<div class="env-icon" data-ok="true" id="env-dsh-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
<path d="M5 4h14M9 4v16M15 4v16M5 20h14"/>
|
|
</svg>
|
|
</div>
|
|
<div class="env-body">
|
|
<span class="env-name">dsh CLI</span>
|
|
<span class="env-value" id="env-dsh-val">0.5.2 · /opt/homebrew/bin/dsh</span>
|
|
</div>
|
|
</div>
|
|
<div class="env-cell" data-od-id="env-cred">
|
|
<div class="env-icon" data-ok="true" id="env-cred-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
<rect x="4" y="9" width="16" height="11" rx="2"/>
|
|
<path d="M8 9V6a4 4 0 0 1 8 0v3"/>
|
|
</svg>
|
|
</div>
|
|
<div class="env-body">
|
|
<span class="env-name">登录凭证</span>
|
|
<span class="env-value" id="env-cred-val">~/.dsh/.credentials.yaml</span>
|
|
</div>
|
|
</div>
|
|
<div class="env-cell" data-od-id="env-port">
|
|
<div class="env-icon" data-ok="true" id="env-port-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
<path d="M4 17h16M4 17v-3a8 8 0 0 1 16 0v3"/>
|
|
<circle cx="12" cy="17" r="1.5" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
<div class="env-body">
|
|
<span class="env-name">端口 3080</span>
|
|
<span class="env-value" id="env-port-val">空闲</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- issues only show when present (hidden by default) -->
|
|
<div class="issue-list" id="issue-list" hidden></div>
|
|
</section>
|
|
|
|
<!-- SESSION: only when running -->
|
|
<section class="panel" data-od-id="session" id="session-panel" hidden>
|
|
<div class="panel-head">
|
|
<span class="panel-title">当前会话</span>
|
|
<span class="panel-meta" id="session-meta">运行中</span>
|
|
</div>
|
|
<div class="session">
|
|
<div class="session-cell">
|
|
<span class="session-label">URL</span>
|
|
<span class="session-value" id="session-url">http://127.0.0.1:3080/?token=…</span>
|
|
</div>
|
|
<div class="session-cell">
|
|
<span class="session-label">PID</span>
|
|
<span class="session-value" id="session-pid">—</span>
|
|
</div>
|
|
<div class="session-cell">
|
|
<span class="session-label">启动于</span>
|
|
<span class="session-value" id="session-started">—</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- LOG: real-time output -->
|
|
<section class="log" data-od-id="log">
|
|
<div class="log-head">
|
|
<span class="panel-title">输出日志</span>
|
|
<div class="log-head-actions">
|
|
<button aria-label="清空日志" onclick="clearLog()">
|
|
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4">
|
|
<path d="M3 4h6m-5 0V3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1M4 4l.5 6a1 1 0 0 0 1 .9h1a1 1 0 0 0 1-.9L8 4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="log-body" id="log-body">
|
|
<div class="log-line" data-kind="info">
|
|
<span class="log-ts">12:42:08</span>
|
|
<span class="log-msg">环境检查通过 · node v20.14.0 · dsh 0.5.2 · 端口 3080 空闲</span>
|
|
</div>
|
|
<div class="log-empty" id="log-empty" hidden>暂无输出</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="foot" data-od-id="footer">
|
|
<span>dsh-launcher · local-only</span>
|
|
<div class="foot-links">
|
|
<span class="foot-shortcut">退出 <span class="kbd">⌘ Q</span></span>
|
|
<a href="#" onclick="event.preventDefault(); ShowWindow()">显示窗口</a>
|
|
<a href="#" onclick="event.preventDefault(); HideWindow()">隐藏窗口</a>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/* ─── Wails-bound stubs (real app replaces these) ─────────────── */
|
|
function CheckEnv() { /* wails: App.CheckEnv() */ }
|
|
function Start() {
|
|
/* wails: App.Start() — populates session panel + log */
|
|
document.getElementById('status-pill').setAttribute('data-state', 'running');
|
|
document.getElementById('status-text').textContent = '启动中…';
|
|
appendLog('info', '执行 dsh --profile web --no-open');
|
|
}
|
|
function Stop() { /* wails: App.Stop() */ }
|
|
function ShowWindow() { /* wails: App.ShowWindow() */ }
|
|
function HideWindow() { /* wails: App.HideWindow() */ }
|
|
function GetStatus() { /* wails: App.GetStatus() */ }
|
|
|
|
/* ─── log helper (demo only) ──────────────────────────────────── */
|
|
function appendLog(kind, msg) {
|
|
const body = document.getElementById('log-body');
|
|
const empty = document.getElementById('log-empty');
|
|
if (empty) empty.hidden = true;
|
|
const ts = new Date().toTimeString().slice(0, 8);
|
|
const line = document.createElement('div');
|
|
line.className = 'log-line';
|
|
line.setAttribute('data-kind', kind);
|
|
line.innerHTML = '<span class="log-ts">' + ts + '</span><span class="log-msg"></span>';
|
|
line.querySelector('.log-msg').textContent = msg;
|
|
body.appendChild(line);
|
|
body.scrollTop = body.scrollHeight;
|
|
}
|
|
function clearLog() {
|
|
const body = document.getElementById('log-body');
|
|
body.innerHTML = '<div class="log-empty" id="log-empty">暂无输出</div>';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |