diff --git a/_public/static/admin/css/cache.css b/_public/static/admin/css/cache.css index 2b372953..1cdd38f3 100644 --- a/_public/static/admin/css/cache.css +++ b/_public/static/admin/css/cache.css @@ -302,3 +302,41 @@ box-shadow: 0 0 0 2px #000; border-color: #000; } + +.cache-table-scroll { + max-height: 60vh; + overflow: auto; +} + +.cache-pagination-bar { + margin-top: 12px; +} + +.pagination-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + .cache-table-scroll { + max-height: 52vh; + } +} + +.online-table-scroll { + max-height: 60vh; +} + +@media (max-width: 768px) { + .online-table-scroll { + max-height: 52vh; + } +} + +.cache-page-size-select { + width: 96px; + min-width: 96px; +} diff --git a/_public/static/admin/css/logs.css b/_public/static/admin/css/logs.css new file mode 100644 index 00000000..e8723350 --- /dev/null +++ b/_public/static/admin/css/logs.css @@ -0,0 +1,457 @@ +.panel-card { + background: rgba(255, 255, 255, 0.88); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04); + backdrop-filter: blur(10px); +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + z-index: 50; +} + +.modal-overlay.hidden { + display: none; +} + +.modal-content { + width: min(1100px, 100%); + background: #fff; + border-radius: 24px; + border: 1px solid var(--border); + box-shadow: 0 24px 80px rgba(15, 23, 42, 0.2); + padding: 20px; +} + +.modal-xl { + width: min(1200px, 100%); +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.modal-title { + font-size: 18px; + font-weight: 700; +} + +.modal-close { + border: 1px solid var(--border); + background: #fff; + border-radius: 999px; + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.auto-refresh-btn { + min-width: 124px; +} + +.auto-refresh-btn.auto-refresh-active { + border-color: rgba(0, 0, 0, 0.18); + background: #f5f5f5; +} + +.log-stat-card { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.92)); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px; +} + +.log-stat-label, +.filter-label, +.panel-subtle { + font-size: 12px; + color: var(--accents-4); +} + +.log-stat-value { + margin-top: 10px; + font-size: 28px; + line-height: 1; + font-weight: 700; + letter-spacing: -0.03em; +} + +.log-stat-hint { + margin-top: 8px; + font-size: 12px; + color: var(--accents-4); +} + +.panel-title-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} + +.panel-title { + font-size: 14px; + font-weight: 600; +} + +.file-panel-header { + margin-bottom: 0; +} + +.file-panel-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid var(--border); + background: #fff; + border-radius: 999px; + padding: 6px 10px; + font-size: 12px; + color: var(--accents-5); +} + +.file-panel-toggle-icon { + transition: transform 0.15s ease; +} + +.file-panel-toggle[aria-expanded="true"] .file-panel-toggle-icon { + transform: rotate(180deg); +} + +.desktop-hidden { + display: none; +} + +.file-panel-body { + margin-top: 14px; +} + +.file-panel-body.mobile-collapsed { + display: none; +} + +.file-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 12px; +} + +.log-file-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 960px; + overflow: auto; +} + +.log-file-item { + border: 1px solid var(--border); + border-radius: 14px; + padding: 12px; + cursor: pointer; + transition: all 0.15s ease; + background: #fff; +} + +.log-file-item:hover, +.log-file-item.active { + border-color: rgba(0, 0, 0, 0.2); + transform: translateY(-1px); + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06); +} + +.log-file-row { + display: flex; + gap: 10px; + align-items: flex-start; +} + +.log-file-body { + min-width: 0; + flex: 1; +} + +.log-file-name { + font-size: 13px; + font-weight: 600; + word-break: break-all; +} + +.log-file-meta { + margin-top: 6px; + display: flex; + justify-content: space-between; + gap: 10px; + font-size: 11px; + color: var(--accents-4); +} + +.log-list-scroll { + max-height: 72vh; + overflow: auto; + padding-right: 4px; +} + +.log-entry { + border: 1px solid var(--border); + border-radius: 18px; + background: rgba(255, 255, 255, 0.95); + overflow: hidden; +} + +.log-entry-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px 10px; +} + +.log-entry-main { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.log-entry-time { + font-size: 12px; + color: var(--accents-4); + white-space: nowrap; +} + +.log-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 74px; + height: 24px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + border: 0; +} + +.log-badge.debug, +.level-pill.debug { + color: #1d4ed8; + background: #dbeafe; +} + +.log-badge.info, +.level-pill.info { + color: #0369a1; + background: #e0f2fe; +} + +.log-badge.warning, +.level-pill.warning { + color: #b45309; + background: #fef3c7; +} + +.log-badge.error, +.level-pill.error { + color: #b91c1c; + background: #fee2e2; +} + +.log-entry-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.log-entry-body { + padding: 0 16px 16px; +} + +.log-entry-scroll { + max-height: 420px; + overflow: auto; + padding-right: 4px; +} + +.log-message { + font-size: 14px; + line-height: 1.6; + word-break: break-word; + white-space: pre-wrap; + overflow-wrap: anywhere; + max-height: 160px; + overflow: auto; + padding-right: 4px; +} + +.log-meta-grid { + margin-top: 12px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; +} + +.log-meta-card { + border: 1px solid rgba(0, 0, 0, 0.05); + background: #fafafa; + border-radius: 14px; + padding: 12px; +} + +.log-meta-key { + font-size: 11px; + color: var(--accents-4); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.log-meta-value { + font-size: 12px; + color: var(--accents-6); + word-break: break-word; + white-space: pre-wrap; + overflow-wrap: anywhere; + max-height: 96px; + overflow: auto; + padding-right: 4px; +} + +.log-stacktrace { + margin-top: 12px; + border-radius: 14px; + background: #0f172a; + color: #e2e8f0; + font-size: 12px; + line-height: 1.55; + padding: 14px; + white-space: pre-wrap; + overflow-wrap: anywhere; + overflow: auto; + max-height: 220px; +} + +.level-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 999px; + border: 1px solid transparent; + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; +} + +.level-pill:hover, +.level-pill.active { + border-color: rgba(0, 0, 0, 0.12); + transform: translateY(-1px); +} + +.level-pill-dot { + width: 8px; + height: 8px; + border-radius: 999px; +} + +.level-pill-dot.debug { background: #3b82f6; } +.level-pill-dot.info { background: #0ea5e9; } +.level-pill-dot.warning { background: #f59e0b; } +.level-pill-dot.error { background: #ef4444; } +.level-pill-dot.unknown { background: #94a3b8; } + +.log-modal-body { + margin-top: 14px; + border-radius: 16px; + background: #0b1120; + color: #dbeafe; + font-size: 12px; + line-height: 1.6; + padding: 16px; + overflow: auto; + max-height: min(70vh, 760px); +} + +@media (max-width: 768px) { + .modal-overlay { + padding: 12px; + } + + .modal-content { + padding: 16px; + border-radius: 18px; + } + + .panel-card.h-full { + padding-bottom: 14px; + } + + .file-panel { + padding-bottom: 14px; + } + + .log-entry-header { + flex-direction: column; + align-items: flex-start; + } + + .file-toolbar { + margin-bottom: 10px; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .log-file-list { + max-height: 248px; + overflow-y: auto; + padding-right: 4px; + -webkit-overflow-scrolling: touch; + } + + .log-file-item { + padding: 10px; + } + + .log-entry-actions { + width: 100%; + justify-content: flex-end; + } +} + +@media (max-width: 768px) { + .log-entry-scroll { + max-height: 360px; + } + + .log-message { + max-height: 132px; + } + + .log-meta-value { + max-height: 84px; + } +} + +@media (max-width: 768px) { + .log-list-scroll { + max-height: 68vh; + } +} diff --git a/_public/static/admin/css/token.css b/_public/static/admin/css/token.css index 8bf11b84..93b32768 100644 --- a/_public/static/admin/css/token.css +++ b/_public/static/admin/css/token.css @@ -74,6 +74,28 @@ padding: 8px 4px; } + + .token-table-scroll { + max-height: 68vh; + overflow: auto; + } + + .token-note-cell { + max-width: 220px; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + max-height: 80px; + overflow: auto; + padding-right: 4px; + } + + .token-table-card .font-mono { + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + } + /* Elegant Badges */ .badge { display: inline-flex; @@ -356,3 +378,14 @@ background: #e5e5e5; margin: 0 4px; } + + @media (max-width: 768px) { + .token-table-scroll { + max-height: 62vh; + } + + .token-note-cell { + max-width: 160px; + max-height: 72px; + } + } diff --git a/_public/static/admin/js/cache.js b/_public/static/admin/js/cache.js index 3b0ffc30..cccf1ff8 100644 --- a/_public/static/admin/js/cache.js +++ b/_public/static/admin/js/cache.js @@ -16,9 +16,10 @@ let currentBatchAction = null; let lastBatchAction = null; let isLocalDeleting = false; const cacheListState = { - image: { loaded: false, visible: false, items: [] }, - video: { loaded: false, visible: false, items: [] } + image: { loaded: false, visible: false, items: [], total: 0, page: 1, pageSize: 50 }, + video: { loaded: false, visible: false, items: [], total: 0, page: 1, pageSize: 50 } }; +const onlineTableState = { rows: [], page: 1, pageSize: 50 }; const UI_MAP = { imgCount: 'img-count', imgSize: 'img-size', @@ -29,6 +30,10 @@ const UI_MAP = { onlineLastClear: 'online-last-clear', accountTableBody: 'account-table-body', accountEmpty: 'account-empty', + onlinePaginationInfo: 'online-pagination-info', + onlinePagePrev: 'online-page-prev', + onlinePageNext: 'online-page-next', + onlinePageSize: 'online-page-size', selectAll: 'select-all', localImageSelectAll: 'local-image-select-all', localVideoSelectAll: 'local-video-select-all', @@ -41,6 +46,14 @@ const UI_MAP = { localVideoList: 'local-video-list', localImageBody: 'local-image-body', localVideoBody: 'local-video-body', + localImagePaginationInfo: 'local-image-pagination-info', + localVideoPaginationInfo: 'local-video-pagination-info', + localImagePagePrev: 'local-image-page-prev', + localVideoPagePrev: 'local-video-page-prev', + localImagePageNext: 'local-image-page-next', + localVideoPageNext: 'local-video-page-next', + localImagePageSize: 'local-image-page-size', + localVideoPageSize: 'local-video-page-size', onlineAssetsTable: 'online-assets-table', batchProgress: 'batch-progress', batchProgressText: 'batch-progress-text', @@ -311,18 +324,13 @@ function updateAccountSelect(accounts) { }); } -function renderAccountTable(data) { - const tbody = ui.accountTableBody; - const empty = ui.accountEmpty; - if (!tbody || !empty) return; - +function buildOnlineRows(data) { const details = Array.isArray(data.online_details) ? data.online_details : []; const accounts = Array.isArray(data.online_accounts) ? data.online_accounts : []; const detailsMap = new Map(details.map(item => [item.token, item])); - let rows = []; if (accounts.length > 0) { - rows = accounts.map(item => { + return accounts.map(item => { const detail = detailsMap.get(item.token); const state = accountStates.get(item.token); let count = '-'; @@ -352,8 +360,10 @@ function renderAccountTable(data) { last_asset_clear_at }; }); - } else if (details.length > 0) { - rows = details.map(item => ({ + } + + if (details.length > 0) { + return details.map(item => ({ token: item.token, token_masked: item.token_masked, pool: (accountMap.get(item.token) || {}).pool || '-', @@ -363,16 +373,32 @@ function renderAccountTable(data) { })); } + return []; +} + +function renderOnlineTablePage() { + const tbody = ui.accountTableBody; + const empty = ui.accountEmpty; + if (!tbody || !empty) return; + + const rows = onlineTableState.rows; + const totalPages = Math.max(1, Math.ceil(rows.length / onlineTableState.pageSize)); + onlineTableState.page = Math.min(Math.max(1, onlineTableState.page), totalPages); + if (rows.length === 0) { tbody.replaceChildren(); empty.classList.remove('hidden'); + renderOnlinePagination(); + syncSelectAllState(); + updateSelectedCount(); return; } empty.classList.add('hidden'); + const visibleRows = getVisibleOnlineRows(); const selected = selectedTokens; const fragment = document.createDocumentFragment(); - rows.forEach(row => { + visibleRows.forEach(row => { const tr = document.createElement('tr'); const isSelected = selected.has(row.token); if (isSelected) tr.classList.add('row-selected'); @@ -436,11 +462,22 @@ function renderAccountTable(data) { fragment.appendChild(tr); }); tbody.replaceChildren(fragment); + renderOnlinePagination(); syncSelectAllState(); updateSelectedCount(); updateBatchActionsVisibility(); } +function renderAccountTable(data) { + onlineTableState.rows = buildOnlineRows(data); + renderOnlineTablePage(); +} + +function getVisibleOnlineRows() { + const start = Math.max(0, (onlineTableState.page - 1) * onlineTableState.pageSize); + return onlineTableState.rows.slice(start, start + onlineTableState.pageSize); +} + async function clearCache(type) { const ok = await confirmAction(t(type === 'image' ? 'cache.confirmClearImage' : 'cache.confirmClearVideo'), { okText: t('cache.clear') }); if (!ok) return; @@ -495,10 +532,11 @@ function toggleSelect(token, checkbox) { function toggleSelectAll(checkbox) { const shouldSelect = checkbox.checked; - selectedTokens.clear(); - if (shouldSelect) { - accountMap.forEach((_, token) => selectedTokens.add(token)); - } + const visibleRows = getVisibleOnlineRows(); + visibleRows.forEach(row => { + if (shouldSelect) selectedTokens.add(row.token); + else selectedTokens.delete(row.token); + }); syncRowCheckboxes(); updateSelectedCount(); } @@ -534,6 +572,64 @@ function toggleLocalSelectAll(type, checkbox) { updateSelectedCount(); } + +function selectVisibleLocal(type) { + const set = selectedLocal[type]; + const items = cacheListState[type]?.items || []; + if (!set || items.length === 0) return; + items.forEach(item => { + if (item && item.name) set.add(item.name); + }); + syncLocalRowCheckboxes(type); + updateSelectedCount(); +} + +async function selectAllLocal(type) { + const state = cacheListState[type]; + const set = selectedLocal[type]; + if (!state || !set) return; + + const total = Number(state.total || 0); + if (total === 0) return; + + try { + const params = new URLSearchParams({ + type, + page: '1', + page_size: String(total), + }); + const res = await fetch(`/v1/admin/cache/list?${params.toString()}`, { + headers: buildAuthHeaders(apiKey), + }); + if (!res.ok) { + showToast(t('common.loadFailed'), 'error'); + return; + } + const data = await res.json(); + const items = Array.isArray(data.items) ? data.items : []; + set.clear(); + items.forEach((item) => { + if (item && item.name) set.add(item.name); + }); + syncLocalRowCheckboxes(type); + updateSelectedCount(); + } catch (error) { + showToast(t('common.loadFailed'), 'error'); + } +} + +function clearAllLocalSelection(type) { + const set = selectedLocal[type]; + if (!set || set.size === 0) return; + set.clear(); + syncLocalRowCheckboxes(type); + updateSelectedCount(); +} + +window.selectVisibleLocal = selectVisibleLocal; +window.selectAllLocal = selectAllLocal; +window.clearAllLocalSelection = clearAllLocalSelection; + function syncLocalRowCheckboxes(type) { const body = type === 'image' ? ui.localImageBody : ui.localVideoBody; if (!body) return; @@ -574,12 +670,81 @@ function syncRowCheckboxes() { function syncSelectAllState() { const selectAll = ui.selectAll; if (!selectAll) return; - const total = accountMap.size; - const selected = selectedTokens.size; + const visibleRows = getVisibleOnlineRows(); + const total = visibleRows.length; + const selected = visibleRows.filter(row => selectedTokens.has(row.token)).length; + selectAll.disabled = total === 0; selectAll.checked = total > 0 && selected === total; selectAll.indeterminate = selected > 0 && selected < total; } +function renderOnlinePagination() { + const info = ui.onlinePaginationInfo; + const prevBtn = ui.onlinePagePrev; + const nextBtn = ui.onlinePageNext; + const sizeSelect = ui.onlinePageSize; + if (!info || !prevBtn || !nextBtn || !sizeSelect) return; + + const totalCount = onlineTableState.rows.length; + const pageSize = Number(onlineTableState.pageSize || 50); + const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); + const currentPage = Math.min(Math.max(1, Number(onlineTableState.page || 1)), totalPages); + info.textContent = `第 ${totalCount === 0 ? 0 : currentPage} / ${totalPages} 页 · 共 ${totalCount} 条`; + prevBtn.disabled = currentPage <= 1 || totalCount === 0; + nextBtn.disabled = currentPage >= totalPages || totalCount === 0; + sizeSelect.value = String(pageSize); +} + +function selectVisibleAccounts() { + const visibleRows = getVisibleOnlineRows(); + if (visibleRows.length === 0) return; + visibleRows.forEach(row => selectedTokens.add(row.token)); + syncRowCheckboxes(); + updateSelectedCount(); +} + +function selectAllAccounts() { + if (onlineTableState.rows.length === 0) return; + onlineTableState.rows.forEach(row => selectedTokens.add(row.token)); + syncRowCheckboxes(); + updateSelectedCount(); +} + +function clearAllAccountSelection() { + if (selectedTokens.size === 0) return; + selectedTokens.clear(); + syncRowCheckboxes(); + updateSelectedCount(); +} + +function onlineGoPrevPage() { + if (onlineTableState.page <= 1) return; + onlineTableState.page -= 1; + renderOnlineTablePage(); +} + +function onlineGoNextPage() { + const totalPages = Math.max(1, Math.ceil(onlineTableState.rows.length / onlineTableState.pageSize)); + if (onlineTableState.page >= totalPages) return; + onlineTableState.page += 1; + renderOnlineTablePage(); +} + +function changeOnlinePageSize() { + const sizeSelect = ui.onlinePageSize; + if (!sizeSelect) return; + onlineTableState.pageSize = Number(sizeSelect.value || 50); + onlineTableState.page = 1; + renderOnlineTablePage(); +} + +window.selectVisibleAccounts = selectVisibleAccounts; +window.selectAllAccounts = selectAllAccounts; +window.clearAllAccountSelection = clearAllAccountSelection; +window.onlineGoPrevPage = onlineGoPrevPage; +window.onlineGoNextPage = onlineGoNextPage; +window.changeOnlinePageSize = changeOnlinePageSize; + function updateSelectedCount() { const el = ui.selectedCount; const selected = getActiveSelectedSet().size; @@ -728,7 +893,7 @@ async function showCacheSection(type) { if (type === 'image') { cacheListState.image.visible = true; cacheListState.video.visible = false; - if (cacheListState.image.loaded) renderLocalCacheList('image', cacheListState.image.items); + if (cacheListState.image.loaded) { renderLocalCacheList('image', cacheListState.image.items); renderLocalPagination('image'); } else await loadLocalCacheList('image'); if (ui.localCacheLists) ui.localCacheLists.classList.remove('hidden'); if (ui.localImageList) ui.localImageList.classList.remove('hidden'); @@ -740,7 +905,7 @@ async function showCacheSection(type) { if (type === 'video') { cacheListState.video.visible = true; cacheListState.image.visible = false; - if (cacheListState.video.loaded) renderLocalCacheList('video', cacheListState.video.items); + if (cacheListState.video.loaded) { renderLocalCacheList('video', cacheListState.video.items); renderLocalPagination('video'); } else await loadLocalCacheList('video'); if (ui.localCacheLists) ui.localCacheLists.classList.remove('hidden'); if (ui.localVideoList) ui.localVideoList.classList.remove('hidden'); @@ -766,10 +931,15 @@ async function toggleCacheList(type) { async function loadLocalCacheList(type) { const body = type === 'image' ? ui.localImageBody : ui.localVideoBody; - if (!body) return; + const state = cacheListState[type]; + if (!body || !state) return; body.innerHTML = `${t('common.loading')}`; try { - const params = new URLSearchParams({ type, page: '1', page_size: '1000' }); + const params = new URLSearchParams({ + type, + page: String(state.page || 1), + page_size: String(state.pageSize || 50) + }); const res = await fetch(`/v1/admin/cache/list?${params.toString()}`, { headers: buildAuthHeaders(apiKey) }); @@ -779,16 +949,21 @@ async function loadLocalCacheList(type) { } const data = await res.json(); const items = Array.isArray(data.items) ? data.items : []; - cacheListState[type].items = items; - cacheListState[type].loaded = true; + state.items = items; + state.total = Number(data.total || 0); + state.page = Number(data.page || state.page || 1); + state.pageSize = Number(data.page_size || state.pageSize || 50); + state.loaded = true; const keep = new Set(items.map(item => item.name)); const selected = selectedLocal[type]; Array.from(selected).forEach(name => { if (!keep.has(name)) selected.delete(name); }); renderLocalCacheList(type, items); + renderLocalPagination(type); } catch (e) { body.innerHTML = `${t('common.loadFailed')}`; + renderLocalPagination(type); } } @@ -798,6 +973,7 @@ function renderLocalCacheList(type, items) { if (!items || items.length === 0) { body.innerHTML = `${t('cache.noFiles')}`; syncLocalSelectAllState(type); + renderLocalPagination(type); return; } const selected = selectedLocal[type]; @@ -873,6 +1049,53 @@ function renderLocalCacheList(type, items) { updateSelectedCount(); } +function renderLocalPagination(type) { + const state = cacheListState[type]; + const info = type === 'image' ? ui.localImagePaginationInfo : ui.localVideoPaginationInfo; + const prevBtn = type === 'image' ? ui.localImagePagePrev : ui.localVideoPagePrev; + const nextBtn = type === 'image' ? ui.localImagePageNext : ui.localVideoPageNext; + const sizeSelect = type === 'image' ? ui.localImagePageSize : ui.localVideoPageSize; + if (!state || !info || !prevBtn || !nextBtn || !sizeSelect) return; + + const totalCount = Number(state.total || 0); + const pageSize = Number(state.pageSize || 50); + const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); + const currentPage = Math.min(Math.max(1, Number(state.page || 1)), totalPages); + info.textContent = `第 ${totalCount === 0 ? 0 : currentPage} / ${totalPages} 页 · 共 ${totalCount} 条`; + prevBtn.disabled = currentPage <= 1 || totalCount === 0; + nextBtn.disabled = currentPage >= totalPages || totalCount === 0; + sizeSelect.value = String(pageSize); +} + +async function localGoPrevPage(type) { + const state = cacheListState[type]; + if (!state || state.page <= 1) return; + state.page -= 1; + await loadLocalCacheList(type); +} + +async function localGoNextPage(type) { + const state = cacheListState[type]; + if (!state) return; + const totalPages = Math.max(1, Math.ceil((state.total || 0) / (state.pageSize || 50))); + if (state.page >= totalPages) return; + state.page += 1; + await loadLocalCacheList(type); +} + +async function changeLocalPageSize(type) { + const state = cacheListState[type]; + const sizeSelect = type === 'image' ? ui.localImagePageSize : ui.localVideoPageSize; + if (!state || !sizeSelect) return; + state.pageSize = Number(sizeSelect.value || 50); + state.page = 1; + await loadLocalCacheList(type); +} + +window.localGoPrevPage = localGoPrevPage; +window.localGoNextPage = localGoNextPage; +window.changeLocalPageSize = changeLocalPageSize; + function viewLocalFile(type, name) { const safeName = encodeURIComponent(name); const url = type === 'image' ? `/v1/files/image/${safeName}` : `/v1/files/video/${safeName}`; @@ -887,10 +1110,9 @@ async function deleteLocalFile(type, name) { showToast(t('common.deleteSuccess'), 'success'); const state = cacheListState[type]; if (state && Array.isArray(state.items)) { - state.items = state.items.filter(item => item.name !== name); - state.loaded = true; selectedLocal[type]?.delete(name); - if (state.visible) renderLocalCacheList(type, state.items); + state.loaded = false; + if (state.visible) await loadLocalCacheList(type); } await loadStats(); } @@ -1099,7 +1321,7 @@ async function startBatchLoad(tokens) { setOnlineStatus(t('cache.loadingStatus'), 'text-xs text-blue-600 mt-1'); updateLoadButton(); if (accountMap.size > 0) { - renderAccountTable({ online_accounts: Array.from(accountMap.values()), online_details: [], online: {} }); + renderOnlineTablePage(); } refreshBatchUI(); diff --git a/_public/static/admin/js/logs.js b/_public/static/admin/js/logs.js new file mode 100644 index 00000000..c0d8f07a --- /dev/null +++ b/_public/static/admin/js/logs.js @@ -0,0 +1,676 @@ +let adminAuthHeader = ''; +let logFiles = []; +let currentResponse = null; +let selectedFiles = new Set(); +let autoRefreshTimer = null; +let autoRefreshIndex = -1; +let autoRefreshLongPressTimer = null; +let autoRefreshLongPressTriggered = false; +let isRefreshing = false; +let currentFileName = ''; +let isMobileFilePanelCollapsed = false; + +const AUTO_REFRESH_OPTIONS = [5000, 10000, 30000]; +const logsById = (id) => document.getElementById(id); +const isMobileViewport = () => window.innerWidth <= 768; +const t = (key, vars = {}) => (window.I18n?.t ? I18n.t(key, vars) : key); + +document.addEventListener('DOMContentLoaded', async () => { + adminAuthHeader = await ensureAdminKey(); + if (!adminAuthHeader) return; + + bindEvents(); + syncFilePanelMode(); + if (window.I18n?.onReady) { + I18n.onReady(() => { + updateAutoRefreshButton(); + updateFileCountSummary(); + updateStats(currentResponse); + }); + } else { + updateAutoRefreshButton(); + } + await loadFiles(); + syncAutoRefresh(); +}); + +function bindEvents() { + logsById('refresh-btn')?.addEventListener('click', async () => { + await refreshCurrentView(); + }); + + const autoRefreshBtn = logsById('auto-refresh-btn'); + autoRefreshBtn?.addEventListener('click', (event) => { + if (autoRefreshLongPressTriggered) { + autoRefreshLongPressTriggered = false; + event.preventDefault(); + return; + } + cycleAutoRefresh(); + }); + autoRefreshBtn?.addEventListener('mousedown', startAutoRefreshLongPress); + autoRefreshBtn?.addEventListener('touchstart', startAutoRefreshLongPress, { passive: true }); + autoRefreshBtn?.addEventListener('mouseup', cancelAutoRefreshLongPress); + autoRefreshBtn?.addEventListener('mouseleave', cancelAutoRefreshLongPress); + autoRefreshBtn?.addEventListener('touchend', cancelAutoRefreshLongPress); + autoRefreshBtn?.addEventListener('touchcancel', cancelAutoRefreshLongPress); + + logsById('apply-btn')?.addEventListener('click', async () => { + await loadLogs(); + }); + + logsById('reset-btn')?.addEventListener('click', async () => { + logsById('log-level').value = ''; + logsById('log-limit').value = '200'; + logsById('log-keyword').value = ''; + logsById('exclude-admin-routes').checked = true; + updateClearLevelButton(); + await loadLogs(); + }); + + logsById('clear-level-btn')?.addEventListener('click', async () => { + logsById('log-level').value = ''; + updateClearLevelButton(); + await loadLogs(); + }); + + logsById('log-keyword')?.addEventListener('keydown', async (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + await loadLogs(); + } + }); + + logsById('select-all-files')?.addEventListener('change', (event) => { + toggleSelectAllFiles(Boolean(event.target.checked)); + }); + + logsById('delete-selected-btn')?.addEventListener('click', async () => { + await deleteSelectedFiles(); + }); + + logsById('file-panel-toggle')?.addEventListener('click', () => { + toggleFilePanel(); + }); + + window.addEventListener('resize', () => { + syncFilePanelMode(); + }); + + logsById('close-modal-btn')?.addEventListener('click', closeModal); + logsById('log-modal')?.addEventListener('click', (event) => { + if (event.target.id === 'log-modal') closeModal(); + }); +} + +async function loadFiles(keepSelection = false) { + setLoading(true); + try { + const previousSelection = keepSelection ? currentFileName : ''; + const res = await fetch('/v1/admin/logs/files', { + headers: buildAuthHeaders(adminAuthHeader), + }); + if (!res.ok) throw new Error(await getErrorMessage(res)); + const data = await res.json(); + logFiles = Array.isArray(data.files) ? data.files : []; + selectedFiles = new Set([...selectedFiles].filter((name) => logFiles.some((item) => item.name === name))); + currentFileName = resolveCurrentFileName(previousSelection); + renderFileList(); + updateFileCountSummary(); + if (currentFileName) { + await loadLogs(); + } else { + renderEmptyFiles(); + } + } catch (error) { + showToast(error.message || t('logs.loadFilesFailed'), 'error'); + renderEmptyFiles(); + } finally { + setLoading(false); + } +} + +function resolveCurrentFileName(previousSelection) { + if (previousSelection && logFiles.some((item) => item.name === previousSelection)) { + return previousSelection; + } + return logFiles[0]?.name || ''; +} + +async function refreshCurrentView() { + if (isRefreshing) return; + isRefreshing = true; + try { + await loadFiles(true); + } finally { + isRefreshing = false; + } +} + +async function loadLogs() { + const file = currentFileName; + if (!file) { + renderEntries([]); + renderLevelBreakdown({}); + updateStats(null); + return; + } + + setLoading(true); + try { + const params = new URLSearchParams({ + file, + limit: logsById('log-limit').value || '200', + }); + const level = logsById('log-level').value; + const keyword = logsById('log-keyword').value.trim(); + if (level) params.set('level', level); + if (keyword) params.set('keyword', keyword); + if (logsById('exclude-admin-routes').checked) { + params.set('exclude_admin_routes', 'true'); + } + + const res = await fetch(`/v1/admin/logs?${params.toString()}`, { + headers: buildAuthHeaders(adminAuthHeader), + }); + if (!res.ok) throw new Error(await getErrorMessage(res)); + currentResponse = await res.json(); + renderEntries(currentResponse.entries || []); + renderLevelBreakdown(currentResponse.stats?.levels || {}); + updateStats(currentResponse); + updateClearLevelButton(); + syncFileListState(currentFileName); + } catch (error) { + renderEntries([]); + renderLevelBreakdown({}); + updateStats(null); + showToast(error.message || t('logs.loadFailed'), 'error'); + } finally { + setLoading(false); + } +} + +function renderFileList() { + const container = logsById('log-file-list'); + const count = logsById('file-count-summary'); + const selectAll = logsById('select-all-files'); + if (!container || !count || !selectAll) return; + + count.textContent = t('logs.fileCount', { count: logFiles.length }); + container.replaceChildren(); + + logFiles.forEach((item) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = `log-file-item ${item.name === currentFileName ? 'active' : ''}`; + + const row = document.createElement('div'); + row.className = 'log-file-row'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'checkbox log-file-check'; + checkbox.checked = selectedFiles.has(item.name); + + const body = document.createElement('div'); + body.className = 'log-file-body'; + + const name = document.createElement('div'); + name.className = 'log-file-name font-mono'; + name.textContent = item.name; + + const meta = document.createElement('div'); + meta.className = 'log-file-meta'; + const updated = document.createElement('span'); + updated.textContent = formatDate(item.updated_at); + const size = document.createElement('span'); + size.textContent = formatBytes(item.size); + meta.append(updated, size); + + body.append(name, meta); + row.append(checkbox, body); + button.appendChild(row); + + button.addEventListener('click', async () => { + currentFileName = item.name; + syncFileListState(item.name); + if (isMobileViewport()) { + setMobileFilePanelCollapsed(true); + } + await loadLogs(); + }); + + checkbox.addEventListener('click', (event) => { + event.stopPropagation(); + }); + checkbox.addEventListener('change', (event) => { + if (event.target.checked) { + selectedFiles.add(item.name); + } else { + selectedFiles.delete(item.name); + } + updateFileSelectionState(); + }); + + container.appendChild(button); + }); + + updateFileSelectionState(); +} + +function renderEntries(entries) { + const list = logsById('log-list'); + const empty = logsById('empty-state'); + if (!list || !empty) return; + + list.replaceChildren(); + empty.classList.toggle('hidden', entries.length > 0); + + entries.forEach((entry) => { + const card = document.createElement('article'); + card.className = 'log-entry'; + + const header = document.createElement('div'); + header.className = 'log-entry-header'; + + const main = document.createElement('div'); + main.className = 'log-entry-main'; + + const levelBtn = document.createElement('button'); + levelBtn.type = 'button'; + levelBtn.className = `log-badge ${sanitizeLevelClass(entry.level)}`; + levelBtn.dataset.level = entry.level || ''; + levelBtn.textContent = entry.level || 'unknown'; + + const time = document.createElement('div'); + time.className = 'log-entry-time font-mono'; + time.textContent = entry.time_display || '-'; + + const caller = document.createElement('div'); + caller.className = 'text-xs text-[var(--accents-4)] font-mono'; + caller.textContent = entry.caller || '-'; + + main.append(levelBtn, time, caller); + + const actions = document.createElement('div'); + actions.className = 'log-entry-actions'; + + const copyBtn = document.createElement('button'); + copyBtn.type = 'button'; + copyBtn.className = 'geist-button-outline text-xs h-8 px-3'; + copyBtn.dataset.action = 'copy'; + copyBtn.textContent = t('common.copy'); + + const rawBtn = document.createElement('button'); + rawBtn.type = 'button'; + rawBtn.className = 'geist-button-outline text-xs h-8 px-3'; + rawBtn.dataset.action = 'raw'; + rawBtn.textContent = t('common.raw'); + + actions.append(copyBtn, rawBtn); + header.append(main, actions); + + const body = document.createElement('div'); + body.className = 'log-entry-body'; + const scroll = document.createElement('div'); + scroll.className = 'log-entry-scroll'; + + const message = document.createElement('div'); + message.className = 'log-message'; + message.textContent = entry.msg || ''; + scroll.appendChild(message); + + const extras = extractExtras(entry); + if (extras.length) { + const extrasGrid = document.createElement('div'); + extrasGrid.className = 'log-meta-grid'; + extras.forEach(([key, value]) => { + const extraCard = document.createElement('div'); + extraCard.className = 'log-meta-card'; + const extraKey = document.createElement('div'); + extraKey.className = 'log-meta-key'; + extraKey.textContent = key; + const extraValue = document.createElement('div'); + extraValue.className = 'log-meta-value font-mono'; + extraValue.textContent = stringifyValue(value); + extraCard.append(extraKey, extraValue); + extrasGrid.appendChild(extraCard); + }); + scroll.appendChild(extrasGrid); + } + + if (entry.stacktrace) { + const stacktrace = document.createElement('pre'); + stacktrace.className = 'log-stacktrace font-mono'; + stacktrace.textContent = entry.stacktrace; + scroll.appendChild(stacktrace); + } + + body.appendChild(scroll); + card.append(header, body); + + copyBtn.addEventListener('click', async () => { + await copyText(entry.raw || JSON.stringify(entry, null, 2)); + }); + rawBtn.addEventListener('click', () => { + openModal(entry); + }); + levelBtn.addEventListener('click', async () => { + await setLevelFilter(entry.level || ''); + }); + + list.appendChild(card); + }); +} + +function renderLevelBreakdown(levels) { + const container = logsById('level-breakdown'); + if (!container) return; + container.replaceChildren(); + const activeLevel = logsById('log-level').value; + const names = Object.keys(levels).sort((a, b) => (levels[b] || 0) - (levels[a] || 0)); + + names.forEach((level) => { + const pill = document.createElement('button'); + pill.type = 'button'; + pill.className = `level-pill ${sanitizeLevelClass(level)} ${activeLevel === level ? 'active' : ''}`; + + const dot = document.createElement('span'); + dot.className = `level-pill-dot ${sanitizeLevelClass(level)}`; + const label = document.createElement('span'); + label.className = 'font-mono'; + label.textContent = level; + const count = document.createElement('strong'); + count.textContent = String(levels[level]); + + pill.append(dot, label, count); + pill.addEventListener('click', async () => { + await setLevelFilter(activeLevel === level ? '' : level); + }); + container.appendChild(pill); + }); +} + +async function setLevelFilter(level) { + logsById('log-level').value = level; + updateClearLevelButton(); + await loadLogs(); +} + +function updateClearLevelButton() { + const button = logsById('clear-level-btn'); + if (!button) return; + button.classList.toggle('hidden', !logsById('log-level').value); +} + +function updateStats(response) { + const fileName = response?.file?.name || currentFileName || '-'; + const updated = response?.file?.updated_at ? formatDate(response.file.updated_at) : '-'; + const totalSize = logFiles.reduce((sum, item) => sum + Number(item?.size || 0), 0); + const matched = response?.stats?.matched || 0; + const warning = response?.stats?.levels?.warning || 0; + const error = response?.stats?.levels?.error || 0; + + logsById('stat-file').textContent = fileName; + logsById('stat-updated').textContent = updated; + logsById('stat-size').textContent = formatBytes(totalSize); + logsById('stat-matched').textContent = `${matched}`; + logsById('stat-risk').textContent = `${warning + error}`; +} + +function renderEmptyFiles() { + const list = logsById('log-file-list'); + if (list) { + const empty = document.createElement('div'); + empty.className = 'table-empty'; + empty.textContent = t('logs.noFiles'); + list.replaceChildren(empty); + } + currentFileName = ''; + selectedFiles.clear(); + updateFileCountSummary(); + updateFileSelectionState(); + renderEntries([]); + renderLevelBreakdown({}); + updateStats(null); +} + +function updateFileCountSummary() { + const summary = logsById('file-count-summary'); + if (summary) { + summary.textContent = t('logs.fileCount', { count: logFiles.length }); + } +} + +function setLoading(loading) { + logsById('loading-state')?.classList.toggle('hidden', !loading); +} + +function syncFileListState(activeName) { + document.querySelectorAll('.log-file-item').forEach((item) => { + const name = item.querySelector('.log-file-name')?.textContent; + item.classList.toggle('active', name === activeName); + }); +} + +function toggleSelectAllFiles(checked) { + selectedFiles = checked ? new Set(logFiles.map((item) => item.name)) : new Set(); + renderFileList(); +} + +function updateFileSelectionState() { + const selectAll = logsById('select-all-files'); + if (selectAll) { + selectAll.checked = logFiles.length > 0 && selectedFiles.size === logFiles.length; + selectAll.indeterminate = selectedFiles.size > 0 && selectedFiles.size < logFiles.length; + } + const deleteButton = logsById('delete-selected-btn'); + if (deleteButton) { + deleteButton.disabled = selectedFiles.size === 0; + } +} + +async function deleteSelectedFiles() { + const files = [...selectedFiles]; + if (!files.length) { + showToast(t('logs.deleteNone'), 'error'); + return; + } + if (!window.confirm(t('logs.deleteConfirm', { count: files.length }))) { + return; + } + + try { + const res = await fetch('/v1/admin/logs/delete', { + method: 'POST', + headers: { + ...buildAuthHeaders(adminAuthHeader), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ files }), + }); + if (!res.ok) throw new Error(await getErrorMessage(res)); + const data = await res.json(); + selectedFiles.clear(); + const deleted = Number(data.deleted?.length || 0); + const failed = Number(data.failed?.length || 0); + showToast(t('logs.deleteResult', { deleted, failed }), failed > 0 ? 'info' : 'success'); + await loadFiles(true); + } catch (error) { + showToast(error.message || t('logs.deleteFailed'), 'error'); + } +} + +function cycleAutoRefresh() { + autoRefreshIndex += 1; + if (autoRefreshIndex >= AUTO_REFRESH_OPTIONS.length) { + autoRefreshIndex = -1; + } + syncAutoRefresh(); + updateAutoRefreshButton(); + const label = autoRefreshIndex >= 0 + ? t('logs.autoRefresh.intervalLabel', { seconds: AUTO_REFRESH_OPTIONS[autoRefreshIndex] / 1000 }) + : t('logs.autoRefresh.offShort'); + showToast(t('logs.autoRefresh.changed', { label }), 'success'); +} + +function syncAutoRefresh() { + if (autoRefreshTimer) { + clearInterval(autoRefreshTimer); + autoRefreshTimer = null; + } + + if (autoRefreshIndex < 0) { + return; + } + + const interval = AUTO_REFRESH_OPTIONS[autoRefreshIndex]; + autoRefreshTimer = window.setInterval(async () => { + if (document.hidden) return; + await refreshCurrentView(); + }, interval); +} + +function updateAutoRefreshButton() { + const button = logsById('auto-refresh-btn'); + if (!button) return; + const text = autoRefreshIndex < 0 + ? t('logs.autoRefresh.off') + : t('logs.autoRefresh.interval', { seconds: AUTO_REFRESH_OPTIONS[autoRefreshIndex] / 1000 }); + button.textContent = text; + button.setAttribute('aria-label', t('logs.autoRefresh.buttonAria')); + button.classList.toggle('auto-refresh-active', autoRefreshIndex >= 0); +} + +function startAutoRefreshLongPress() { + cancelAutoRefreshLongPress(); + autoRefreshLongPressTriggered = false; + autoRefreshLongPressTimer = window.setTimeout(() => { + if (autoRefreshIndex >= 0) { + autoRefreshIndex = -1; + autoRefreshLongPressTriggered = true; + syncAutoRefresh(); + updateAutoRefreshButton(); + showToast(t('logs.autoRefresh.disabled'), 'success'); + } + autoRefreshLongPressTimer = null; + }, 600); +} + +function cancelAutoRefreshLongPress() { + if (autoRefreshLongPressTimer) { + clearTimeout(autoRefreshLongPressTimer); + autoRefreshLongPressTimer = null; + } +} + +function toggleFilePanel() { + if (!isMobileViewport()) return; + setMobileFilePanelCollapsed(!isMobileFilePanelCollapsed); +} + +function syncFilePanelMode() { + if (isMobileViewport()) { + if (!logsById('file-panel-body')?.dataset.initialized) { + logsById('file-panel-body').dataset.initialized = 'true'; + isMobileFilePanelCollapsed = true; + } + setMobileFilePanelCollapsed(isMobileFilePanelCollapsed); + } else { + isMobileFilePanelCollapsed = false; + logsById('file-panel-body')?.classList.remove('mobile-collapsed'); + logsById('file-panel-toggle')?.classList.add('desktop-hidden'); + logsById('file-panel-toggle')?.setAttribute('aria-expanded', 'true'); + } +} + +function setMobileFilePanelCollapsed(collapsed) { + isMobileFilePanelCollapsed = collapsed; + const body = logsById('file-panel-body'); + const toggle = logsById('file-panel-toggle'); + const text = logsById('file-panel-toggle-text'); + if (!body || !toggle || !text) return; + + if (isMobileViewport()) { + toggle.classList.remove('desktop-hidden'); + body.classList.toggle('mobile-collapsed', collapsed); + toggle.setAttribute('aria-expanded', String(!collapsed)); + text.textContent = collapsed ? t('logs.toggle.expand') : t('logs.toggle.collapse'); + } else { + body.classList.remove('mobile-collapsed'); + toggle.classList.add('desktop-hidden'); + toggle.setAttribute('aria-expanded', 'true'); + text.textContent = t('logs.toggle.expand'); + } +} + +function extractExtras(entry) { + const hiddenKeys = new Set(['time', 'time_display', 'level', 'msg', 'caller', 'stacktrace', 'raw']); + return Object.entries(entry).filter(([key, value]) => !hiddenKeys.has(key) && value !== null && value !== ''); +} + +function openModal(entry) { + logsById('log-modal-meta').textContent = `${entry.time_display || '-'} · ${entry.level || 'unknown'} · ${entry.caller || '-'}`; + logsById('log-modal-body').textContent = safePrettyJson(entry.raw); + logsById('log-modal').classList.remove('hidden'); +} + +function closeModal() { + logsById('log-modal').classList.add('hidden'); +} + +async function copyText(text) { + try { + await navigator.clipboard.writeText(text); + showToast(t('common.copied'), 'success'); + } catch (error) { + showToast(t('common.copyFailed'), 'error'); + } +} + +async function getErrorMessage(res) { + try { + const data = await res.json(); + return data.detail || data.message || `${res.status}`; + } catch (error) { + return `${res.status}`; + } +} + +function safePrettyJson(raw) { + try { + return JSON.stringify(JSON.parse(raw), null, 2); + } catch (error) { + return raw || ''; + } +} + +function stringifyValue(value) { + if (typeof value === 'string') return value; + return JSON.stringify(value); +} + +function formatDate(value) { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); +} + +function formatBytes(bytes) { + const size = Number(bytes || 0); + if (size < 1024) return `${size} B`; + const units = ['KB', 'MB', 'GB', 'TB']; + let value = size / 1024; + let unitIndex = 0; + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`; +} + +function sanitizeLevelClass(level) { + const normalized = String(level || '').toLowerCase(); + return ['debug', 'info', 'warning', 'error'].includes(normalized) ? normalized : 'unknown'; +} diff --git a/_public/static/admin/js/token.js b/_public/static/admin/js/token.js index 219a4c16..ff8877f2 100644 --- a/_public/static/admin/js/token.js +++ b/_public/static/admin/js/token.js @@ -295,7 +295,7 @@ function renderTable() { // Note (Left) const tdNote = document.createElement('td'); - tdNote.className = 'text-left text-gray-500 text-xs truncate max-w-[150px]'; + tdNote.className = 'text-left text-gray-500 text-xs token-note-cell'; tdNote.innerText = item.note || '-'; // Actions (Center) diff --git a/_public/static/admin/pages/cache.html b/_public/static/admin/pages/cache.html index 2f283e75..35476938 100644 --- a/_public/static/admin/pages/cache.html +++ b/_public/static/admin/pages/cache.html @@ -88,60 +88,123 @@

