拆分部署服务器弹窗并压缩单屏布局及说明提示

This commit is contained in:
xuans 2026-09-25 01:36:58 +08:00
parent 7ba299d607
commit 1f8495ad49
4 changed files with 374 additions and 171 deletions

View File

@ -364,3 +364,72 @@ test("domainModal.compilesAndRendersSanFieldsWithoutLiteralNewline", async () =>
}
}
});
test("serverModal.compilesAndPreservesDeploymentFields", async () => {
const { readFile } = await import("node:fs/promises");
const { parse, compileScript, compileTemplate } = await import("@vue/compiler-sfc");
const { renderToString } = await import("@vue/server-renderer");
const Vue = await import("vue");
const source = await readFile(new URL("./ServerModal.vue", import.meta.url), "utf8");
const { descriptor, errors } = parse(source);
assert.deepEqual(errors, []);
assert.ok(compileScript(descriptor, { id: "server-modal" }));
const compiled = compileTemplate({
source: descriptor.template.content,
filename: "ServerModal.vue",
id: "server-modal",
compilerOptions: { mode: "function" },
});
assert.deepEqual(compiled.errors, []);
const render = new Function("Vue", compiled.code)(Vue);
for (const webServer of ["nginx", "caddy", "apache", "custom"]) {
const app = Vue.createSSRApp({
render,
setup: () => ({
showServerForm: true,
closeServerForm: () => {},
serverEditingId: null,
serverForm: {
name: "test-server",
web_server: webServer,
address: "example.test",
port: 22,
username: "deploy",
private_key_ref: "key-ref",
host_key_fingerprint: "SHA256:test-fingerprint",
certificate_path: "/test/cert.pem",
certificate_chain_path: "/test/fullchain.pem",
private_key_path: "/test/key.pem",
validation_command: "nginx\n-t",
reload_command: "nginx\n-s\nreload",
},
sshKeys: [],
openSSHKeyCreate: () => {},
defaultCertificatePath: "",
defaultCertificateChainPath: "",
defaultPrivateKeyPath: "",
serverFormError: "test-validation-error",
serverSaving: false,
saveServer: () => {},
}),
});
app.component("HelpTip", {
setup: (_, { slots }) => () => Vue.h("span", { class: "test-help-tip" }, slots.default?.()),
});
const html = await renderToString(app);
assert.match(html, /class="modal-card server-modal"/);
assert.match(html, /class="server-paths"/);
assert.match(html, /type="submit"/);
assert.match(html, /SHA256:test-fingerprint/);
assert.match(html, /test-validation-error/);
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);
for (const path of ["/test/cert.pem", "/test/fullchain.pem", "/test/key.pem"]) {
assert.ok(html.includes(path));
}
assert.ok(html.includes("nginx\n-t"));
assert.ok(html.includes("nginx\n-s\nreload"));
}
});

View File

