Files
2026-10-02 13:12:49 +08:00

179 lines
5.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}