fixup
This commit is contained in:
@@ -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 %}
|
||||
Reference in New Issue
Block a user