This commit is contained in:
2026-10-02 13:12:49 +08:00
parent 2d4bdd0f85
commit a8ab1dfcc6
38 changed files with 10385 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
{% extends "_shell.html" %}
{% block title %}{% if let Some(db) = editing %}編輯連接:{{ db.name }}{% else %}新增連接{% endif %} — alterdb{% endblock %}
{% block style %}
<style>
.form-card {
padding: 1.4rem 1.6rem;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 10px;
margin-bottom: 1.5rem;
}
.form-card h2 { margin: 0 0 1.1rem; }
.error {
margin: 0 0 1rem;
padding: .5rem .75rem;
border-radius: 6px;
color: light-dark(#b3261e, #ffb4ab);
background: color-mix(in srgb, currentColor 8%, transparent);
}
.db-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem 1.4rem;
}
.db-form label {
display: flex;
flex-direction: column;
gap: .35rem;
font-weight: 600;
}
.db-form input, .db-form select {
font: inherit;
padding: .45rem .65rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 6px;
background: transparent;
color: inherit;
}
.db-form .form-actions {
grid-column: 1 / -1;
display: flex;
gap: .6rem;
}
.db-form .form-actions button,
.db-form .form-actions a {
font: inherit;
font-weight: 600;
padding: .5rem 1.1rem;
border: none;
border-radius: 6px;
cursor: pointer;
color: inherit;
text-decoration: none;
background: color-mix(in srgb, currentColor 12%, transparent);
}
.db-form .form-actions a {
background: transparent;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}
.db-form .form-actions button:hover,
.db-form .form-actions a:hover { background: color-mix(in srgb, currentColor 22%, transparent); }
.db-form .hint { font-size: .85rem; opacity: .7; font-weight: 400; }
@media (max-width: 34rem) { .db-form { grid-template-columns: 1fr; } }
</style>
{% endblock %}
{% block page %}
<div class="form-card">
<h2>{% if let Some(db) = editing %}編輯連接:{{ db.name }}{% else %}新增連接{% endif %}</h2>
<p id="form-error" class="error" role="alert" hidden></p>
<form id="db-form" class="db-form"
data-mode="{% if editing.is_some() %}edit{% else %}create{% endif %}"
{% if let Some(db) = editing %}data-id="{{ db.id }}"{% endif %}>
<label>名稱
<input name="name" required maxlength="128" autocomplete="off"
value="{% if let Some(db) = editing %}{{ db.name }}{% endif %}">
</label>
<label>類型
<select name="type">
<option value="mysql"{% if let Some(db) = editing %}{% if db.db_type.as_str() == "mysql" %} selected{% endif %}{% endif %}>MySQL</option>
<option value="postgres"{% if let Some(db) = editing %}{% if db.db_type.as_str() == "postgres" %} selected{% endif %}{% endif %}>PostgreSQL</option>
</select>
</label>
<label>主機
<input name="host" required maxlength="255" autocomplete="off" placeholder="如 10.0.0.5 或 db.example.com"
value="{% if let Some(db) = editing %}{{ db.host }}{% endif %}">
</label>
<label>埠
<input name="port" type="number" min="1" max="65535" inputmode="numeric"
value="{% if let Some(db) = editing %}{{ db.port }}{% endif %}">
</label>
<label>用戶名
<input name="username" required maxlength="64" autocomplete="off"
value="{% if let Some(db) = editing %}{{ db.username }}{% endif %}">
</label>
<label>密碼
<input name="password" type="password" maxlength="128" autocomplete="new-password"
placeholder="{% if editing.is_some() %}留空保留原密碼{% else %}可留空{% endif %}">
</label>
<label>資料庫名
<input name="database_name" required maxlength="64" autocomplete="off"
value="{% if let Some(db) = editing %}{{ db.database_name }}{% endif %}">
</label>
<div class="form-actions">
<button type="submit">儲存</button>
<a href="/databases">取消</a>
</div>
<p class="hint" style="grid-column: 1 / -1; margin: 0;">埠留空按類型補默認(MySQL 3306 / PostgreSQL 5432);密碼不會顯示與回填。</p>
</form>
</div>
{% endblock %}
{% block script %}
<script>
(() => {
'use strict';
const TYPE_PORTS = { mysql: 3306, postgres: 5432 };
const form = document.getElementById('db-form');
const formError = document.getElementById('form-error');
const typeSelect = form.elements['type'];
const portInput = form.elements['port'];
const passwordInput = form.elements['password'];
const editing = form.dataset.mode === 'edit';
const editingId = form.dataset.id || null;
function updatePortHint() {
portInput.placeholder = '留空使用默認 ' + TYPE_PORTS[typeSelect.value];
}
updatePortHint();
async function api(path, options) {
const response = await fetch(path, options);
if (!response.ok) {
let message = '請求失敗(HTTP ' + response.status + ')';
try {
const body = await response.json();
if (body.error) { message = body.error; }
} catch (_) { /* 非 JSON 錯誤體,保留默認訊息 */ }
throw new Error(message);
}
return response.status === 204 ? null : response.json();
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
formError.hidden = true;
const payload = {
name: form.elements['name'].value.trim(),
type: typeSelect.value,
host: form.elements['host'].value.trim(),
username: form.elements['username'].value.trim(),
database_name: form.elements['database_name'].value.trim(),
};
const port = portInput.value.trim();
if (port !== '') { payload.port = Number(port); }
// 密碼留空時省略該鍵:新增視為空密碼(後端默認)、編輯保留原值
if (passwordInput.value !== '') { payload.password = passwordInput.value; }
const url = editing ? '/api/databases/' + editingId : '/api/databases';
try {
await api(url, {
method: editing ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
window.location.assign('/databases');
} catch (err) {
formError.textContent = err.message;
formError.hidden = false;
}
});
typeSelect.addEventListener('change', updatePortHint);
})();
</script>
{% endblock %}