缓存 + +
+
+
+ - - - + + + + - +
- + 文件大小时间Token类型资产数上次清空时间 操作
+
-
-
- - - - - - - - - - - - -
- - Token类型资产数上次清空时间操作
- +
+
第 0 / 0 页 · 共 0 条
+
+ + + + + + +
+
diff --git a/_public/static/admin/pages/logs.html b/_public/static/admin/pages/logs.html new file mode 100644 index 00000000..abf347a5 --- /dev/null +++ b/_public/static/admin/pages/logs.html @@ -0,0 +1,167 @@ + + + + + + + Grok2API - 日志查看 + + + + + + + + + + +
+ +
+
+
+
+

日志查看

+

按文件、级别和关键词快速筛选,并以更易读的方式查看结构化日志。

+
+
+ + +
+
+ +
+
+
当前文件
+
-
+
-
+
+
+
已加载条目
+
0
+
按当前筛选展示
+
+
+
告警 / 错误
+
0
+
warning + error
+
+
+
总大小
+
0 B
+
日志目录占用
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+
+
+ +
+ + +
+ + +
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/_public/static/admin/pages/token.html b/_public/static/admin/pages/token.html index bd4692c7..dc757328 100644 --- a/_public/static/admin/pages/token.html +++ b/_public/static/admin/pages/token.html @@ -120,7 +120,8 @@

Token -
+
+
@@ -137,6 +138,7 @@

Token

+
加载中...