- Wails v2 + Go 1.21+ + Vue 3 dark UI - 3 buttons: Check env (auto-install dsh) / Start dsh web / Stop - 升级 / 检查版本 一键按钮 - 启动后窗口自动最小化到 Dock - 顶部 logo 鼠标 hover 显示大图预览 - scripts/: build-mac.sh, build-windows.bat, install-app.sh - ui-design/: 视觉设计稿 (HTML + tokens)
707 lines
25 KiB
HTML
707 lines
25 KiB
HTML
<!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> |