v2.5.0: replace keys table with responsive cards

This commit is contained in:
Виталий Литвинов
2026-04-25 15:52:17 +03:00
parent 7b1a79bba9
commit d62991857f
4 changed files with 143 additions and 34 deletions

View File

@@ -1138,12 +1138,12 @@ function renderUserTraffic() {
}
function renderUsers() {
const tbody = $("#usersTable");
const container = $("#usersTable");
if (!state.users.length) {
tbody.innerHTML = `<tr><td colspan="6" class="empty-cell">${escapeHtml(t("noKeys"))}</td></tr>`;
container.innerHTML = `<div class="empty empty-cell">${escapeHtml(t("noKeys"))}</div>`;
return;
}
tbody.innerHTML = state.users.map((user) => {
container.innerHTML = state.users.map((user) => {
const pending = state.pendingUsers.has(user.name);
const selected = user.name === state.userTrafficUser;
const traffic = user.traffic || {};
@@ -1151,13 +1151,12 @@ function renderUsers() {
const activeIps = Number(traffic.active_unique_ips) || 0;
const maxUniqueIps = Number.isFinite(Number(user.max_unique_ips)) ? Math.max(0, Number(user.max_unique_ips)) : 0;
return `
<tr class="${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""} ${selected ? "selected-row" : ""}" data-select-user-traffic="${escapeAttr(user.name)}" aria-selected="${selected ? "true" : "false"}">
<td data-label="${escapeAttr(t("tableUser"))}">
<article class="key-card ${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""} ${selected ? "selected-row" : ""}" data-select-user-traffic="${escapeAttr(user.name)}" aria-selected="${selected ? "true" : "false"}">
<div class="key-card-user">
<span class="field-label">${escapeHtml(t("tableUser"))}</span>
<button class="key-name-button" type="button" data-user-traffic="${escapeAttr(user.name)}">
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
</button>
</td>
<td data-label="${escapeAttr(t("tableStatus"))}">
<div class="status-control">
<label class="switch" title="${escapeAttr(user.main ? t("main") : (user.enabled ? t("disableKey") : t("enableKey")))}">
<input type="checkbox" data-toggle-user="${escapeAttr(user.name)}" ${user.enabled ? "checked" : ""} ${user.main || pending ? "disabled" : ""}>
@@ -1165,15 +1164,20 @@ function renderUsers() {
</label>
<strong class="${user.enabled ? "state-on" : "state-off"}">${escapeHtml(pending ? t("applying") : (user.enabled ? t("enabled") : t("disabled")))}</strong>
</div>
</td>
<td data-label="${escapeAttr(t("tableSecret"))}"><code title="${escapeAttr(user.secret)}">${escapeHtml(user.secret)}</code></td>
<td data-label="${escapeAttr(t("tableLink"))}">
</div>
<div class="key-card-secret">
<span class="field-label">${escapeHtml(t("tableSecret"))}</span>
<code title="${escapeAttr(user.secret)}">${escapeHtml(user.secret)}</code>
</div>
<div class="key-card-links">
<span class="field-label">${escapeHtml(t("tableLink"))}</span>
<div class="mini-actions">
<button class="soft" data-copy="${escapeAttr(user.link)}" ${user.enabled ? "" : "disabled"}>${escapeHtml(t("copyLink"))}</button>
<button class="soft" data-user-qr="${escapeAttr(user.name)}">${escapeHtml(t("showQr"))}</button>
</div>
</td>
<td data-label="${escapeAttr(t("tableTraffic"))}">
</div>
<div class="key-card-traffic">
<span class="field-label">${escapeHtml(t("tableTraffic"))}</span>
<div class="traffic-cell">
<div class="traffic-main">
<span>
@@ -1188,14 +1192,15 @@ function renderUsers() {
<button class="soft" type="submit" data-ip-limit-save="${escapeAttr(user.name)}">${escapeHtml(t("saveIpLimit"))}</button>
</form>
</div>
</td>
<td data-label="${escapeAttr(t("tableActions"))}">
</div>
<div class="key-card-actions">
<span class="field-label">${escapeHtml(t("tableActions"))}</span>
<div class="action-buttons">
<button class="soft" data-copy="${escapeAttr(user.secret)}">${escapeHtml(t("copySecret"))}</button>
<button class="danger" data-delete="${escapeAttr(user.name)}" ${user.main ? "disabled" : ""}>${escapeHtml(t("delete"))}</button>
</div>
</td>
</tr>
</div>
</article>
`; }).join("");
}

View File

@@ -194,20 +194,8 @@
<button type="submit" data-i18n="addKey">Add key</button>
</form>
</div>
<div class="table-wrap">
<table class="keys-table">
<thead>
<tr>
<th data-i18n="tableUser">User</th>
<th data-i18n="tableStatus">Status</th>
<th data-i18n="tableSecret">Secret</th>
<th data-i18n="tableLink">Link</th>
<th data-i18n="tableTraffic">Traffic</th>
<th data-i18n="tableActions">Actions</th>
</tr>
</thead>
<tbody id="usersTable"></tbody>
</table>
<div class="keys-wrap">
<div class="keys-list" id="usersTable" aria-live="polite"></div>
</div>
</div>
<div class="panel user-traffic-panel" id="userTrafficPanel">
@@ -400,6 +388,6 @@
</div>
</div>
</div>
<script src="/app.js?v=2.5.0-admin16" type="module"></script>
<script src="/app.js?v=2.5.0-admin17" type="module"></script>
</body>
</html>

View File

@@ -783,6 +783,79 @@ h2 {
border-radius: 8px;
}
.keys-wrap {
margin-top: 14px;
}
.keys-list {
display: grid;
gap: 12px;
}
.key-card {
display: grid;
grid-template-columns:
minmax(150px, .85fr)
minmax(260px, 1.35fr)
minmax(200px, 1fr)
minmax(260px, 1.1fr)
minmax(210px, .9fr);
grid-template-areas: "user secret links traffic actions";
gap: 12px;
align-items: stretch;
min-width: 0;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
cursor: pointer;
transition: background .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.key-card:hover {
background: color-mix(in srgb, var(--blue) 7%, var(--panel));
}
.key-card.selected-row {
border-color: color-mix(in srgb, var(--blue) 34%, var(--line));
background: color-mix(in srgb, var(--blue) 10%, var(--panel));
box-shadow: inset 4px 0 0 var(--blue);
}
.key-card-user,
.key-card-secret,
.key-card-links,
.key-card-traffic,
.key-card-actions {
display: grid;
align-content: center;
gap: 8px;
min-width: 0;
}
.key-card-user { grid-area: user; }
.key-card-secret { grid-area: secret; }
.key-card-links { grid-area: links; }
.key-card-traffic { grid-area: traffic; }
.key-card-actions { grid-area: actions; }
.key-card-secret code {
display: block;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
font-size: 13px;
line-height: 1.35;
}
.field-label {
color: var(--muted);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
}
table {
width: 100%;
min-width: 720px;
@@ -863,6 +936,10 @@ td small {
color: var(--muted);
}
.key-name-button small {
color: var(--muted);
}
.action-buttons {
display: flex;
align-items: center;
@@ -899,6 +976,24 @@ td small {
flex-wrap: nowrap;
}
.key-card .mini-actions,
.key-card .action-buttons {
display: grid;
grid-template-columns: 1fr;
align-content: center;
align-items: stretch;
gap: 8px;
width: 100%;
}
.key-card .mini-actions button,
.key-card .action-buttons button {
width: 100%;
min-height: 44px;
white-space: normal;
line-height: 1.2;
}
.traffic-cell {
display: grid;
gap: 10px;
@@ -1310,6 +1405,14 @@ td small {
}
@media (max-width: 1280px) {
.key-card {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-areas:
"user traffic"
"secret links"
"actions actions";
}
.service-grid {
grid-template-columns: repeat(3, minmax(150px, 1fr));
}
@@ -1488,6 +1591,16 @@ td small {
flex-direction: column;
}
.key-card {
grid-template-columns: 1fr;
grid-template-areas:
"user"
"secret"
"links"
"traffic"
"actions";
}
.ip-limit-control {
grid-template-columns: 1fr;
}