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
+178
View File
@@ -0,0 +1,178 @@
{% extends "_shell.html" %}
{% block title %}數據庫連接 — alterdb{% endblock %}
{% block style %}
<style>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.toolbar h2 { margin: 0; }
.toolbar a, .db-table .actions a, .db-table .actions button {
font: inherit;
padding: .35rem .9rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
text-decoration: none;
}
.toolbar a:hover, .db-table .actions a:hover, .db-table .actions button:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
.db-table .actions { white-space: nowrap; }
.db-table .actions a, .db-table .actions button {
padding: .2rem .6rem;
font-size: .85rem;
margin-left: .3rem;
}
.db-table .actions .danger { color: light-dark(#b3261e, #ffb4ab); }
.db-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
font-size: .95rem;
}
.db-table th, .db-table td {
padding: .5rem .6rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
text-align: left;
vertical-align: top;
}
.db-table th { font-weight: 600; }
.db-table td.type { text-transform: uppercase; font-size: .8rem; letter-spacing: .04em; }
.db-table td.empty { text-align: center; opacity: .7; padding: 1.5rem 0; }
.error {
margin: 0 0 1rem;
padding: .5rem .75rem;
border-radius: 6px;
color: light-dark(#b3261e, #ffb4ab);
background: color-mix(in srgb, currentColor 8%, transparent);
}
</style>
{% endblock %}
{% block page %}
<div class="toolbar">
<h2>數據庫連接</h2>
<a href="/databases/new">新增連接</a>
</div>
<p id="list-error" class="error" role="alert" hidden></p>
<table class="db-table">
<thead>
<tr>
<th>名稱</th><th>類型</th><th>主機:埠</th><th>資料庫</th><th>用戶</th><th>更新時間</th><th>操作</th>
</tr>
</thead>
<tbody id="db-rows"></tbody>
</table>
{% endblock %}
{% block script %}
<script>
(() => {
'use strict';
const listError = document.getElementById('list-error');
const tbody = document.getElementById('db-rows');
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();
}
function formatTime(iso) {
return iso.replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
}
function renderRows(databases) {
const rows = [];
if (databases.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 7;
td.className = 'empty';
td.textContent = '尚無連接,點擊「新增連接」建立第一個。';
tr.append(td);
rows.push(tr);
} else {
for (const item of databases) { rows.push(renderRow(item)); }
}
tbody.replaceChildren(...rows);
}
// 所有動態值一律經 textContent 寫入,避免以字串拼接 HTML 引入注入風險
function renderRow(item) {
const tr = document.createElement('tr');
// 名稱連到詳情頁(連接狀態與數據表瀏覽)
const nameCell = document.createElement('td');
const nameLink = document.createElement('a');
nameLink.href = '/databases/' + item.id;
nameLink.textContent = item.name;
nameCell.append(nameLink);
tr.append(nameCell);
const cells = [
{ text: item.type, className: 'type' },
{ text: item.host + ':' + item.port },
{ text: item.database_name },
{ text: item.username },
{ text: formatTime(item.updated_at) },
];
for (const cell of cells) {
const td = document.createElement('td');
td.textContent = cell.text;
if (cell.className) { td.className = cell.className; }
tr.append(td);
}
const actions = document.createElement('td');
actions.className = 'actions';
const editLink = document.createElement('a');
editLink.href = '/databases/' + item.id + '/edit';
editLink.textContent = '編輯';
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.textContent = '刪除';
deleteButton.className = 'danger';
deleteButton.addEventListener('click', () => removeDatabase(item));
actions.append(editLink, deleteButton);
tr.append(actions);
return tr;
}
async function loadList() {
listError.hidden = true;
try {
renderRows(await api('/api/databases'));
} catch (err) {
listError.textContent = err.message;
listError.hidden = false;
}
}
async function removeDatabase(item) {
if (!window.confirm('確定刪除連接「' + item.name + '」?')) { return; }
try {
await api('/api/databases/' + item.id, { method: 'DELETE' });
await loadList();
} catch (err) {
listError.textContent = err.message;
listError.hidden = false;
}
}
loadList();
})();
</script>
{% endblock %}