mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
295befe42b | ||
|
|
a07faddeff |
333
app.js
333
app.js
@@ -30,6 +30,12 @@ class CLIProxyManager {
|
||||
// Auth file filter state cache
|
||||
this.currentAuthFileFilter = 'all';
|
||||
this.cachedAuthFiles = [];
|
||||
this.authFilesPagination = {
|
||||
pageSize: 9,
|
||||
currentPage: 1,
|
||||
totalPages: 1
|
||||
};
|
||||
this.authFileStatsCache = {};
|
||||
|
||||
// Vertex AI credential import state
|
||||
this.vertexImportState = {
|
||||
@@ -616,6 +622,7 @@ class CLIProxyManager {
|
||||
if (authFileInput) {
|
||||
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
|
||||
}
|
||||
this.bindAuthFilesPaginationEvents();
|
||||
|
||||
// Vertex AI credential import
|
||||
const vertexSelectFile = document.getElementById('vertex-select-file');
|
||||
@@ -3874,25 +3881,21 @@ class CLIProxyManager {
|
||||
// 渲染认证文件列表
|
||||
async renderAuthFiles(files, keyStats = null) {
|
||||
const container = document.getElementById('auth-files-list');
|
||||
const isRuntimeOnlyFile = (file) => {
|
||||
if (!file) return false;
|
||||
const runtimeValue = file.runtime_only;
|
||||
return runtimeValue === true || runtimeValue === 'true';
|
||||
};
|
||||
const shouldDisplayDisabledGeminiCli = (file) => {
|
||||
if (!file) return false;
|
||||
const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
|
||||
const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
|
||||
const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
|
||||
return isGeminiCli && !isRuntimeOnlyFile(file);
|
||||
};
|
||||
const visibleFiles = Array.isArray(files) ? files.filter(file => {
|
||||
if (!file) return false;
|
||||
return shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
|
||||
}) : [];
|
||||
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleFiles.length === 0) {
|
||||
const allFiles = Array.isArray(files) ? files : [];
|
||||
const visibleFiles = allFiles.filter(file => {
|
||||
if (!file) return false;
|
||||
return this.shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
|
||||
});
|
||||
const stats = keyStats || await this.getKeyStats();
|
||||
|
||||
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
|
||||
this.authFileStatsCache = stats || {};
|
||||
|
||||
if (this.cachedAuthFiles.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
@@ -3902,81 +3905,131 @@ class CLIProxyManager {
|
||||
`;
|
||||
this.updateFilterButtons(new Set(['all']));
|
||||
this.bindAuthFileFilterEvents();
|
||||
this.applyAuthFileFilterState();
|
||||
this.applyAuthFileFilterState(false);
|
||||
this.authFilesPagination.currentPage = 1;
|
||||
this.authFilesPagination.totalPages = 1;
|
||||
this.updatePaginationControls(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用传入的keyStats,如果没有则获取一次
|
||||
if (!keyStats) {
|
||||
keyStats = await this.getKeyStats();
|
||||
}
|
||||
const stats = keyStats;
|
||||
|
||||
// 收集所有文件类型(使用API返回的type字段)
|
||||
const existingTypes = new Set(['all']); // 'all' 总是存在
|
||||
visibleFiles.forEach(file => {
|
||||
const existingTypes = new Set(['all']);
|
||||
this.cachedAuthFiles.forEach(file => {
|
||||
if (file.type) {
|
||||
existingTypes.add(file.type);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新筛选按钮显示
|
||||
this.updateFilterButtons(existingTypes);
|
||||
this.bindAuthFileFilterEvents();
|
||||
this.applyAuthFileFilterState(false);
|
||||
this.renderAuthFilesPage(this.authFilesPagination.currentPage);
|
||||
this.bindAuthFileActionEvents();
|
||||
}
|
||||
|
||||
container.innerHTML = visibleFiles.map(file => {
|
||||
const rawFileName = typeof file.name === 'string' ? file.name : '';
|
||||
const safeFileName = this.escapeHtml(rawFileName);
|
||||
// 认证文件的统计匹配逻辑:
|
||||
// 1. 首先尝试完整文件名匹配
|
||||
// 2. 如果没有匹配,尝试脱敏文件名匹配(去掉扩展名后的脱敏版本)
|
||||
let fileStats = stats[rawFileName] || { success: 0, failure: 0 };
|
||||
isRuntimeOnlyAuthFile(file) {
|
||||
if (!file) return false;
|
||||
const runtimeValue = file.runtime_only;
|
||||
return runtimeValue === true || runtimeValue === 'true';
|
||||
}
|
||||
|
||||
// 如果完整文件名没有统计,尝试基于文件名的脱敏版本匹配
|
||||
shouldDisplayDisabledGeminiCli(file) {
|
||||
if (!file) return false;
|
||||
const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
|
||||
const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
|
||||
const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
|
||||
return isGeminiCli && !this.isRuntimeOnlyAuthFile(file);
|
||||
}
|
||||
|
||||
resolveAuthFileStats(file, stats = {}) {
|
||||
const rawFileName = typeof file?.name === 'string' ? file.name : '';
|
||||
const defaultStats = { success: 0, failure: 0 };
|
||||
if (!rawFileName) {
|
||||
return defaultStats;
|
||||
}
|
||||
|
||||
const fromName = stats[rawFileName];
|
||||
if (fromName && (fromName.success > 0 || fromName.failure > 0)) {
|
||||
return fromName;
|
||||
}
|
||||
|
||||
let fileStats = fromName || defaultStats;
|
||||
if (fileStats.success === 0 && fileStats.failure === 0) {
|
||||
const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ""); // 去掉扩展名
|
||||
const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, "");
|
||||
|
||||
if (nameWithoutExt && nameWithoutExt !== rawFileName) {
|
||||
const candidateNames = new Set([nameWithoutExt]);
|
||||
const normalizedName = nameWithoutExt.toLowerCase();
|
||||
const typePrefix = typeof file?.type === 'string' ? file.type.trim().toLowerCase() : '';
|
||||
const providerPrefix = typeof file?.provider === 'string' ? file.provider.trim().toLowerCase() : '';
|
||||
const prefixList = [];
|
||||
|
||||
if (typePrefix) {
|
||||
prefixList.push(`${typePrefix}-`);
|
||||
}
|
||||
if (providerPrefix && providerPrefix !== typePrefix) {
|
||||
prefixList.push(`${providerPrefix}-`);
|
||||
}
|
||||
|
||||
prefixList.forEach(prefix => {
|
||||
if (prefix && normalizedName.startsWith(prefix)) {
|
||||
const trimmed = nameWithoutExt.substring(prefix.length);
|
||||
if (trimmed) {
|
||||
candidateNames.add(trimmed);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const candidate of candidateNames) {
|
||||
const statsByName = stats[candidate];
|
||||
if (statsByName && (statsByName.success > 0 || statsByName.failure > 0)) {
|
||||
return statsByName;
|
||||
}
|
||||
|
||||
const maskedCandidate = this.maskApiKey(candidate);
|
||||
if (maskedCandidate && maskedCandidate !== candidate) {
|
||||
const statsByMaskedName = stats[maskedCandidate];
|
||||
if (statsByMaskedName && (statsByMaskedName.success > 0 || statsByMaskedName.failure > 0)) {
|
||||
return statsByMaskedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const possibleSources = [];
|
||||
|
||||
// 规则1:尝试完整描述脱敏
|
||||
const match = nameWithoutExt.match(/^([^@]+@[^-]+)-(.+)$/);
|
||||
if (match) {
|
||||
const email = match[1];
|
||||
const projectName = match[2];
|
||||
|
||||
// 组合成完整的描述格式
|
||||
const fullDescription = `${email} (${projectName})`;
|
||||
|
||||
// 对完整描述进行脱敏
|
||||
const maskedDescription = this.maskApiKey(fullDescription);
|
||||
possibleSources.push(maskedDescription);
|
||||
possibleSources.push(this.maskApiKey(fullDescription));
|
||||
}
|
||||
|
||||
// 规则2:类型-个人标识.json 格式,去掉类型前缀后脱敏
|
||||
const typeMatch = nameWithoutExt.match(/^[^-]+-(.+)$/);
|
||||
if (typeMatch) {
|
||||
const personalId = typeMatch[1]; // 个人标识部分
|
||||
const maskedPersonalId = this.maskApiKey(personalId);
|
||||
possibleSources.push(maskedPersonalId);
|
||||
possibleSources.push(this.maskApiKey(typeMatch[1]));
|
||||
}
|
||||
|
||||
// 规则3:AI Studio 特殊处理 - 对完整文件名脱敏
|
||||
if (nameWithoutExt.startsWith('aistudio-')) {
|
||||
const maskedFullName = this.maskApiKey(nameWithoutExt);
|
||||
possibleSources.push(maskedFullName);
|
||||
possibleSources.push(this.maskApiKey(nameWithoutExt));
|
||||
}
|
||||
|
||||
// 查找第一个有统计数据的匹配
|
||||
for (const source of possibleSources) {
|
||||
if (stats[source] && (stats[source].success > 0 || stats[source].failure > 0)) {
|
||||
fileStats = stats[source];
|
||||
break;
|
||||
return stats[source];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用API返回的文件类型
|
||||
return fileStats;
|
||||
}
|
||||
|
||||
buildAuthFileItemHtml(file) {
|
||||
const rawFileName = typeof file?.name === 'string' ? file.name : '';
|
||||
const safeFileName = this.escapeHtml(rawFileName);
|
||||
const stats = this.authFileStatsCache || {};
|
||||
const fileStats = this.resolveAuthFileStats(file, stats);
|
||||
const fileType = file.type || 'unknown';
|
||||
// 首字母大写显示类型,特殊处理 iFlow
|
||||
let typeDisplayKey;
|
||||
switch (fileType) {
|
||||
case 'qwen':
|
||||
@@ -4011,15 +4064,10 @@ class CLIProxyManager {
|
||||
break;
|
||||
}
|
||||
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
|
||||
|
||||
// Determine whether the entry is runtime-only
|
||||
const isRuntimeOnly = isRuntimeOnlyFile(file);
|
||||
|
||||
const shouldShowMainFlag = shouldDisplayDisabledGeminiCli(file);
|
||||
const isRuntimeOnly = this.isRuntimeOnlyAuthFile(file);
|
||||
const shouldShowMainFlag = this.shouldDisplayDisabledGeminiCli(file);
|
||||
const mainFlagButton = shouldShowMainFlag ? `
|
||||
<button class="btn-small btn-warning main-flag-btn" title="主" disabled>主</button>` : '';
|
||||
|
||||
// Build action buttons; runtime-only entries display placeholder badge
|
||||
const actionsHtml = isRuntimeOnly ? `
|
||||
<div class="item-actions">
|
||||
<span class="virtual-auth-badge">虚拟认证文件</span>
|
||||
@@ -4059,19 +4107,110 @@ class CLIProxyManager {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 绑定筛选按钮事件
|
||||
this.bindAuthFileFilterEvents();
|
||||
|
||||
// 绑定认证文件操作按钮事件(使用事件委托)
|
||||
this.bindAuthFileActionEvents();
|
||||
|
||||
// Reapply current filter state
|
||||
this.applyAuthFileFilterState();
|
||||
}
|
||||
|
||||
// 更新筛选按钮显示
|
||||
getFilteredAuthFiles(filterType = this.currentAuthFileFilter) {
|
||||
const files = Array.isArray(this.cachedAuthFiles) ? this.cachedAuthFiles : [];
|
||||
const filterValue = (filterType || 'all').toLowerCase();
|
||||
if (filterValue === 'all') {
|
||||
return files;
|
||||
}
|
||||
return files.filter(file => {
|
||||
const type = (file?.type || 'unknown').toLowerCase();
|
||||
return type === filterValue;
|
||||
});
|
||||
}
|
||||
|
||||
renderAuthFilesPage(page = null) {
|
||||
const container = document.getElementById('auth-files-list');
|
||||
if (!container) return;
|
||||
|
||||
const pageSize = this.authFilesPagination?.pageSize || 9;
|
||||
const filteredFiles = this.getFilteredAuthFiles();
|
||||
const totalItems = filteredFiles.length;
|
||||
|
||||
if (totalItems === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<h3>${i18n.t('auth_files.empty_title')}</h3>
|
||||
<p>${i18n.t('auth_files.empty_desc')}</p>
|
||||
</div>
|
||||
`;
|
||||
this.authFilesPagination.currentPage = 1;
|
||||
this.authFilesPagination.totalPages = 1;
|
||||
this.updatePaginationControls(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
|
||||
let targetPage = typeof page === 'number' ? page : (this.authFilesPagination.currentPage || 1);
|
||||
targetPage = Math.max(1, Math.min(targetPage, totalPages));
|
||||
|
||||
this.authFilesPagination.currentPage = targetPage;
|
||||
this.authFilesPagination.totalPages = totalPages;
|
||||
|
||||
const startIndex = (targetPage - 1) * pageSize;
|
||||
const pageFiles = filteredFiles.slice(startIndex, startIndex + pageSize);
|
||||
container.innerHTML = pageFiles.map(file => this.buildAuthFileItemHtml(file)).join('');
|
||||
|
||||
this.updatePaginationControls(totalItems);
|
||||
}
|
||||
|
||||
updatePaginationControls(totalItems = 0) {
|
||||
const paginationContainer = document.getElementById('auth-files-pagination');
|
||||
const infoEl = document.getElementById('auth-files-pagination-info');
|
||||
if (!paginationContainer || !infoEl) return;
|
||||
|
||||
const prevBtn = paginationContainer.querySelector('button[data-action="prev"]');
|
||||
const nextBtn = paginationContainer.querySelector('button[data-action="next"]');
|
||||
const pageSize = this.authFilesPagination?.pageSize || 9;
|
||||
const totalPages = this.authFilesPagination?.totalPages || 1;
|
||||
const currentPage = Math.min(this.authFilesPagination?.currentPage || 1, totalPages);
|
||||
const shouldShow = totalItems > pageSize;
|
||||
|
||||
paginationContainer.style.display = shouldShow ? 'flex' : 'none';
|
||||
|
||||
const infoParams = totalItems === 0
|
||||
? { current: 0, total: 0, count: 0 }
|
||||
: { current: currentPage, total: totalPages, count: totalItems };
|
||||
infoEl.textContent = i18n.t('auth_files.pagination_info', infoParams);
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.disabled = currentPage <= 1;
|
||||
}
|
||||
if (nextBtn) {
|
||||
nextBtn.disabled = currentPage >= totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
bindAuthFilesPaginationEvents() {
|
||||
const container = document.getElementById('auth-files-pagination');
|
||||
if (!container) return;
|
||||
|
||||
const oldListener = container._paginationListener;
|
||||
if (oldListener) {
|
||||
container.removeEventListener('click', oldListener);
|
||||
}
|
||||
|
||||
const listener = (event) => {
|
||||
const button = event.target.closest('button[data-action]');
|
||||
if (!button || !container.contains(button)) return;
|
||||
event.preventDefault();
|
||||
const action = button.dataset.action;
|
||||
const currentPage = this.authFilesPagination?.currentPage || 1;
|
||||
if (action === 'prev') {
|
||||
this.renderAuthFilesPage(currentPage - 1);
|
||||
} else if (action === 'next') {
|
||||
this.renderAuthFilesPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
container._paginationListener = listener;
|
||||
container.addEventListener('click', listener);
|
||||
}
|
||||
|
||||
// 更新筛选按钮显示
|
||||
updateFilterButtons(existingTypes) {
|
||||
const filterContainer = document.querySelector('.auth-file-filter');
|
||||
if (!filterContainer) return;
|
||||
@@ -4143,28 +4282,22 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 处理筛选按钮点击
|
||||
handleFilterClick(clickedBtn) {
|
||||
handleFilterClick(clickedBtn, options = {}) {
|
||||
if (!clickedBtn) return;
|
||||
const { skipRender = false } = options;
|
||||
const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn');
|
||||
|
||||
// 更新按钮状态
|
||||
filterBtns.forEach(b => b.classList.remove('active'));
|
||||
clickedBtn.classList.add('active');
|
||||
|
||||
// 获取筛选类型
|
||||
const filterType = clickedBtn.dataset.type;
|
||||
this.currentAuthFileFilter = filterType || 'all';
|
||||
|
||||
// 筛选文件
|
||||
const fileItems = document.querySelectorAll('.file-item');
|
||||
fileItems.forEach(item => {
|
||||
if (filterType === 'all' || item.dataset.fileType === filterType) {
|
||||
item.classList.remove('hidden');
|
||||
} else {
|
||||
item.classList.add('hidden');
|
||||
if (!skipRender) {
|
||||
this.authFilesPagination.currentPage = 1;
|
||||
this.renderAuthFilesPage(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新筛选按钮文本(以防语言切换后新按钮未刷新)
|
||||
this.refreshFilterButtonTexts();
|
||||
}
|
||||
|
||||
@@ -4205,7 +4338,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// Apply current filter selection to the list
|
||||
applyAuthFileFilterState() {
|
||||
applyAuthFileFilterState(shouldRender = false) {
|
||||
const filterContainer = document.querySelector('.auth-file-filter');
|
||||
if (!filterContainer) return;
|
||||
|
||||
@@ -4228,7 +4361,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
if (targetButton) {
|
||||
this.handleFilterClick(targetButton);
|
||||
this.handleFilterClick(targetButton, { skipRender: !shouldRender });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4241,26 +4374,10 @@ class CLIProxyManager {
|
||||
const removalSet = new Set(filenames);
|
||||
this.cachedAuthFiles = (this.cachedAuthFiles || []).filter(file => file && !removalSet.has(file.name));
|
||||
|
||||
const container = document.getElementById('auth-files-list');
|
||||
if (!container) return;
|
||||
|
||||
const fileItems = container.querySelectorAll('.file-item');
|
||||
fileItems.forEach(item => {
|
||||
const fileNameAttr = item.getAttribute('data-file-name');
|
||||
if (fileNameAttr && removalSet.has(fileNameAttr)) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
|
||||
if (!container.querySelector('.file-item')) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<h3>${i18n.t('auth_files.empty_title')}</h3>
|
||||
<p>${i18n.t('auth_files.empty_desc')}</p>
|
||||
</div>
|
||||
`;
|
||||
if (!this.cachedAuthFiles.length) {
|
||||
this.authFilesPagination.currentPage = 1;
|
||||
}
|
||||
this.renderAuthFilesPage(this.authFilesPagination.currentPage);
|
||||
}
|
||||
|
||||
// 刷新筛选按钮文本(根据 data-i18n-text)
|
||||
|
||||
6
i18n.js
6
i18n.js
@@ -244,6 +244,9 @@ const i18n = {
|
||||
'auth_files.delete_filtered_partial': '{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个',
|
||||
'auth_files.delete_filtered_none': '当前筛选类型 ({type}) 下没有可删除的认证文件',
|
||||
'auth_files.files_count': '个文件',
|
||||
'auth_files.pagination_prev': '上一页',
|
||||
'auth_files.pagination_next': '下一页',
|
||||
'auth_files.pagination_info': '第 {current} / {total} 页 · 共 {count} 个文件',
|
||||
'auth_files.filter_all': '全部',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
@@ -737,6 +740,9 @@ const i18n = {
|
||||
'auth_files.delete_filtered_partial': '{type} auth files deletion finished: {success} succeeded, {failed} failed',
|
||||
'auth_files.delete_filtered_none': 'No deletable auth files under the current filter ({type})',
|
||||
'auth_files.files_count': 'files',
|
||||
'auth_files.pagination_prev': 'Previous',
|
||||
'auth_files.pagination_next': 'Next',
|
||||
'auth_files.pagination_info': 'Page {current} / {total} · {count} files',
|
||||
'auth_files.filter_all': 'All',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
|
||||
11
index.html
11
index.html
@@ -520,6 +520,17 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div id="auth-files-list" class="file-list file-grid"></div>
|
||||
<div id="auth-files-pagination" class="pagination-controls" style="display: none;">
|
||||
<button class="btn btn-secondary pagination-btn" data-action="prev">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<span data-i18n="auth_files.pagination_prev">上一页</span>
|
||||
</button>
|
||||
<div id="auth-files-pagination-info" class="pagination-info">-</div>
|
||||
<button class="btn btn-secondary pagination-btn" data-action="next">
|
||||
<span data-i18n="auth_files.pagination_next">下一页</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="auth-file-input" accept=".json" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
27
styles.css
27
styles.css
@@ -1567,6 +1567,33 @@ input:checked+.slider:before {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
gap: 6px;
|
||||
min-width: 120px;
|
||||
justify-content: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 响应式:中等屏幕2列 */
|
||||
@media (max-width: 1400px) {
|
||||
.file-grid {
|
||||
|
||||
Reference in New Issue
Block a user