合并服务器配置与部署命令流程

将服务器连接、证书路径、校验命令和重载命令统一到同一表单,并优化 SSH 引导布局。
This commit is contained in:
xuans 2026-09-26 14:56:39 +08:00
parent 6e43d5e3a2
commit 7b9329433a
3 changed files with 258 additions and 193 deletions

View File

@ -596,7 +596,7 @@ test("serverModal.compilesAndPreservesDeploymentFields", async () => {
assert.match(html, /value="22"/);
assert.match(html, /min="1"/);
assert.match(html, /max="65535"/);
assert.equal((html.match(/class="test-help-tip"/g) || []).length, 4);
assert.equal((html.match(/class="test-help-tip"/g) || []).length, 5);
for (const path of ["/test/cert.pem", "/test/fullchain.pem", "/test/key.pem"]) {
assert.ok(html.includes(path));
}

View File

@ -17,24 +17,62 @@
.server-modal form {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0 18px;
}
.server-modal form > .form-grid,
.server-modal form > .field {
grid-column: span 2;
}
.server-modal .form-grid {
gap: 14px;
}
.server-modal .server-form-section {
display: grid;
grid-column: 1 / -1;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0 18px;
min-width: 0;
padding: 14px 0 4px;
border-top: 1px solid #dce6f1;
}
.server-modal .server-form-section:first-child {
padding-top: 2px;
border-top: 0;
}
.server-modal .server-form-section-heading,
.server-modal .server-form-grid,
.server-modal .server-paths,
.server-modal .form-error,
.server-modal .modal-actions {
grid-column: 1 / -1;
}
.server-modal .server-form-section-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 8px;
}
.server-modal .server-form-section-heading h3 {
margin: 2px 0 0;
color: #214d86;
font-size: 17px;
}
.server-modal .server-form-heading-actions {
display: flex;
align-items: center;
gap: 10px;
}
.server-modal .server-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 18px;
}
.server-modal .server-form-field-wide {
grid-column: 1 / -1;
}
.server-modal .server-paths {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@ -75,29 +113,13 @@
}
.server-modal .server-ssh-key-guide {
grid-column: 1 / -1;
min-width: 0;
margin: 2px 0 8px;
margin: 0;
padding: 14px;
border: 1px solid #c7daf1;
border-radius: 14px;
background: #f5f9ff;
}
.server-modal .server-ssh-key-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 10px;
}
.server-modal .server-ssh-key-heading h3 {
margin: 2px 0 4px;
color: #214d86;
font-size: 18px;
}
.server-modal .server-ssh-key-guide .ssh-key-steps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@ -229,13 +251,17 @@
border-top: 1px solid #d1e0f1;
}
.server-modal .server-command-section textarea {
min-height: 82px;
}
@media (max-width: 900px) {
.server-modal form {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.server-modal form > .field {
grid-column: span 1;
.server-modal .server-form-section {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.server-modal .server-paths {
@ -253,12 +279,15 @@
}
.server-modal form,
.server-modal .form-grid {
.server-modal .server-form-section,
.server-modal .server-form-grid {
grid-template-columns: minmax(0, 1fr);
}
.server-modal form > .form-grid,
.server-modal form > .field {
.server-modal .server-form-section,
.server-modal .server-form-section-heading,
.server-modal .server-form-grid,
.server-modal .server-form-field-wide {
grid-column: 1 / -1;
}

View File

@ -61,103 +61,130 @@ const {
×
</button>
</div>
<form @submit.prevent="saveServer">
<div class="form-grid">
<label class="field"
><span>服务器名称</span
><input
v-model="serverForm.name"
required
type="text"
placeholder="例如生产 Nginx" /></label
><label class="field"
><span>网站服务</span
><select v-model="serverForm.web_server">
<option value="nginx">Nginx</option>
<option value="caddy">Caddy</option>
<option value="apache">Apache</option>
<option value="custom">自定义</option>
</select></label
>
</div>
<div class="form-grid">
<label class="field"
><span>服务器地址</span
><input
v-model="serverForm.address"
required
type="text"
placeholder="IP 或主机名" /></label
><label class="field"
><span>SSH 端口</span
><input
v-model.number="serverForm.port"
required
type="number"
min="1"
max="65535"
/></label>
</div>
<div class="form-grid">
<label class="field"
><span>SSH 用户</span
><input
v-model="serverForm.username"
required
type="text" /></label
><label class="field"
><span>认证方式</span
><select v-model="serverForm.auth_method">
<option value="password">SSH 密码</option>
<option value="private_key">SSH 私钥</option>
</select></label
>
</div>
<label v-if="serverForm.auth_method === 'password'" class="field">
<span>SSH 密码</span>
<input
v-model="serverForm.password"
type="password"
:required="!serverEditingId || !serverForm.passwordConfigured"
autocomplete="new-password"
placeholder="编辑时留空表示保留原密码"
/>
</label>
<div v-else class="form-grid">
<label class="field"
><span class="field-label-row"
><span>SSH 私钥</span
><button
class="inline-button"
type="button"
@click="openSSHKeyCreate({ embedded: true })"
>
新建或导入
</button></span
><select v-model="serverForm.private_key_ref" required>
<option value="">请选择已加密保存的私钥</option>
<option
v-for="key in sshKeys"
:key="key.id"
:value="key.reference"
>
{{ key.name }} · {{ key.algorithm }}
</option></select
><HelpTip v-if="sshKeys.length === 0"
>还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。</HelpTip
></label
>
</div>
<section v-if="showEmbeddedSSHKeyGuide" class="server-ssh-key-guide">
<div class="server-ssh-key-heading">
<form class="server-form" @submit.prevent="saveServer">
<section class="server-form-section">
<div class="server-form-section-heading">
<div>
<p class="eyebrow">服务器配置</p>
<h3>基本信息与连接方式</h3>
</div>
<HelpTip>
这里的内容、SSH 认证方式、证书路径和服务命令会作为同一台部署服务器的完整配置保存。
</HelpTip>
</div>
<div class="server-form-grid">
<label class="field">
<span>服务器名称</span>
<input
v-model="serverForm.name"
required
type="text"
placeholder="例如生产 Nginx"
/>
</label>
<label class="field">
<span>网站服务</span>
<select v-model="serverForm.web_server">
<option value="nginx">Nginx</option>
<option value="caddy">Caddy</option>
<option value="apache">Apache</option>
<option value="custom">自定义</option>
</select>
</label>
<label class="field">
<span>服务器地址</span>
<input
v-model="serverForm.address"
required
type="text"
placeholder="IP 或主机名"
/>
</label>
<label class="field">
<span>SSH 端口</span>
<input
v-model.number="serverForm.port"
required
type="number"
min="1"
max="65535"
/>
</label>
<label class="field">
<span>SSH 用户</span>
<input v-model="serverForm.username" required type="text" />
</label>
<label class="field">
<span>认证方式</span>
<select v-model="serverForm.auth_method">
<option value="password">SSH 密码</option>
<option value="private_key">SSH 私钥</option>
</select>
</label>
<label v-if="serverForm.auth_method === 'password'" class="field server-form-field-wide">
<span>SSH 密码</span>
<input
v-model="serverForm.password"
type="password"
:required="!serverEditingId || !serverForm.passwordConfigured"
autocomplete="new-password"
placeholder="编辑时留空表示保留原密码"
/>
</label>
<label v-else class="field server-form-field-wide">
<span class="field-label-row">
<span>SSH 私钥</span>
<button
class="inline-button"
type="button"
@click="openSSHKeyCreate({ embedded: true })"
>
新建或导入
</button>
</span>
<select v-model="serverForm.private_key_ref" required>
<option value="">请选择已加密保存的私钥</option>
<option
v-for="key in sshKeys"
:key="key.id"
:value="key.reference"
>
{{ key.name }} · {{ key.algorithm }}
</option>
</select>
<HelpTip v-if="sshKeys.length === 0">
还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。
</HelpTip>
</label>
<label class="field server-form-field-wide">
<span>
SSH 主机 SHA256 指纹
<HelpTip>请填写通过可信渠道核对过的服务器主机指纹,避免连接到错误的服务器。</HelpTip>
</span>
<input
v-model="serverForm.host_key_fingerprint"
required
type="text"
placeholder="SHA256:..."
/>
</label>
</div>
</section>
<section v-if="showEmbeddedSSHKeyGuide" class="server-form-section server-ssh-key-guide">
<div class="server-form-section-heading">
<div>
<p class="eyebrow">当前服务器</p>
<h3>在这里准备 SSH 私钥</h3>
<span class="muted">准备完成后会自动选中,不需要离开服务器配置。</span>
<h3>准备 SSH 私钥</h3>
</div>
<div class="server-form-heading-actions">
<HelpTip>
按顺序执行命令后读取本机私钥,保存后会自动选中到当前服务器配置。
</HelpTip>
<button class="inline-button" type="button" @click="closeSSHKeyForm">
收起引导
</button>
</div>
<button class="inline-button" type="button" @click="closeSSHKeyForm">
收起引导
</button>
</div>
<div class="ssh-key-steps">
<article>
@ -275,72 +302,84 @@ const {
</div>
<p v-if="sshKeyFormError" class="form-error">{{ sshKeyFormError }}</p>
<div class="server-ssh-key-actions">
<span class="muted">私钥只会加密保存,服务器配置仅保存安全引用。</span>
<HelpTip>私钥会加密保存,服务器配置中只保存安全引用。</HelpTip>
<button class="primary-button" type="button" :disabled="sshKeySaving" @click="saveSSHKey">
{{ sshKeySaving ? "加密保存中……" : "加密保存并用于当前服务器" }}
</button>
</div>
</section>
<label class="field"
><span>SSH 主机 SHA256 指纹</span
><input
v-model="serverForm.host_key_fingerprint"
required
type="text"
placeholder="SHA256:..."
/></label>
<div class="server-paths">
<label class="field"
><span>远程证书路径</span
><input
v-model="serverForm.certificate_path"
required
type="text"
:placeholder="defaultCertificatePath"
/></label>
<label class="field"
><span>远程完整证书链路径</span
><input
v-model="serverForm.certificate_chain_path"
required
type="text"
:placeholder="defaultCertificateChainPath"
/></label>
<label class="field"
><span>远程私钥路径 <HelpTip>
保存的是路径模板,部署时会将 {域名} 和 {证书类型}
替换为实际值。证书类型目录固定为 single、wildcard 或 san。
</HelpTip></span
><input
v-model="serverForm.private_key_path"
required
type="text"
:placeholder="defaultPrivateKeyPath"
/></label>
</div>
<label class="field"
><span
>配置校验命令
<HelpTip
>每行一个参数;Nginx、Caddy、Apache 留空使用默认命令,自定义服务必须填写</HelpTip
></span
><textarea
v-model="serverForm.validation_command"
rows="2"
placeholder="nginx&#10;-t"
></textarea>
</label>
<label class="field"
><span
>重载命令
<HelpTip>每行一个参数;留空使用网站服务默认命令</HelpTip></span
><textarea
v-model="serverForm.reload_command"
rows="2"
placeholder="nginx&#10;-s&#10;reload"
></textarea>
</label>
<section class="server-form-section">
<div class="server-form-section-heading">
<div>
<p class="eyebrow">证书部署</p>
<h3>远程文件路径</h3>
</div>
<HelpTip>
路径支持 {域名} 和 {证书类型} 占位符,部署时会替换为实际目录。
</HelpTip>
</div>
<div class="server-paths">
<label class="field">
<span>远程证书路径</span>
<input
v-model="serverForm.certificate_path"
required
type="text"
:placeholder="defaultCertificatePath"
/>
</label>
<label class="field">
<span>远程完整证书链路径</span>
<input
v-model="serverForm.certificate_chain_path"
required
type="text"
:placeholder="defaultCertificateChainPath"
/>
</label>
<label class="field">
<span>远程私钥路径</span>
<input
v-model="serverForm.private_key_path"
required
type="text"
:placeholder="defaultPrivateKeyPath"
/>
</label>
</div>
</section>
<section class="server-form-section server-command-section">
<div class="server-form-section-heading">
<div>
<p class="eyebrow">部署命令</p>
<h3>服务校验与重载</h3>
</div>
<HelpTip>
每行填写一个命令参数;标准 Nginx、Caddy、Apache 可以留空使用默认命令,自定义服务需要填写。
</HelpTip>
</div>
<div class="server-form-grid">
<label class="field">
<span>配置校验命令</span>
<textarea
v-model="serverForm.validation_command"
rows="2"
placeholder="nginx&#10;-t"
></textarea>
</label>
<label class="field">
<span>重载命令</span>
<textarea
v-model="serverForm.reload_command"
rows="2"
placeholder="nginx&#10;-s&#10;reload"
></textarea>
</label>
</div>
</section>
<p v-if="serverFormError" class="form-error">{{ serverFormError }}</p>
<div class="modal-actions">
<button
@ -349,13 +388,10 @@ const {
:disabled="serverSaving"
@click="closeServerForm"
>
取消</button
><button
class="primary-button"
type="submit"
:disabled="serverSaving"
>
{{ serverSaving ? "保存中……" : "保存服务器" }}
取消
</button>
<button class="primary-button" type="submit" :disabled="serverSaving">
{{ serverSaving ? "保存中……" : "保存服务器配置" }}
</button>
</div>
</form>