chore: sync
This commit is contained in:
38
AGENTS.md
Normal file
38
AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- `mengyaconnect-backend/` Go (Gin) HTTP + WebSocket SSH bridge. Most logic in `main.go`; config helpers in `config.go`. Persistent data under `data/` (`data/ssh/*.json`, `data/command/command.json`, `data/script/`).
|
||||||
|
- `mengyaconnect-frontend/` Vite + Vue 3 SPA. Entry at `index.html`; UI in `src/App.vue`; build output in `dist/`.
|
||||||
|
- `debug-logs/` local logs, not part of app runtime.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
Backend:
|
||||||
|
- `cd mengyaconnect-backend && go run .` Run API server (default `:8080`).
|
||||||
|
- `go build -o mengyaconnect-backend` Build local binary.
|
||||||
|
- `go fmt ./...` Format Go code.
|
||||||
|
- `go test ./...` Run Go tests (if present).
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
- `cd mengyaconnect-frontend && npm install` Install dependencies.
|
||||||
|
- `npm run dev` Start Vite dev server (`http://localhost:5173`).
|
||||||
|
- `npm run build` Build production assets to `dist/`.
|
||||||
|
- `npm run preview` Preview the production build.
|
||||||
|
|
||||||
|
Optional container:
|
||||||
|
- `cd mengyaconnect-backend && docker-compose up --build` Run backend in Docker (host `2431` -> container `8080`).
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Go: follow `gofmt` output; prefer `camelCase` for unexported identifiers and `PascalCase` for exported ones.
|
||||||
|
- Vue: keep 2-space indentation, double quotes, and semicolons as used in `src/App.vue`. Use `<script setup>` for new components and keep UI code in `mengyaconnect-frontend/src/`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Backend uses Go’s standard testing (`*_test.go`). No dedicated lint/test tooling beyond `go test`.
|
||||||
|
- Frontend has no test runner configured; if you add one, add an npm script and document it here.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Commit history is minimal; one commit uses Conventional Commits (`chore: ...`). Prefer `type: short summary` for consistency and keep commits scoped.
|
||||||
|
- PRs should include: a clear summary, testing notes (commands run), and screenshots for UI changes. Call out any changes to env/config or persisted data.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
- Do not commit real SSH credentials or production data in `mengyaconnect-backend/data/`; sanitize sample files.
|
||||||
|
- Key env vars: backend `PORT`, `DATA_DIR`, `ALLOWED_ORIGINS`; frontend `VITE_WS_URL`/`VITE_WS_PORT`.
|
||||||
5
mengyaconnect-frontend/.env.development
Normal file
5
mengyaconnect-frontend/.env.development
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 本地开发:前端 5173 → Vite proxy → 后端 8080
|
||||||
|
# 留空即走同源代理(vite.config.js 里已配置 /api proxy)
|
||||||
|
# 如果你想跳过代理直连,取消注释下面两行
|
||||||
|
# VITE_API_BASE=http://localhost:8080/api
|
||||||
|
# VITE_WS_URL=ws://localhost:8080/api/ws/ssh
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
VITE_API_BASE=https://ssh.api.shumengya.top/api
|
VITE_API_BASE=https://ssh.api.shumengya.top/api
|
||||||
VITE_WS_URL=wss://ssh.api.shumengya.top/api/ws/ssh
|
VITE_WS_URL=wss://ssh.api.shumengya.top/api/ws/ssh
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,285 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>萌芽SSH</title>
|
<title>萌芽SSH</title>
|
||||||
<meta name="description" content="柔和渐变风格的 Web SSH 连接面板,支持多窗口终端。" />
|
<meta name="description" content="柔和渐变风格的 Web SSH 连接面板,支持多窗口终端。" />
|
||||||
|
<meta name="theme-color" content="#0f172a" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/logo192.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="192x192" href="/logo192.png" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--splash-bg: radial-gradient(
|
||||||
|
circle at top,
|
||||||
|
#1f2937 0,
|
||||||
|
#020617 55%,
|
||||||
|
#000 100%
|
||||||
|
);
|
||||||
|
--splash-ink: #e2e8f0;
|
||||||
|
--splash-accent: #22c55e;
|
||||||
|
--splash-accent-soft: rgba(34, 197, 94, 0.35);
|
||||||
|
--splash-glow: rgba(56, 189, 248, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--splash-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0.45s ease, visibility 0.45s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -20%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 20% 20%,
|
||||||
|
rgba(34, 197, 94, 0.18),
|
||||||
|
transparent 55%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 10%,
|
||||||
|
rgba(56, 189, 248, 0.2),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 70% 80%,
|
||||||
|
rgba(14, 116, 144, 0.25),
|
||||||
|
transparent 65%
|
||||||
|
);
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: splashPulse 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 70vmax;
|
||||||
|
height: 70vmax;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
var(--splash-glow),
|
||||||
|
transparent 65%
|
||||||
|
);
|
||||||
|
animation: splashGlow 7.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--splash-ink);
|
||||||
|
font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-logo img {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.45),
|
||||||
|
0 0 30px rgba(34, 197, 94, 0.25);
|
||||||
|
animation: logoFloat 3.6s ease-in-out infinite;
|
||||||
|
background: rgba(15, 23, 42, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-ring {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--splash-accent-soft);
|
||||||
|
animation: ringPulse 3.6s ease-out infinite;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-ring.ring-1 {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-ring.ring-2 {
|
||||||
|
width: 170px;
|
||||||
|
height: 170px;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-ring.ring-3 {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
animation-delay: 2.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86efac;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--splash-accent);
|
||||||
|
box-shadow: 0 0 12px rgba(34, 197, 94, 0.55);
|
||||||
|
animation: dotBounce 1.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-dots span:nth-child(2) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-dots span:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen.is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.splash-active #app {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logoFloat {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ringPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.65);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dotBounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.7);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes splashPulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.75;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes splashGlow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: translate(-10%, -5%) scale(0.95);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translate(10%, 5%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#splash-screen::before,
|
||||||
|
#splash-screen::after,
|
||||||
|
.splash-logo img,
|
||||||
|
.splash-ring,
|
||||||
|
.splash-dots span {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="splash-active">
|
||||||
|
<div id="splash-screen" aria-live="polite">
|
||||||
|
<div class="splash-content">
|
||||||
|
<div class="splash-logo">
|
||||||
|
<span class="splash-ring ring-1"></span>
|
||||||
|
<span class="splash-ring ring-2"></span>
|
||||||
|
<span class="splash-ring ring-3"></span>
|
||||||
|
<img src="/logo192.png" alt="萌芽SSH Logo" />
|
||||||
|
</div>
|
||||||
|
<div class="splash-title">萌芽SSH</div>
|
||||||
|
<div class="splash-subtitle">加载中</div>
|
||||||
|
<div class="splash-dots" aria-hidden="true">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
window.__hideSplash = () => {
|
||||||
|
const splash = document.getElementById("splash-screen");
|
||||||
|
if (!splash) return;
|
||||||
|
splash.classList.add("is-hidden");
|
||||||
|
document.body.classList.remove("splash-active");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
splash.remove();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4919
mengyaconnect-frontend/package-lock.json
generated
4919
mengyaconnect-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.2",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"vite": "^5.4.10"
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-pwa": "^0.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
mengyaconnect-frontend/public/favicon.ico
Normal file
BIN
mengyaconnect-frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
mengyaconnect-frontend/public/logo.png
Normal file
BIN
mengyaconnect-frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
BIN
mengyaconnect-frontend/public/logo192.png
Normal file
BIN
mengyaconnect-frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
mengyaconnect-frontend/public/logo512.png
Normal file
BIN
mengyaconnect-frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
mengyaconnect-frontend/public/rounded-image (2).png
Normal file
BIN
mengyaconnect-frontend/public/rounded-image (2).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
@@ -41,6 +41,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div v-if="notice.text" class="global-notice" :class="notice.type">
|
||||||
|
{{ notice.text }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 全局专注模式小按钮,始终固定在角落 -->
|
<!-- 全局专注模式小按钮,始终固定在角落 -->
|
||||||
<button class="focus-floating" type="button" @click="toggleFocus">
|
<button class="focus-floating" type="button" @click="toggleFocus">
|
||||||
{{ focusMode ? "□" : "👁" }}
|
{{ focusMode ? "□" : "👁" }}
|
||||||
@@ -1175,6 +1179,114 @@ function toggleFocus() {
|
|||||||
focusMode.value = !focusMode.value;
|
focusMode.value = !focusMode.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notice = reactive({
|
||||||
|
text: "",
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
let noticeTimer = null;
|
||||||
|
|
||||||
|
function showNotice(text, type = "info", duration = 3200) {
|
||||||
|
notice.text = text;
|
||||||
|
notice.type = type;
|
||||||
|
if (noticeTimer) {
|
||||||
|
clearTimeout(noticeTimer);
|
||||||
|
}
|
||||||
|
noticeTimer = setTimeout(() => {
|
||||||
|
notice.text = "";
|
||||||
|
noticeTimer = null;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditableTarget(target) {
|
||||||
|
if (!target || typeof target !== "object") return false;
|
||||||
|
const tag = target.tagName?.toLowerCase();
|
||||||
|
if (tag === "input" || tag === "textarea") return true;
|
||||||
|
return !!target.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToBase64(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkBase64(text, size = 76) {
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < text.length; i += size) {
|
||||||
|
chunks.push(text.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return chunks.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeFilename(name) {
|
||||||
|
return name.replace(/[^a-zA-Z0-9._-]/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(date = new Date()) {
|
||||||
|
const pad = (v) => String(v).padStart(2, "0");
|
||||||
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate()
|
||||||
|
)}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendImageToTerminal(file) {
|
||||||
|
const session = sessions.value.find((item) => item.id === activeId.value);
|
||||||
|
if (!session || !session.ws || session.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
showNotice("已识别剪贴板图片,但当前没有可用终端连接", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxBytes = 2 * 1024 * 1024;
|
||||||
|
if (file.size > maxBytes) {
|
||||||
|
showNotice("图片过大,已取消粘贴(限制 2MB)", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ext = (file.type || "image/png").split("/")[1] || "png";
|
||||||
|
const filename = safeFilename(`clipboard-${formatTimestamp()}.${ext}`);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
const base64 = bytesToBase64(reader.result);
|
||||||
|
const payload = chunkBase64(base64);
|
||||||
|
const marker = `__MENGYA_CLIP_${Date.now()}__`;
|
||||||
|
const script =
|
||||||
|
`cat <<'${marker}' | base64 -d > ${filename}\n` +
|
||||||
|
`${payload}\n` +
|
||||||
|
`${marker}\n`;
|
||||||
|
session.ws.send(JSON.stringify({ type: "input", data: script }));
|
||||||
|
session.term?.writeln(
|
||||||
|
`\r\n\x1b[90m[已接收剪贴板图片,保存为 ${filename}]\x1b[0m`
|
||||||
|
);
|
||||||
|
showNotice(`已发送图片到终端:${filename}`, "success");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showNotice("图片粘贴失败,请重试", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
showNotice("读取剪贴板图片失败", "error");
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePaste(event) {
|
||||||
|
if (!event || !event.clipboardData) return;
|
||||||
|
if (isEditableTarget(event.target)) return;
|
||||||
|
const text = event.clipboardData.getData("text/plain");
|
||||||
|
if (text) return;
|
||||||
|
const items = Array.from(event.clipboardData.items || []);
|
||||||
|
const imageItem = items.find((item) => item.type?.startsWith("image/"));
|
||||||
|
if (!imageItem) return;
|
||||||
|
const file = imageItem.getAsFile();
|
||||||
|
if (!file) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
sendImageToTerminal(file);
|
||||||
|
}
|
||||||
|
|
||||||
// 根元素引用,用于动态调整高度以适配移动端虚拟键盘
|
// 根元素引用,用于动态调整高度以适配移动端虚拟键盘
|
||||||
const appRef = ref(null);
|
const appRef = ref(null);
|
||||||
let viewportFitTimer = null;
|
let viewportFitTimer = null;
|
||||||
@@ -1205,8 +1317,17 @@ function updateAppHeight() {
|
|||||||
}, 140);
|
}, 140);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideSplashScreen() {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const hide = window.__hideSplash;
|
||||||
|
if (typeof hide === "function") {
|
||||||
|
requestAnimationFrame(() => hide());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener("resize", handleWindowResize);
|
window.addEventListener("resize", handleWindowResize);
|
||||||
|
document.addEventListener("paste", handlePaste);
|
||||||
// 移动端虚拟键盘弹出时,visualViewport 会缩减,用它动态撑高度
|
// 移动端虚拟键盘弹出时,visualViewport 会缩减,用它动态撑高度
|
||||||
if (window.visualViewport) {
|
if (window.visualViewport) {
|
||||||
window.visualViewport.addEventListener("resize", updateAppHeight);
|
window.visualViewport.addEventListener("resize", updateAppHeight);
|
||||||
@@ -1216,13 +1337,14 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
updateAppHeight();
|
updateAppHeight();
|
||||||
// 尝试加载后端配置,如后端未启动仅在面板中提示错误
|
// 尝试加载后端配置,如后端未启动仅在面板中提示错误
|
||||||
loadSSH();
|
Promise.allSettled([loadSSH(), loadCommands(), loadScripts()]).finally(() => {
|
||||||
loadCommands();
|
hideSplashScreen();
|
||||||
loadScripts();
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("resize", handleWindowResize);
|
window.removeEventListener("resize", handleWindowResize);
|
||||||
|
document.removeEventListener("paste", handlePaste);
|
||||||
if (viewportFitTimer) {
|
if (viewportFitTimer) {
|
||||||
clearTimeout(viewportFitTimer);
|
clearTimeout(viewportFitTimer);
|
||||||
viewportFitTimer = null;
|
viewportFitTimer = null;
|
||||||
@@ -1255,6 +1377,36 @@ onBeforeUnmount(() => {
|
|||||||
"Segoe UI", sans-serif;
|
"Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-notice {
|
||||||
|
position: fixed;
|
||||||
|
top: 64px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: rgba(15, 23, 42, 0.92);
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.45);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-notice.success {
|
||||||
|
border-color: rgba(34, 197, 94, 0.4);
|
||||||
|
color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-notice.warning {
|
||||||
|
border-color: rgba(251, 191, 36, 0.4);
|
||||||
|
color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-notice.error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.45);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
createApp(App).mount("#app");
|
createApp(App).mount("#app");
|
||||||
|
|
||||||
|
registerSW({ immediate: true });
|
||||||
|
|||||||
@@ -1,8 +1,37 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
includeAssets: ["favicon.ico", "logo.png", "logo192.png", "logo512.png"],
|
||||||
|
manifest: {
|
||||||
|
name: "萌芽SSH",
|
||||||
|
short_name: "萌芽SSH",
|
||||||
|
description: "柔和渐变风格的 Web SSH 连接面板,支持多窗口终端。",
|
||||||
|
start_url: "/",
|
||||||
|
display: "standalone",
|
||||||
|
theme_color: "#0f172a",
|
||||||
|
background_color: "#020617",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "logo192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "logo512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Reference in New Issue
Block a user