@ -3,6 +3,7 @@ import ChangePassword from "../components/ChangePassword/ChangePassword.vue";
import UserManagement from "../UserManagement/UserManagement.vue";
import SettingsModal from "./SettingsModal.vue";
import DomainModal from "./DomainModal.vue";
import ServerModal from "./ServerModal.vue";
import { useDashboard } from "./Dashboard.js";
const dashboard = useDashboard();
@ -471,177 +472,7 @@ const {
<DomainModal v-if="showForm" :state="dashboard" />
<div
v-if="showServerForm"
class="modal-backdrop"
@click.self="closeServerForm"
>
<section
class="modal-card server-modal"
role="dialog"
aria-modal="true"
aria-label="服务器配置"
>
<div class="modal-heading">
<div>
<p class="eyebrow">远程部署</p>
<h2>{{ serverEditingId ? "编辑部署服务器" : "添加部署服务器" }}</h2>
</div>
<button
class="close-button"
type="button"
aria-label="关闭"
@click="closeServerForm"
>
×
</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 class="field-label-row"
><span>SSH 私钥</span
><button
class="inline-button"
type="button"
@click="openSSHKeyCreate"
>
新建或导入
</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
><small v-if="sshKeys.length === 0"
>还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。</small
></label
>
</div>
<label class="field"
><span>SSH 主机 SHA256 指纹</span
><input
v-model="serverForm.host_key_fingerprint"
required
type="text"
placeholder="SHA256:..."
/></label>
<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>
<p class="field-hint">
保存的是路径模板,部署时会将 {域名} 和 {证书类型}
替换为实际值。证书类型目录固定为 single、wildcard 或 san。
</p>
<label class="field"
><span
>配置校验命令
<small
>每行一个参数;Nginx、Caddy、Apache 留空使用默认命令,自定义服务必须填写</small
></span
><textarea
v-model="serverForm.validation_command"
rows="3"
placeholder="nginx&#10;-t"
></textarea>
</label>
<label class="field"
><span
>重载命令
<small>每行一个参数;留空使用网站服务默认命令</small></span
><textarea
v-model="serverForm.reload_command"
rows="3"
placeholder="nginx&#10;-s&#10;reload"
></textarea>
</label>
<p v-if="serverFormError" class="form-error">{{ serverFormError }}</p>
<div class="modal-actions">
<button
class="secondary-button"
type="button"
:disabled="serverSaving"
@click="closeServerForm"
>
取消</button
><button
class="primary-button"
type="submit"
:disabled="serverSaving"
>
{{ serverSaving ? "保存中……" : "保存服务器" }}
</button>
</div>
</form>
</section>
</div>
<ServerModal v-if="showServerForm" :state="dashboard" />
<div
v-if="showSSHKeyForm"

View File

@ -0,0 +1,105 @@
.modal-backdrop.server-backdrop {
padding: 16px;
}
.server-modal {
box-sizing: border-box;
width: min(100%, 1120px);
max-height: calc(100dvh - 32px);
padding: 22px 28px;
}
.server-modal .modal-heading {
margin-bottom: 12px;
padding-bottom: 10px;
}
.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-paths,
.server-modal .form-error,
.server-modal .modal-actions {
grid-column: 1 / -1;
}
.server-modal .server-paths {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.server-modal .field {
min-width: 0;
margin-bottom: 10px;
gap: 5px;
}
.server-modal .field > span {
display: flex;
align-items: center;
gap: 6px;
min-height: 22px;
}
.server-modal .field input,
.server-modal .field select,
.server-modal .field textarea {
min-height: 36px;
padding: 8px 10px;
}
.server-modal .field textarea {
min-height: 76px;
}
.server-modal .form-error {
margin-bottom: 8px;
}
.server-modal .modal-actions {
padding-top: 10px;
border-top: 1px solid #dce6f1;
}
@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-paths {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 640px) {
.server-modal {
padding: 18px;
}
.server-modal form,
.server-modal .form-grid {
grid-template-columns: minmax(0, 1fr);
}
.server-modal form > .form-grid,
.server-modal form > .field {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,198 @@
<script setup>
import HelpTip from "../components/HelpTip/HelpTip.vue";
const props = defineProps({ state: { type: Object, required: true } });
const {
showServerForm,
closeServerForm,
serverEditingId,
serverForm,
sshKeys,
openSSHKeyCreate,
defaultCertificatePath,
defaultCertificateChainPath,
defaultPrivateKeyPath,
serverFormError,
serverSaving,
saveServer,
} = props.state;
</script>
<template>
<div
v-if="showServerForm"
class="modal-backdrop server-backdrop"
@click.self="closeServerForm"
>
<section
class="modal-card server-modal"
role="dialog"
aria-modal="true"
aria-label="服务器配置"
>
<div class="modal-heading">
<div>
<p class="eyebrow">远程部署</p>
<h2>{{ serverEditingId ? "编辑部署服务器" : "添加部署服务器" }}</h2>
</div>
<button
class="close-button"
type="button"
aria-label="关闭"
@click="closeServerForm"
>
×
</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 class="field-label-row"
><span>SSH 私钥</span
><button
class="inline-button"
type="button"
@click="openSSHKeyCreate"
>
新建或导入
</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>
<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>
<p v-if="serverFormError" class="form-error">{{ serverFormError }}</p>
<div class="modal-actions">
<button
class="secondary-button"
type="button"
:disabled="serverSaving"
@click="closeServerForm"
>
取消</button
><button
class="primary-button"
type="submit"
:disabled="serverSaving"
>
{{ serverSaving ? "保存中……" : "保存服务器" }}
</button>
</div>
</form>
</section>
</div>
</template>
<style scoped src="./Dashboard.css"></style>
<style scoped src="./ServerModal.css"></style>