chore: trim repo for customer delivery
This commit is contained in:
3
frontend/.vscode/extensions.json
vendored
3
frontend/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
本模板用于在 Vite 中基于 Vue 3 与 TypeScript 进行开发。模板使用 Vue 3 的 `<script setup>` 单文件组件,可参阅 [script setup 文档](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) 了解更多。
|
||||
|
||||
推荐的项目配置与 IDE 支持请参考 [Vue 文档 TypeScript 指南](https://vuejs.org/guide/typescript/overview.html#project-setup)。
|
||||
@@ -1,17 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="SSH Manager">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="SSH Manager">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0f172a" />
|
||||
<stop offset="100%" stop-color="#164e63" />
|
||||
<stop offset="0%" stop-color="#07152f" />
|
||||
<stop offset="100%" stop-color="#0a1d3f" />
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#22d3ee" />
|
||||
<stop offset="100%" stop-color="#67e8f9" />
|
||||
<linearGradient id="neon" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#3cc8ff" />
|
||||
<stop offset="55%" stop-color="#6ce7ff" />
|
||||
<stop offset="100%" stop-color="#7fffd4" />
|
||||
</linearGradient>
|
||||
<linearGradient id="panel" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#17345f" />
|
||||
<stop offset="100%" stop-color="#112748" />
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="5" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="2.4" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style>
|
||||
.trace { fill: none; stroke: #143b6f; stroke-linecap: round; stroke-width: 2.2; opacity: 0.75; }
|
||||
.trace2 { fill: none; stroke: #1a4f88; stroke-linecap: round; stroke-width: 1.6; opacity: 0.6; }
|
||||
.dot { fill: #1f78b4; opacity: 0.85; }
|
||||
.rack { fill: #17345f; stroke: url(#neon); stroke-width: 2.2; }
|
||||
.lightG { fill: #88ff96; }
|
||||
.lightY { fill: #ffd96b; }
|
||||
.lightR { fill: #ff7686; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="128" height="128" rx="28" fill="url(#bg)" />
|
||||
<rect x="18" y="22" width="92" height="84" rx="14" fill="#020617" stroke="#155e75" stroke-width="4" />
|
||||
<path d="M36 48 54 64 36 80" fill="none" stroke="url(#accent)" stroke-linecap="round" stroke-linejoin="round" stroke-width="10" />
|
||||
<path d="M64 82h28" fill="none" stroke="url(#accent)" stroke-linecap="round" stroke-width="10" />
|
||||
<circle cx="96" cy="36" r="8" fill="#22d3ee" opacity="0.9" />
|
||||
|
||||
<rect x="18" y="18" width="220" height="220" rx="34" fill="url(#bg)" />
|
||||
<rect x="18" y="18" width="220" height="220" rx="34" fill="none" stroke="#3cc8ff" stroke-width="4" filter="url(#glow)" />
|
||||
|
||||
<path class="trace" d="M36 62h28v-18h18v34h20" />
|
||||
<path class="trace" d="M36 96h42v18h22v-16h24" />
|
||||
<path class="trace" d="M36 160h24v22h34v-18h18" />
|
||||
<path class="trace" d="M36 196h54v-26h20" />
|
||||
<path class="trace" d="M220 58h-24v-14h-22v36h-14" />
|
||||
<path class="trace" d="M220 98h-38v14h-20v-18h-18" />
|
||||
<path class="trace" d="M220 156h-28v24h-36v-20h-16" />
|
||||
<path class="trace" d="M220 194h-42v-18h-24" />
|
||||
<circle class="dot" cx="64" cy="44" r="3" />
|
||||
<circle class="dot" cx="78" cy="114" r="3" />
|
||||
<circle class="dot" cx="60" cy="182" r="3" />
|
||||
<circle class="dot" cx="196" cy="44" r="3" />
|
||||
<circle class="dot" cx="182" cy="112" r="3" />
|
||||
<circle class="dot" cx="194" cy="180" r="3" />
|
||||
|
||||
<g filter="url(#softGlow)">
|
||||
<rect x="72" y="74" width="114" height="96" rx="12" fill="url(#panel)" stroke="#8af3ff" stroke-width="3" />
|
||||
<rect x="82" y="86" width="94" height="72" rx="10" fill="#101c35" stroke="#53d9ff" stroke-opacity="0.7" />
|
||||
<circle cx="90" cy="80" r="3.4" class="lightR" />
|
||||
<circle cx="98" cy="80" r="3.4" class="lightY" />
|
||||
<circle cx="106" cy="80" r="3.4" class="lightG" />
|
||||
<text x="94" y="103" font-size="8.5" fill="#baf7ff" font-family="IBM Plex Mono, monospace">$ ssh -i keys/mgmt.pem</text>
|
||||
<text x="94" y="114" font-size="7.8" fill="#8bcfe2" font-family="IBM Plex Mono, monospace">admin@prod-1.net</text>
|
||||
<text x="92" y="132" font-size="34" fill="#b4ffff" font-family="IBM Plex Sans, sans-serif" font-weight="700">$</text>
|
||||
<rect x="108" y="125" width="24" height="6" rx="3" fill="url(#neon)" />
|
||||
</g>
|
||||
|
||||
<g transform="translate(127 148)" filter="url(#glow)">
|
||||
<circle r="47" fill="rgba(10,29,63,0.5)" stroke="#5ce8ff" stroke-width="1.8" />
|
||||
<circle r="36" fill="none" stroke="#2fc7ff" stroke-width="1.4" stroke-opacity="0.5" />
|
||||
<circle cx="-45" cy="0" r="3.4" fill="#55efff" />
|
||||
<circle cx="45" cy="0" r="3.4" fill="#55efff" />
|
||||
<circle cx="0" cy="-45" r="3.4" fill="#55efff" />
|
||||
<circle cx="0" cy="45" r="3.4" fill="#55efff" />
|
||||
|
||||
<rect class="rack" x="-27" y="-24" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="-27" y="-10" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="-27" y="4" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="-27" y="18" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="5" y="-24" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="5" y="-10" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="5" y="4" width="22" height="10" rx="3" />
|
||||
<rect class="rack" x="5" y="18" width="22" height="10" rx="3" />
|
||||
|
||||
<circle cx="-10" cy="-19" r="1.6" class="lightG" />
|
||||
<circle cx="-14" cy="-5" r="1.6" class="lightY" />
|
||||
<circle cx="-8" cy="9" r="1.6" class="lightG" />
|
||||
<circle cx="-12" cy="23" r="1.6" class="lightR" />
|
||||
<circle cx="22" cy="-19" r="1.6" class="lightY" />
|
||||
<circle cx="18" cy="-5" r="1.6" class="lightG" />
|
||||
<circle cx="24" cy="9" r="1.6" class="lightY" />
|
||||
<circle cx="20" cy="23" r="1.6" class="lightG" />
|
||||
|
||||
<path class="trace2" d="M-16 -14v-10c0-7 5-12 12-12h8c7 0 12 5 12 12v10" />
|
||||
<path class="trace2" d="M-16 18v10c0 7 5 12 12 12h8c7 0 12-5 12-12v-10" />
|
||||
<path class="trace2" d="M-5 -14v28" />
|
||||
<path class="trace2" d="M5 -14v28" />
|
||||
<rect x="-6" y="32" width="12" height="8" rx="2.2" fill="#17345f" stroke="#79f7ff" stroke-width="1.4" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 961 B After Width: | Height: | Size: 5.1 KiB |
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { BadgeInfo, Copy, FileCode2, ShieldCheck, X } from 'lucide-vue-next'
|
||||
import { BadgeInfo, Copy, ShieldCheck, X } from 'lucide-vue-next'
|
||||
import { useProductStatusStore } from '../stores/productStatus'
|
||||
import { useConnectionsStore } from '../stores/connections'
|
||||
import { useActivityLogStore } from '../stores/activityLog'
|
||||
@@ -26,8 +26,7 @@ const appVersion = computed(() => import.meta.env.VITE_APP_VERSION || '2026.04 S
|
||||
const diagnostics = computed(() => {
|
||||
return [
|
||||
`Version: ${appVersion.value}`,
|
||||
`Delivery Mode: ${productStatusStore.licenseStatusText}`,
|
||||
`Environment Fingerprint: ${productStatusStore.machineFingerprint}`,
|
||||
`First Launch: ${formatTime(productStatusStore.firstLaunchedAt)}`,
|
||||
`Connections: ${connectionsStore.connections.length}`,
|
||||
`Transfer Runs: ${transfersStore.runs.length}`,
|
||||
`Activity Logs: ${activityLogStore.entries.length}`,
|
||||
@@ -57,16 +56,16 @@ function formatTime(ts: number) {
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-3 sm:p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="关于与交付信息"
|
||||
aria-label="关于与诊断"
|
||||
>
|
||||
<div class="w-full max-w-3xl overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-2xl">
|
||||
<div class="max-h-[90vh] w-full max-w-3xl overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-2xl">
|
||||
<header class="flex items-center justify-between border-b border-slate-700 px-4 py-3">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<BadgeInfo class="h-4 w-4 text-cyan-300" />
|
||||
<h2 class="text-sm font-semibold text-slate-100">关于与交付信息</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">关于与诊断</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -78,102 +77,51 @@ function formatTime(ts: number) {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-4 p-4 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<section class="space-y-4">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-cyan-300/80">Product</p>
|
||||
<h3 class="mt-2 text-xl font-semibold text-slate-50">SSH Manager</h3>
|
||||
<p class="mt-2 text-sm text-slate-400">
|
||||
当前是源码交付版,适合按源码、部署脚本和文档一起打包出售,重点保留工作区、备份恢复、批量命令、日志和诊断能力。
|
||||
</p>
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">版本</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ appVersion }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">版本定位</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">Source Delivery Edition</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 overflow-y-auto p-4">
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-950/60 p-4 sm:p-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<ShieldCheck class="h-4 w-4 text-cyan-300" />
|
||||
<h3 class="text-base font-semibold text-slate-50">SSH Manager</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-sm leading-6 text-slate-400">
|
||||
当前版本按“源码交付 + Docker 部署”方式使用。出问题时,把下面的诊断信息复制出来,发给卖家或自己排查即可。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<ShieldCheck class="h-4 w-4 text-emerald-300" />
|
||||
<h3 class="text-sm font-semibold text-slate-100">交付状态</h3>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">当前状态</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ productStatusStore.licenseStatusText }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">首次启动</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ formatTime(productStatusStore.firstLaunchedAt) }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3 sm:col-span-2">
|
||||
<p class="text-xs text-slate-500">环境指纹</p>
|
||||
<p class="mt-1 break-all font-mono text-sm text-slate-100">{{ productStatusStore.machineFingerprint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-slate-500">
|
||||
当前交付模式不依赖授权码。你可以把源码、Docker 文件、部署文档和售后说明一起交付给客户,自行二开或部署。
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">版本</p>
|
||||
<p class="mt-1 break-all text-sm font-medium text-slate-100">{{ appVersion }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs text-slate-500">首次启动</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ formatTime(productStatusStore.firstLaunchedAt) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs text-slate-500">连接数</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ connectionsStore.connections.length }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs text-slate-500">传输记录</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ transfersStore.runs.length }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<FileCode2 class="h-4 w-4 text-cyan-300" />
|
||||
<h3 class="text-sm font-semibold text-slate-100">源码交付建议</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-400">
|
||||
对外售卖源码版时,建议把仓库代码、初始化账号说明、环境变量模板、部署脚本和备份示例一起交付,减少来回答疑。
|
||||
</p>
|
||||
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||
<li>交付仓库源码与 README/部署文档</li>
|
||||
<li>附带默认账号、环境变量和数据目录说明</li>
|
||||
<li>说明哪些功能可直接用,哪些是二开骨架</li>
|
||||
<li>保留诊断信息入口,方便客户反馈问题时定位环境</li>
|
||||
</ul>
|
||||
</div>
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-950/60 p-4 sm:p-5">
|
||||
<h3 class="text-sm font-semibold text-slate-100">诊断信息</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-400">
|
||||
如果页面异常、连接失败或者功能表现不对,先复制这段信息,再配合截图或报错内容一起发出去。
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-4 inline-flex min-h-[44px] w-full items-center justify-center gap-1.5 rounded-md border border-slate-700 px-3 py-2 text-sm text-slate-200 transition-colors hover:bg-slate-800 sm:w-auto"
|
||||
@click="copyDiagnostics"
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
<span>复制诊断信息</span>
|
||||
</button>
|
||||
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<h3 class="text-sm font-semibold text-slate-100">运行摘要</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">连接数</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ connectionsStore.connections.length }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">传输历史数</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ transfersStore.runs.length }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
||||
<p class="text-xs text-slate-500">操作日志数</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ activityLogStore.entries.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<h3 class="text-sm font-semibold text-slate-100">售后诊断</h3>
|
||||
<p class="mt-2 text-sm text-slate-400">
|
||||
客户反馈问题时,可以先复制这份诊断摘要给你,用来快速判断版本、交付模式和本地数据规模。
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-4 inline-flex items-center gap-1.5 rounded-md border border-slate-700 px-3 py-2 text-sm text-slate-200 transition-colors hover:bg-slate-800"
|
||||
@click="copyDiagnostics"
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
<span>复制诊断信息</span>
|
||||
</button>
|
||||
|
||||
<pre class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950 px-3 py-3 text-xs text-slate-300"><code>{{ diagnostics }}</code></pre>
|
||||
</div>
|
||||
<pre class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950 px-3 py-3 text-xs leading-6 text-slate-300"><code>{{ diagnostics }}</code></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Rocket, FolderInput, ShieldCheck, X } from 'lucide-vue-next'
|
||||
import { useProductStatusStore } from '../stores/productStatus'
|
||||
import { Rocket, ShieldCheck, X } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
@@ -13,8 +12,6 @@ const emit = defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const productStatusStore = useProductStatusStore()
|
||||
|
||||
function dismissGuide() {
|
||||
localStorage.setItem('ssh-manager.first-run-dismissed', 'true')
|
||||
emit('dismiss')
|
||||
@@ -33,41 +30,40 @@ function openAbout() {
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 p-4 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 p-3 sm:p-4 backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="首次启动引导"
|
||||
>
|
||||
<div class="w-full max-w-4xl overflow-hidden rounded-2xl border border-cyan-500/20 bg-slate-900 shadow-2xl">
|
||||
<div class="grid gap-0 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<section class="border-b border-slate-800 p-6 lg:border-b-0 lg:border-r lg:p-8">
|
||||
<div class="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl border border-cyan-500/20 bg-slate-900 shadow-2xl">
|
||||
<div class="p-5 sm:p-6 lg:p-8">
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-cyan-500/20 bg-cyan-500/10 px-3 py-1 text-xs text-cyan-200">
|
||||
<Rocket class="h-3.5 w-3.5" />
|
||||
<span>Source Delivery Edition</span>
|
||||
<span>首次使用</span>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 text-2xl font-semibold tracking-tight text-slate-50">
|
||||
{{ props.displayName || '欢迎使用 SSH Manager' }}
|
||||
</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm leading-6 text-slate-400">
|
||||
这是首次启动引导。你现在拿到的是源码交付版,已经具备工作区、SFTP、备份、批量命令、历史日志和环境诊断入口,适合作为源码项目继续交付或二开。
|
||||
第一次进入时,先完成下面 3 件事。做完以后,就可以正常开始使用。
|
||||
</p>
|
||||
|
||||
<div class="mt-6 grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.14em] text-cyan-300/80">Step 1</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-100">创建第一条连接</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">填写主机、端口和认证方式后,就能直接进入终端与文件工作区。</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">填写主机、端口和认证方式后,就能直接进入终端和 SFTP 工作区。</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.14em] text-cyan-300/80">Step 2</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-100">如果有旧数据</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">顶部工具栏已经支持导入备份,可直接恢复连接和会话树,不需要手工重建。</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-100">有旧数据就导入备份</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">顶部工具栏支持导入备份,可以直接恢复连接和会话树,不需要手工重建。</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.14em] text-cyan-300/80">Step 3</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-100">查看交付信息</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">“关于与交付信息”里保留了版本、环境指纹和诊断摘要,方便你整理给客户的交付说明。</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-100">需要排查时看诊断</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">工具栏里的“关于与诊断”可以复制当前版本和运行摘要,方便发给卖家或自己排查。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +82,7 @@ function openAbout() {
|
||||
@click="openAbout"
|
||||
>
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
<span>查看交付信息</span>
|
||||
<span>查看关于与诊断</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -97,38 +93,6 @@ function openAbout() {
|
||||
<span>稍后处理</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="p-6 lg:p-8">
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<ShieldCheck class="h-4 w-4 text-emerald-300" />
|
||||
<h3 class="text-sm font-semibold text-slate-100">源码交付摘要</h3>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<p class="text-xs text-slate-500">当前状态</p>
|
||||
<p class="mt-1 text-sm font-medium text-slate-100">{{ productStatusStore.licenseStatusText }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<p class="text-xs text-slate-500">环境指纹</p>
|
||||
<p class="mt-1 break-all font-mono text-xs text-slate-200">{{ productStatusStore.machineFingerprint }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<FolderInput class="h-4 w-4 text-cyan-300" />
|
||||
<p class="text-sm font-medium text-slate-100">交付建议</p>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-2 text-xs leading-5 text-slate-400">
|
||||
<li>顶部工具栏可导入备份,适合恢复客户环境</li>
|
||||
<li>历史日志里可以查看最近传输和关键操作</li>
|
||||
<li>关于页里的诊断信息可直接发给你排查部署问题</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FolderPlus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
CopyPlus,
|
||||
Search,
|
||||
X,
|
||||
ChevronDown,
|
||||
@@ -171,19 +172,30 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
const items: ContextMenuItem[] = [
|
||||
...(node.type === 'connection' && node.connectionId
|
||||
? [
|
||||
{
|
||||
label: '新开标签',
|
||||
icon: CopyPlus,
|
||||
action: () => workspaceStore.openWorkspace(node.connectionId!),
|
||||
} as ContextMenuItem,
|
||||
{
|
||||
label: '编辑连接',
|
||||
icon: Edit2,
|
||||
action: () => openEditConnection(node.connectionId!),
|
||||
} as ContextMenuItem,
|
||||
{ divider: true } as ContextMenuItem,
|
||||
{
|
||||
label: '重命名',
|
||||
icon: Edit2,
|
||||
action: () => startRename(node.id),
|
||||
} as ContextMenuItem,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: '重命名',
|
||||
icon: Edit2,
|
||||
action: () => startRename(node.id),
|
||||
},
|
||||
: [
|
||||
{
|
||||
label: '重命名',
|
||||
icon: Edit2,
|
||||
action: () => startRename(node.id),
|
||||
},
|
||||
]),
|
||||
]
|
||||
|
||||
if (node.type === 'folder') {
|
||||
@@ -257,7 +269,7 @@ function handleNodeClick(nodeId: string) {
|
||||
if (node.type === 'folder') {
|
||||
treeStore.toggleExpanded(nodeId)
|
||||
} else if (node.type === 'connection' && node.connectionId) {
|
||||
workspaceStore.openWorkspace(node.connectionId)
|
||||
workspaceStore.openOrActivateWorkspace(node.connectionId)
|
||||
}
|
||||
|
||||
treeStore.selectNode(nodeId)
|
||||
|
||||
@@ -21,7 +21,6 @@ const activityLogStore = useActivityLogStore()
|
||||
const draft = reactive<AppSettingsState>({
|
||||
terminalFontFamily: settingsStore.terminalFontFamily,
|
||||
terminalFontSize: settingsStore.terminalFontSize,
|
||||
defaultSplitRatio: settingsStore.defaultSplitRatio,
|
||||
uploadConflictStrategy: settingsStore.uploadConflictStrategy,
|
||||
downloadNamingStrategy: settingsStore.downloadNamingStrategy,
|
||||
})
|
||||
@@ -29,7 +28,6 @@ const draft = reactive<AppSettingsState>({
|
||||
function syncDraft() {
|
||||
draft.terminalFontFamily = settingsStore.terminalFontFamily
|
||||
draft.terminalFontSize = settingsStore.terminalFontSize
|
||||
draft.defaultSplitRatio = settingsStore.defaultSplitRatio
|
||||
draft.uploadConflictStrategy = settingsStore.uploadConflictStrategy
|
||||
draft.downloadNamingStrategy = settingsStore.downloadNamingStrategy
|
||||
}
|
||||
@@ -50,7 +48,7 @@ function submit() {
|
||||
category: 'settings',
|
||||
level: 'success',
|
||||
title: '设置已更新',
|
||||
detail: `终端字号 ${draft.terminalFontSize},默认分屏 ${Math.round(draft.defaultSplitRatio * 100)}%`,
|
||||
detail: `终端字号 ${draft.terminalFontSize}`,
|
||||
})
|
||||
toast.success('设置已保存')
|
||||
closeModal()
|
||||
@@ -120,25 +118,6 @@ function resetSettings() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-100">工作区</h3>
|
||||
<p class="mt-1 text-xs text-slate-400">用于新建工作区和重置分屏时的默认值。</p>
|
||||
</div>
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm text-slate-300">默认分屏比例</span>
|
||||
<input
|
||||
v-model.number="draft.defaultSplitRatio"
|
||||
type="range"
|
||||
min="0.2"
|
||||
max="0.8"
|
||||
step="0.05"
|
||||
class="w-full accent-cyan-500"
|
||||
>
|
||||
<span class="mt-1 block text-xs text-slate-400">终端 {{ Math.round(draft.defaultSplitRatio * 100) }}% / 文件 {{ 100 - Math.round(draft.defaultSplitRatio * 100) }}%</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-100">文件传输</h3>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Settings,
|
||||
HelpCircle,
|
||||
Bell,
|
||||
MonitorCog,
|
||||
ShieldCheck,
|
||||
X,
|
||||
ArrowLeftRight,
|
||||
@@ -269,210 +268,212 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-slate-700 bg-slate-900 px-3 py-2 sm:px-4">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg border border-slate-700 bg-slate-800 text-slate-300 transition-colors hover:border-slate-600 hover:text-slate-100 lg:hidden"
|
||||
:aria-label="props.sidebarOpen ? '收起会话树' : '打开会话树'"
|
||||
@click="emit('toggleSidebar')"
|
||||
>
|
||||
<PanelLeftClose v-if="props.sidebarOpen" class="h-4 w-4" />
|
||||
<PanelLeftOpen v-else class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded bg-gradient-to-br from-cyan-500 to-blue-600">
|
||||
<MonitorCog class="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-slate-100">SSH Manager</p>
|
||||
<p class="hidden text-[11px] text-slate-500 sm:block">Moba Workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-3 flex w-full flex-wrap items-center gap-2 text-xs text-slate-300 lg:order-none lg:w-auto">
|
||||
<button
|
||||
type="button"
|
||||
@click="openBatchCommand"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="批量命令执行"
|
||||
>
|
||||
<TerminalSquare class="h-3.5 w-3.5" />
|
||||
<span>批量命令</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openOperationsHistory"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="传输历史与操作日志"
|
||||
>
|
||||
<ClipboardList class="h-3.5 w-3.5" />
|
||||
<span>历史日志</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleExportBackup"
|
||||
:disabled="backupBusy"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="导出备份"
|
||||
>
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
<span>导出备份</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="triggerImportBackup"
|
||||
:disabled="backupBusy"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="导入备份"
|
||||
>
|
||||
<Upload class="h-3.5 w-3.5" />
|
||||
<span>导入备份</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openTransfers"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors"
|
||||
:class="workspaceStore.transfersModalOpen
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="打开传输页面"
|
||||
>
|
||||
<ArrowLeftRight class="h-3.5 w-3.5" />
|
||||
<span>Transfers</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateSession"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="新增会话"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
<span>新增连接</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="duplicateActiveWorkspace"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="复制当前工作区"
|
||||
>
|
||||
<CopyPlus class="h-3.5 w-3.5" />
|
||||
<span>复制会话</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleTerminal"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="activeWorkspace?.terminalVisible
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="切换终端面板"
|
||||
>
|
||||
<SquareTerminal class="h-3.5 w-3.5" />
|
||||
<span>终端</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleSftp"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="activeWorkspace?.sftpVisible
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="切换 SFTP 面板"
|
||||
>
|
||||
<FolderTree class="h-3.5 w-3.5" />
|
||||
<span>文件</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="resetSplit"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="重置分屏比例"
|
||||
>
|
||||
<Columns2 class="h-3.5 w-3.5" />
|
||||
<span>重置分屏</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1 sm:gap-2">
|
||||
<button
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="通知"
|
||||
type="button"
|
||||
>
|
||||
<Bell class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openAbout"
|
||||
class="hidden min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200 sm:inline-flex"
|
||||
title="关于与交付信息"
|
||||
type="button"
|
||||
>
|
||||
<HelpCircle class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openSettings"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="设置中心"
|
||||
aria-label="设置中心"
|
||||
type="button"
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openChangePassword"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="修改密码"
|
||||
aria-label="修改密码"
|
||||
type="button"
|
||||
>
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg px-2.5 py-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="退出登录"
|
||||
aria-label="退出登录"
|
||||
type="button"
|
||||
>
|
||||
<LogOut class="h-4 w-4" />
|
||||
<span class="hidden text-xs sm:inline">退出</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 min-w-0">
|
||||
<div v-if="workspaceTabs.length > 0" class="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-thin">
|
||||
<div
|
||||
v-for="tab in workspaceTabs"
|
||||
:key="tab.workspaceId"
|
||||
class="group flex min-h-[36px] max-w-[280px] shrink-0 items-center gap-1 rounded-lg border px-1.5 text-xs transition-colors cursor-pointer"
|
||||
:class="tab.active
|
||||
? 'border-cyan-500/40 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600 hover:text-slate-100'"
|
||||
@click="activateTab(tab.workspaceId)"
|
||||
@contextmenu="(e) => openTabContextMenu(tab.workspaceId, e)"
|
||||
>
|
||||
<span class="h-2 w-2 rounded-full" :class="tab.active ? 'bg-cyan-400' : 'bg-slate-500'" />
|
||||
<span class="truncate max-w-[220px]">{{ tab.title }}</span>
|
||||
<div class="border-b border-slate-700 bg-slate-900">
|
||||
<div class="border-b border-slate-800/80 px-3 py-2 sm:px-4">
|
||||
<div class="flex flex-wrap items-center gap-2 lg:flex-nowrap lg:gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-slate-700 hover:text-slate-200"
|
||||
@click="(e) => closeTab(tab.workspaceId, e)"
|
||||
:aria-label="`关闭会话 ${tab.title}`"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] shrink-0 items-center justify-center rounded-lg border border-slate-700 bg-slate-800 text-slate-300 transition-colors hover:border-slate-600 hover:text-slate-100 lg:hidden"
|
||||
:aria-label="props.sidebarOpen ? '收起会话树' : '打开会话树'"
|
||||
@click="emit('toggleSidebar')"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
<PanelLeftClose v-if="props.sidebarOpen" class="h-4 w-4" />
|
||||
<PanelLeftOpen v-else class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="min-w-0 flex-1 overflow-x-auto pb-1 lg:pb-0 scrollbar-thin">
|
||||
<div class="flex min-w-max items-center gap-2 text-xs text-slate-300">
|
||||
<button
|
||||
type="button"
|
||||
@click="openBatchCommand"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="批量命令执行"
|
||||
>
|
||||
<TerminalSquare class="h-3.5 w-3.5" />
|
||||
<span>批量命令</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openOperationsHistory"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="传输历史与操作日志"
|
||||
>
|
||||
<ClipboardList class="h-3.5 w-3.5" />
|
||||
<span>历史日志</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleExportBackup"
|
||||
:disabled="backupBusy"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="导出备份"
|
||||
>
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
<span>导出备份</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="triggerImportBackup"
|
||||
:disabled="backupBusy"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="导入备份"
|
||||
>
|
||||
<Upload class="h-3.5 w-3.5" />
|
||||
<span>导入备份</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openTransfers"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors"
|
||||
:class="workspaceStore.transfersModalOpen
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="打开传输页面"
|
||||
>
|
||||
<ArrowLeftRight class="h-3.5 w-3.5" />
|
||||
<span>Transfers</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateSession"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
||||
aria-label="新增会话"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
<span>新增连接</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="duplicateActiveWorkspace"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="复制当前工作区"
|
||||
>
|
||||
<CopyPlus class="h-3.5 w-3.5" />
|
||||
<span>复制会话</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleTerminal"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="activeWorkspace?.terminalVisible
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="切换终端面板"
|
||||
>
|
||||
<SquareTerminal class="h-3.5 w-3.5" />
|
||||
<span>终端</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleSftp"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="activeWorkspace?.sftpVisible
|
||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
||||
aria-label="切换 SFTP 面板"
|
||||
>
|
||||
<FolderTree class="h-3.5 w-3.5" />
|
||||
<span>文件</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="resetSplit"
|
||||
:disabled="!activeWorkspace"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="重置分屏比例"
|
||||
>
|
||||
<Columns2 class="h-3.5 w-3.5" />
|
||||
<span>重置分屏</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1 sm:gap-2">
|
||||
<button
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="通知"
|
||||
type="button"
|
||||
>
|
||||
<Bell class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openAbout"
|
||||
class="hidden min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200 sm:inline-flex"
|
||||
title="关于与诊断"
|
||||
type="button"
|
||||
>
|
||||
<HelpCircle class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openSettings"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="设置中心"
|
||||
aria-label="设置中心"
|
||||
type="button"
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="openChangePassword"
|
||||
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="修改密码"
|
||||
aria-label="修改密码"
|
||||
type="button"
|
||||
>
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg px-2.5 py-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
||||
title="退出登录"
|
||||
aria-label="退出登录"
|
||||
type="button"
|
||||
>
|
||||
<LogOut class="h-4 w-4" />
|
||||
<span class="hidden text-xs sm:inline">退出</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-slate-500">未打开工作区。点击左侧连接可创建新实例。</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-950/35 px-3 py-2 sm:px-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="hidden shrink-0 pt-2 sm:block">
|
||||
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500">会话</p>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div v-if="workspaceTabs.length > 0" class="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-thin">
|
||||
<div
|
||||
v-for="tab in workspaceTabs"
|
||||
:key="tab.workspaceId"
|
||||
class="group -mb-px flex min-h-[38px] max-w-[280px] shrink-0 items-center gap-2 rounded-t-lg border border-b-0 px-3 text-xs transition-colors cursor-pointer"
|
||||
:class="tab.active
|
||||
? 'border-cyan-400/35 bg-slate-900 text-cyan-100 shadow-[0_-1px_0_rgba(34,211,238,0.16)]'
|
||||
: 'border-slate-700/80 bg-slate-800/65 text-slate-400 hover:bg-slate-800 hover:text-slate-100'"
|
||||
@click="activateTab(tab.workspaceId)"
|
||||
@contextmenu="(e) => openTabContextMenu(tab.workspaceId, e)"
|
||||
>
|
||||
<span class="h-2 w-2 rounded-full" :class="tab.active ? 'bg-cyan-400' : 'bg-slate-500'" />
|
||||
<span class="truncate max-w-[220px]">{{ tab.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-slate-700 hover:text-slate-200"
|
||||
@click="(e) => closeTab(tab.workspaceId, e)"
|
||||
:aria-label="`关闭会话 ${tab.title}`"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="py-2 text-xs text-slate-500">未打开工作区。点击左侧连接可创建新实例。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ watch(
|
||||
>
|
||||
<div v-if="workspace.terminalVisible || workspace.sftpVisible" class="h-full">
|
||||
<SplitPane
|
||||
direction="vertical"
|
||||
direction="horizontal"
|
||||
:initial-ratio="workspace.splitRatio"
|
||||
:show-first="workspace.terminalVisible"
|
||||
:show-second="workspace.sftpVisible"
|
||||
|
||||
@@ -46,10 +46,12 @@ const showBatchCommandModal = ref(false)
|
||||
const showOperationsHistoryModal = ref(false)
|
||||
const showAboutModal = ref(false)
|
||||
const sidebarOpen = computed(() => workspaceStore.sidebarOpen)
|
||||
const sidebarWidth = computed(() => workspaceStore.sidebarWidth)
|
||||
const transfersModalOpen = computed(() => workspaceStore.transfersModalOpen)
|
||||
const sessionModalOpen = computed(() => workspaceStore.sessionModalOpen)
|
||||
const sessionModalMode = computed(() => workspaceStore.sessionModalMode)
|
||||
const forcePasswordChange = computed(() => authStore.passwordChangeRequired)
|
||||
const isSidebarResizing = ref(false)
|
||||
const currentEditingConnection = computed(() => {
|
||||
if (sessionModalMode.value !== 'edit' || workspaceStore.editingConnectionId == null) {
|
||||
return null
|
||||
@@ -57,6 +59,10 @@ const currentEditingConnection = computed(() => {
|
||||
return connectionsStore.getConnection(workspaceStore.editingConnectionId) || null
|
||||
})
|
||||
|
||||
const desktopSidebarStyle = computed(() => ({
|
||||
width: `${sidebarWidth.value}px`,
|
||||
}))
|
||||
|
||||
// Enable bidirectional sync
|
||||
useConnectionSync()
|
||||
|
||||
@@ -158,6 +164,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopSidebarResize()
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
@@ -191,6 +198,36 @@ function closeSidebar() {
|
||||
workspaceStore.closeSidebar()
|
||||
}
|
||||
|
||||
function clampSidebarWidth(width: number) {
|
||||
return Math.max(256, Math.min(420, Math.round(width)))
|
||||
}
|
||||
|
||||
function handleSidebarResize(event: MouseEvent) {
|
||||
if (!isSidebarResizing.value) return
|
||||
workspaceStore.updateSidebarWidth(clampSidebarWidth(event.clientX))
|
||||
}
|
||||
|
||||
function stopSidebarResize() {
|
||||
if (!isSidebarResizing.value) return
|
||||
|
||||
isSidebarResizing.value = false
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.cursor = ''
|
||||
window.removeEventListener('mousemove', handleSidebarResize)
|
||||
window.removeEventListener('mouseup', stopSidebarResize)
|
||||
}
|
||||
|
||||
function startSidebarResize(event: MouseEvent) {
|
||||
if (window.innerWidth < 1024) return
|
||||
|
||||
event.preventDefault()
|
||||
isSidebarResizing.value = true
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.cursor = 'col-resize'
|
||||
window.addEventListener('mousemove', handleSidebarResize)
|
||||
window.addEventListener('mouseup', stopSidebarResize)
|
||||
}
|
||||
|
||||
function closeSessionModal() {
|
||||
workspaceStore.closeSessionModal()
|
||||
}
|
||||
@@ -304,13 +341,31 @@ function resolveErrorMessage(error: unknown, fallback: string) {
|
||||
/>
|
||||
|
||||
<div
|
||||
class="fixed inset-y-0 left-0 z-40 w-[18rem] max-w-[calc(100vw-2rem)] -translate-x-full border-r border-slate-700 bg-slate-900 transition-transform duration-200 lg:static lg:z-auto lg:w-72 lg:max-w-none lg:translate-x-0"
|
||||
class="fixed inset-y-0 left-0 z-40 w-[18rem] max-w-[calc(100vw-2rem)] -translate-x-full border-r border-slate-700 bg-slate-900 transition-transform duration-200 lg:hidden"
|
||||
:class="sidebarOpen ? 'translate-x-0' : ''"
|
||||
>
|
||||
<SessionTree />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 lg:pl-0">
|
||||
<div class="hidden h-full shrink-0 lg:block" :style="desktopSidebarStyle">
|
||||
<SessionTree />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="group relative hidden h-full w-3 shrink-0 cursor-col-resize lg:block"
|
||||
:class="isSidebarResizing ? 'bg-cyan-500/10' : ''"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="调整左侧会话树宽度"
|
||||
@mousedown="startSidebarResize"
|
||||
>
|
||||
<div
|
||||
class="mx-auto h-full w-px bg-slate-700 transition-colors"
|
||||
:class="isSidebarResizing ? 'bg-cyan-400' : 'group-hover:bg-cyan-500'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<WorkspacePanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'ssh-manager.product-status'
|
||||
|
||||
@@ -7,26 +7,8 @@ type ProductStatusSnapshot = {
|
||||
firstLaunchedAt: number
|
||||
}
|
||||
|
||||
function hashString(input: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < input.length; i += 1) {
|
||||
hash = (hash * 31 + input.charCodeAt(i)) >>> 0
|
||||
}
|
||||
return hash.toString(16).padStart(8, '0')
|
||||
}
|
||||
|
||||
export const useProductStatusStore = defineStore('productStatus', () => {
|
||||
const firstLaunchedAt = ref<number>(Date.now())
|
||||
const licenseStatusText = computed(() => '源码交付版(无激活限制)')
|
||||
const machineFingerprint = computed(() => {
|
||||
const source = [
|
||||
window.location.host,
|
||||
navigator.userAgent,
|
||||
navigator.language,
|
||||
navigator.platform,
|
||||
].join('|')
|
||||
return hashString(source).toUpperCase()
|
||||
})
|
||||
|
||||
function restore() {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
@@ -59,8 +41,6 @@ export const useProductStatusStore = defineStore('productStatus', () => {
|
||||
|
||||
return {
|
||||
firstLaunchedAt,
|
||||
licenseStatusText,
|
||||
machineFingerprint,
|
||||
restore,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ const STORAGE_KEY = 'ssh-manager.settings'
|
||||
const DEFAULT_SETTINGS: AppSettingsState = {
|
||||
terminalFontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
terminalFontSize: 14,
|
||||
defaultSplitRatio: 0.5,
|
||||
uploadConflictStrategy: 'ask',
|
||||
downloadNamingStrategy: 'original',
|
||||
}
|
||||
@@ -16,11 +15,6 @@ function normalizeFontSize(value: unknown) {
|
||||
return Math.max(12, Math.min(24, size))
|
||||
}
|
||||
|
||||
function normalizeSplitRatio(value: unknown) {
|
||||
const ratio = typeof value === 'number' ? value : DEFAULT_SETTINGS.defaultSplitRatio
|
||||
return Math.max(0.2, Math.min(0.8, ratio))
|
||||
}
|
||||
|
||||
function normalizeUploadConflictStrategy(value: unknown): UploadConflictStrategy {
|
||||
return value === 'overwrite' || value === 'skip' ? value : 'ask'
|
||||
}
|
||||
@@ -45,7 +39,6 @@ export const useSettingsStore = defineStore('settings', {
|
||||
? parsed.terminalFontFamily
|
||||
: DEFAULT_SETTINGS.terminalFontFamily
|
||||
this.terminalFontSize = normalizeFontSize(parsed.terminalFontSize)
|
||||
this.defaultSplitRatio = normalizeSplitRatio(parsed.defaultSplitRatio)
|
||||
this.uploadConflictStrategy = normalizeUploadConflictStrategy(parsed.uploadConflictStrategy)
|
||||
this.downloadNamingStrategy = normalizeDownloadNamingStrategy(parsed.downloadNamingStrategy)
|
||||
} catch (error) {
|
||||
@@ -60,9 +53,6 @@ export const useSettingsStore = defineStore('settings', {
|
||||
if (next.terminalFontSize != null) {
|
||||
this.terminalFontSize = normalizeFontSize(next.terminalFontSize)
|
||||
}
|
||||
if (next.defaultSplitRatio != null) {
|
||||
this.defaultSplitRatio = normalizeSplitRatio(next.defaultSplitRatio)
|
||||
}
|
||||
if (next.uploadConflictStrategy != null) {
|
||||
this.uploadConflictStrategy = normalizeUploadConflictStrategy(next.uploadConflictStrategy)
|
||||
}
|
||||
@@ -81,7 +71,6 @@ export const useSettingsStore = defineStore('settings', {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
terminalFontFamily: this.terminalFontFamily,
|
||||
terminalFontSize: this.terminalFontSize,
|
||||
defaultSplitRatio: this.defaultSplitRatio,
|
||||
uploadConflictStrategy: this.uploadConflictStrategy,
|
||||
downloadNamingStrategy: this.downloadNamingStrategy,
|
||||
}))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { WorkspaceInstanceState, WorkspaceState } from '../types/workspace'
|
||||
import { useSettingsStore } from './settings'
|
||||
|
||||
const STORAGE_KEY = 'ssh-manager.workspace'
|
||||
const DEFAULT_SIDEBAR_WIDTH = 288
|
||||
const MIN_SIDEBAR_WIDTH = 256
|
||||
const MAX_SIDEBAR_WIDTH = 420
|
||||
const MIN_SFTP_SPLIT_RATIO = 0.8
|
||||
|
||||
type LegacyPanelState = {
|
||||
connectionId: number
|
||||
@@ -41,10 +44,15 @@ function createWorkspace(connectionId: number, instanceNumber: number, splitRati
|
||||
}
|
||||
|
||||
function normalizeSplitRatio(value: unknown) {
|
||||
const ratio = typeof value === 'number' ? value : 0.5
|
||||
const ratio = typeof value === 'number' ? value : MIN_SFTP_SPLIT_RATIO
|
||||
return Math.max(0.2, Math.min(0.8, ratio))
|
||||
}
|
||||
|
||||
function normalizeSidebarWidth(value: unknown) {
|
||||
const width = typeof value === 'number' ? value : DEFAULT_SIDEBAR_WIDTH
|
||||
return Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, Math.round(width)))
|
||||
}
|
||||
|
||||
function normalizeWorkspace(candidate: Partial<WorkspaceInstanceState>, fallbackId: string): WorkspaceInstanceState | null {
|
||||
if (typeof candidate.connectionId !== 'number' || candidate.connectionId <= 0) {
|
||||
return null
|
||||
@@ -73,15 +81,24 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
sessionModalMode: 'create',
|
||||
editingConnectionId: null,
|
||||
sidebarOpen: false,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
activeWorkspace: (state): WorkspaceInstanceState | null => {
|
||||
return state.activeWorkspaceId ? state.workspaces[state.activeWorkspaceId] || null : null
|
||||
},
|
||||
|
||||
firstWorkspaceIdByConnection: (state) => (connectionId: number): string | null => {
|
||||
return state.workspaceOrder.find((workspaceId) => state.workspaces[workspaceId]?.connectionId === connectionId) ?? null
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
applyNewWorkspaceLayoutDefaults() {
|
||||
this.sidebarWidth = MIN_SIDEBAR_WIDTH
|
||||
},
|
||||
|
||||
nextInstanceNumber(connectionId: number) {
|
||||
return Object.values(this.workspaces)
|
||||
.filter((workspace) => workspace.connectionId === connectionId)
|
||||
@@ -89,8 +106,8 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
},
|
||||
|
||||
openWorkspace(connectionId: number) {
|
||||
const settingsStore = useSettingsStore()
|
||||
const workspace = createWorkspace(connectionId, this.nextInstanceNumber(connectionId), settingsStore.defaultSplitRatio)
|
||||
const workspace = createWorkspace(connectionId, this.nextInstanceNumber(connectionId), MIN_SFTP_SPLIT_RATIO)
|
||||
this.applyNewWorkspaceLayoutDefaults()
|
||||
this.workspaces[workspace.id] = workspace
|
||||
this.workspaceOrder.push(workspace.id)
|
||||
this.activeWorkspaceId = workspace.id
|
||||
@@ -98,17 +115,27 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
return workspace.id
|
||||
},
|
||||
|
||||
openOrActivateWorkspace(connectionId: number) {
|
||||
const existingWorkspaceId = this.firstWorkspaceIdByConnection(connectionId)
|
||||
if (existingWorkspaceId) {
|
||||
this.activateWorkspace(existingWorkspaceId)
|
||||
return existingWorkspaceId
|
||||
}
|
||||
|
||||
return this.openWorkspace(connectionId)
|
||||
},
|
||||
|
||||
duplicateWorkspace(workspaceId: string) {
|
||||
const source = this.workspaces[workspaceId]
|
||||
if (!source) return null
|
||||
|
||||
const duplicate = createWorkspace(source.connectionId, this.nextInstanceNumber(source.connectionId), source.splitRatio)
|
||||
duplicate.splitRatio = source.splitRatio
|
||||
const duplicate = createWorkspace(source.connectionId, this.nextInstanceNumber(source.connectionId), MIN_SFTP_SPLIT_RATIO)
|
||||
duplicate.terminalVisible = source.terminalVisible
|
||||
duplicate.sftpVisible = source.sftpVisible
|
||||
duplicate.currentPath = source.currentPath
|
||||
duplicate.selectedFiles = [...source.selectedFiles]
|
||||
|
||||
this.applyNewWorkspaceLayoutDefaults()
|
||||
this.workspaces[duplicate.id] = duplicate
|
||||
this.workspaceOrder.push(duplicate.id)
|
||||
this.activeWorkspaceId = duplicate.id
|
||||
@@ -234,8 +261,7 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
resetSplitRatio(workspaceId: string) {
|
||||
const workspace = this.workspaces[workspaceId]
|
||||
if (!workspace) return
|
||||
const settingsStore = useSettingsStore()
|
||||
workspace.splitRatio = settingsStore.defaultSplitRatio
|
||||
workspace.splitRatio = MIN_SFTP_SPLIT_RATIO
|
||||
this.persist()
|
||||
},
|
||||
|
||||
@@ -279,6 +305,11 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
this.sidebarOpen = !this.sidebarOpen
|
||||
},
|
||||
|
||||
updateSidebarWidth(width: number) {
|
||||
this.sidebarWidth = normalizeSidebarWidth(width)
|
||||
this.persist()
|
||||
},
|
||||
|
||||
persist() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.$state))
|
||||
},
|
||||
@@ -368,6 +399,7 @@ export const useWorkspaceStore = defineStore('workspace', {
|
||||
this.sessionModalMode = data.sessionModalMode === 'edit' ? 'edit' : 'create'
|
||||
this.editingConnectionId = typeof data.editingConnectionId === 'number' ? data.editingConnectionId : null
|
||||
this.sidebarOpen = false
|
||||
this.sidebarWidth = normalizeSidebarWidth(data.sidebarWidth)
|
||||
|
||||
this.transfersModalOpen = false
|
||||
this.sessionModalOpen = false
|
||||
|
||||
@@ -19,6 +19,39 @@
|
||||
linear-gradient(180deg, var(--app-bg-0), var(--app-bg-1));
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(103, 232, 249, 0.38) rgba(15, 23, 42, 0.78);
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(103, 232, 249, 0.38) rgba(15, 23, 42, 0.42);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border: 2px solid rgba(15, 23, 42, 0.4);
|
||||
border-radius: 9999px;
|
||||
background: linear-gradient(180deg, rgba(103, 232, 249, 0.34), rgba(34, 211, 238, 0.22));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, rgba(125, 211, 252, 0.58), rgba(34, 211, 238, 0.4));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
|
||||
@@ -4,7 +4,6 @@ export type DownloadNamingStrategy = 'original' | 'connectionPrefix'
|
||||
export interface AppSettingsState {
|
||||
terminalFontFamily: string
|
||||
terminalFontSize: number
|
||||
defaultSplitRatio: number
|
||||
uploadConflictStrategy: UploadConflictStrategy
|
||||
downloadNamingStrategy: DownloadNamingStrategy
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ export interface WorkspaceState {
|
||||
sessionModalMode: 'create' | 'edit'
|
||||
editingConnectionId: number | null
|
||||
sidebarOpen: boolean
|
||||
sidebarWidth: number
|
||||
}
|
||||
|
||||
@@ -25,18 +25,18 @@ const loading = ref(false)
|
||||
const highlights = [
|
||||
{
|
||||
icon: SquareTerminal,
|
||||
title: 'Moba 工作区',
|
||||
description: '多实例标签、终端与 SFTP 分屏,直接面对日常 SSH 运维场景。',
|
||||
title: 'SSH / SFTP 工作区',
|
||||
description: '一个界面里直接打开终端和 SFTP,适合日常服务器管理。',
|
||||
},
|
||||
{
|
||||
icon: FolderInput,
|
||||
title: '完整备份恢复',
|
||||
description: '支持连接和会话树整体导入导出,迁移客户环境更省事。',
|
||||
title: '备份恢复',
|
||||
description: '支持连接和会话树整体导入导出,迁移环境更省事。',
|
||||
},
|
||||
{
|
||||
icon: History,
|
||||
title: '历史与日志',
|
||||
description: '传输历史、操作日志、诊断信息都能留住,售后定位更快。',
|
||||
title: '日志排查',
|
||||
description: '传输历史、操作日志和诊断信息都保留,出问题更容易定位。',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -74,16 +74,16 @@ async function handleSubmit() {
|
||||
<div class="relative">
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-cyan-500/20 bg-cyan-500/10 px-3 py-1 text-xs tracking-[0.18em] text-cyan-200">
|
||||
<MonitorCog class="h-3.5 w-3.5" />
|
||||
<span>SOURCE DELIVERY EDITION</span>
|
||||
<span>源码交付 + Docker 部署</span>
|
||||
</div>
|
||||
|
||||
<h1 class="mt-5 max-w-3xl text-4xl font-semibold tracking-tight text-slate-50 lg:text-5xl">
|
||||
面向源码交付与二开的
|
||||
面向源码交付的
|
||||
<span class="text-cyan-300"> SSH / SFTP </span>
|
||||
工作区项目
|
||||
</h1>
|
||||
<p class="mt-5 max-w-2xl text-base leading-7 text-slate-300">
|
||||
把终端、文件传输、批量命令、备份恢复和诊断入口整合进一个统一工作区。适合源码交付、私有部署和后续二开。
|
||||
适合按源码 + Docker 方式交付。终端、SFTP、批量命令、备份恢复都放在一个统一工作区里。
|
||||
</p>
|
||||
|
||||
<div class="mt-8 grid gap-4 md:grid-cols-3">
|
||||
@@ -105,9 +105,9 @@ async function handleSubmit() {
|
||||
<h2 class="text-sm font-semibold text-slate-100">交付方式</h2>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||
<li>源码仓库 + 部署说明文档</li>
|
||||
<li>Docker 版一键启动</li>
|
||||
<li>首次启动引导、关于与交付信息、诊断摘要</li>
|
||||
<li>仓库源码</li>
|
||||
<li>README 一份主文档</li>
|
||||
<li>Docker 启动方式</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -117,9 +117,9 @@ async function handleSubmit() {
|
||||
<h2 class="text-sm font-semibold text-slate-100">当前版本能力</h2>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||
<li>源码交付说明与环境诊断入口</li>
|
||||
<li>终端自动重连、批量命令执行</li>
|
||||
<li>传输历史、操作日志、备份恢复</li>
|
||||
<li>SSH 终端 + SFTP 文件管理</li>
|
||||
<li>批量命令 + 历史日志</li>
|
||||
<li>备份恢复 + 基础诊断</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@ async function handleSubmit() {
|
||||
<LogIn class="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Workspace Access</p>
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Workspace Login</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-slate-100">登录 SSH 管理器</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
|
||||
const featureCards = [
|
||||
{
|
||||
title: 'Moba 工作区',
|
||||
description: '统一入口打开终端、SFTP、传输和批量命令,支持多标签、多实例和分屏布局。',
|
||||
title: 'SSH / SFTP 工作区',
|
||||
description: '统一入口打开终端、SFTP、传输和批量命令,适合日常服务器管理。',
|
||||
icon: MonitorCog,
|
||||
},
|
||||
{
|
||||
@@ -25,12 +25,12 @@ const featureCards = [
|
||||
},
|
||||
{
|
||||
title: '备份恢复',
|
||||
description: '连接和会话树整体导入导出,适合迁移环境、售后恢复和客户交付。',
|
||||
description: '连接和会话树整体导入导出,适合迁移环境和售后恢复。',
|
||||
icon: FolderInput,
|
||||
},
|
||||
{
|
||||
title: '历史与日志',
|
||||
description: '保留传输历史、关键操作和诊断信息,方便售后排障和留痕。',
|
||||
description: '保留传输历史、关键操作和诊断信息,方便排障和留痕。',
|
||||
icon: History,
|
||||
},
|
||||
]
|
||||
@@ -41,7 +41,7 @@ const screenshots = [
|
||||
'终端与 SFTP 分屏',
|
||||
'批量命令执行结果',
|
||||
'传输历史与日志',
|
||||
'关于与交付信息',
|
||||
'关于与诊断',
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -54,7 +54,7 @@ const screenshots = [
|
||||
<div>
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-cyan-500/20 bg-cyan-500/10 px-3 py-1 text-xs tracking-[0.2em] text-cyan-200">
|
||||
<BadgeCheck class="h-3.5 w-3.5" />
|
||||
<span>PRODUCT SHOWCASE</span>
|
||||
<span>商品展示页</span>
|
||||
</div>
|
||||
<h1 class="mt-5 max-w-3xl text-4xl font-semibold tracking-tight text-slate-50 lg:text-6xl">
|
||||
可直接截图和录屏的
|
||||
@@ -62,7 +62,7 @@ const screenshots = [
|
||||
演示页
|
||||
</h1>
|
||||
<p class="mt-5 max-w-2xl text-base leading-7 text-slate-300">
|
||||
用于源码商品图、演示视频和介绍页展示。把“可交付、可部署、可二开”的项目感直接摆到买家眼前,不需要先登录再解释这是什么。
|
||||
用于源码商品主图、录屏和详情页展示。重点突出“源码交付 + Docker 部署”,不用再把它包装成 Windows 安装包。
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||
<RouterLink
|
||||
@@ -89,7 +89,7 @@ const screenshots = [
|
||||
<h2 class="text-sm font-semibold text-slate-100">适合谁买</h2>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||
<li>开发者与小团队运维</li>
|
||||
<li>会 Docker 的开发者与小团队运维</li>
|
||||
<li>NAS / 软路由 / 云主机用户</li>
|
||||
<li>想找 MobaXterm / FinalShell 替代品的人</li>
|
||||
</ul>
|
||||
@@ -100,8 +100,8 @@ const screenshots = [
|
||||
<h2 class="text-sm font-semibold text-slate-100">当前可卖能力</h2>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||
<li>源码交付说明与环境诊断</li>
|
||||
<li>Docker / 部署脚本</li>
|
||||
<li>SSH 终端 + SFTP 文件管理</li>
|
||||
<li>Docker 部署 + README 说明</li>
|
||||
<li>批量命令、日志、备份恢复</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user