chore: initial commit
- 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)
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user