feat: 优化管理后台界面,增强目录管理和分页功能,提升用户体验

This commit is contained in:
katelya
2026-03-03 22:00:27 +08:00
parent d608369192
commit 84abbffb29
2 changed files with 116 additions and 97 deletions

View File

@@ -60,12 +60,6 @@
- **前端路径简化** - 以根路径页面为主流程(`/``/admin.html``/webdav.html`
- **GitHub Actions 镜像构建** - 主分支/Tag 自动构建并推送 `api` + `web` 镜像
### 2026-03 近期更新(微调)
- 后台管理页统一为根路径 `/admin.html`,支持文件夹树与批量文件操作。
- 新增 `/webdav.html` 独立 WebDAV 页面UI 与主页风格保持一致。
- 新增 GitHub 存储方案;移除 Google Drive / OneDrive 适配。
- Docker 与 Cloudflare Pages 的页面入口保持一致,便于 Fork 后直接部署。
---
@@ -585,42 +579,4 @@ MIT License
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=katelya77/K-Vault&type=Date)](https://star-history.com/#katelya77/K-Vault&Date)
## Docker Storage Backend Update (2026-03)
Docker runtime now includes additional storage adapters:
- `webdav`
- `github` (`releases` mode and `contents` mode)
Key notes:
- All dynamic storage secrets are still encrypted by `CONFIG_ENCRYPTION_KEY`.
- `/api/status` now reports `webdav/github` as individual `connected/enabled` states.
- GitHub mode guidance:
- `releases`: preferred for binary files.
- `contents`: better for small files/text, subject to tighter API constraints.
Regression helper script:
```bash
npm run regression:storage
```
Optional smoke create/update test:
```bash
BASE_URL=http://localhost:8080 \
BASIC_USER=admin BASIC_PASS=your_password \
SMOKE_STORAGE_TYPE=webdav \
SMOKE_STORAGE_CONFIG_JSON='{"baseUrl":"https://dav.example.com","username":"u","password":"p"}' \
node scripts/storage-regression.js
```
Checklist covered by script:
- `health` / `status`
- `login` with both payloads (`username/password`, `user/pass`)
- storage `list/create/update/test/default`
- `upload/download/delete` for enabled storages
[![Star History Chart](https://api.star-history.com/svg?repos=katelya77/K-Vault&type=Date)](https://star-history.com/#katelya77/K-Vault&Date)

View File

@@ -125,13 +125,44 @@
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: nowrap;
padding: 10px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
font-weight: 600;
color: #4c3b7a;
}
.folder-head-title {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 0 1 auto;
white-space: nowrap;
font-size: 18px;
line-height: 1;
overflow: hidden;
}
.folder-head-title i {
flex: 0 0 auto;
font-size: 18px;
}
.folder-head-title span {
white-space: nowrap;
letter-spacing: 0.01em;
}
.folder-head-actions {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
white-space: nowrap;
margin-left: auto;
}
.folder-head .el-button {
padding: 5px 8px;
width: 28px;
height: 28px;
padding: 0;
margin: 0 !important;
}
.folder-current {
padding: 8px 12px;
@@ -486,8 +517,8 @@
<div class="disk-layout">
<aside class="folder-sidebar">
<div class="folder-head">
<span><i class="fas fa-folder-tree"></i> 目录管理</span>
<span>
<span class="folder-head-title"><i class="fas fa-folder-tree"></i><span>目录管理</span></span>
<span class="folder-head-actions">
<el-button size="mini" circle icon="el-icon-refresh" title="刷新目录与文件" :disabled="folderMutating" :loading="folderLoading" @click="refreshFolderResources"></el-button>
<el-button size="mini" circle icon="el-icon-plus" title="新建目录" :disabled="folderMutating" :loading="folderMutatingAction === 'create'" @click="createFolder"></el-button>
<el-button size="mini" circle icon="el-icon-edit" title="重命名目录" :disabled="folderMutating || !folderPath" :loading="folderMutatingAction === 'rename'" @click="renameCurrentFolder"></el-button>
@@ -751,8 +782,8 @@
</template>
<div class="pagination-container">
<el-pagination
background layout="prev, pager, next"
:total="filteredTableData.length" :page-size="pageSize"
background layout="total, prev, pager, next"
:total="paginationTotal" :page-size="pageSize"
@current-change="handlePageChange" :current-page.sync="currentPage" />
</div>
<div style="text-align:center;margin-top:16px;">
@@ -869,7 +900,7 @@
isLoadingMore: false,
search: '',
currentPage: 1,
pageSize: 100,
pageSize: 24,
selectedFiles: [],
sortOption: 'dateDesc',
filterOption: 'all',
@@ -974,6 +1005,15 @@
return this.sortData(this.filteredTableData)
.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
},
useServerTotalForPagination() {
return !this.search && this.filterOption === 'all' && this.fileType === 'all';
},
paginationTotal() {
if (this.useServerTotalForPagination) {
return Math.max(Number(this.totalCount || 0), this.filteredTableData.length);
}
return this.filteredTableData.length;
},
paginatedData() {
return this.paginatedTableData;
},
@@ -1063,6 +1103,10 @@
},
deep: true
},
search() {
this.currentPage = 1;
this.normalizeCurrentPage();
},
selectedFiles(newValue) {
if (newValue.length === 0) {
this.batchToolbarPosition.left = null;
@@ -1079,7 +1123,11 @@
}
},
sortOption(newOption) { localStorage.setItem('sortOption', newOption); },
filterOption(newOption) { localStorage.setItem('filterOption', newOption); },
filterOption(newOption) {
localStorage.setItem('filterOption', newOption);
this.currentPage = 1;
this.normalizeCurrentPage();
},
viewMode(newValue) { localStorage.setItem('adminViewMode', newValue); },
folderPath(newValue) { localStorage.setItem('adminFolderPath', this.normalizeFolderPath(newValue)); }
},
@@ -1165,6 +1213,7 @@
this.updateStats();
this.calculatePageSize();
this.sortData(this.tableData);
this.normalizeCurrentPage();
},
writeFolderCache(cacheKey) {
if (!cacheKey) return;
@@ -1963,7 +2012,20 @@
this.$message.error('退出失败,请重试');
}
},
handlePageChange(page) { this.currentPage = page; }, // 切换页面
async handlePageChange(page) {
this.currentPage = page;
await this.ensurePageDataForPage(page);
this.normalizeCurrentPage();
}, // 切换页面
async ensurePageDataForPage(page) {
if (!this.useServerTotalForPagination) return;
const needCount = page * this.pageSize;
let guard = 0;
while (this.filteredTableData.length < needCount && this.nextCursor && guard < 10) {
await this.loadMore({ silent: true, auto: true });
guard += 1;
}
},
normalizeListItem(file) {
return {
...file,
@@ -1986,7 +2048,8 @@
this.tableData = Array.from(map.values());
},
getListRequestLimit(forLoadMore = false) {
return forLoadMore ? 1000 : 100;
const basePage = this.isMobileViewport ? 12 : 24;
return forLoadMore ? basePage * 4 : basePage * 3;
},
buildListQueryParams({ cursor = null, limit = null, includeStats = true } = {}) {
const params = new URLSearchParams();
@@ -2012,54 +2075,42 @@
this.currentPage = 1;
await this.refreshFileList({ syncFolders: true });
}, // 切换存储类型筛选
async loadMore() {
async loadMore(options = {}) {
const { silent = false, auto = false } = options;
if (this.isLoadingMore || !this.nextCursor) return;
this.isLoadingMore = true;
const startCount = this.tableData.length;
try {
let cursor = this.nextCursor;
let guard = 0;
const seenCursors = new Set();
while (cursor && guard < 200) {
if (seenCursors.has(cursor)) break;
seenCursors.add(cursor);
const params = this.buildListQueryParams({
cursor,
limit: this.getListRequestLimit(true),
includeStats: true,
});
const result = await fetch(`./api/manage/list?${params.toString()}`, {
method: 'GET',
credentials: 'include',
}).then((r) => r.json());
const mapped = (result.keys || []).map((file) => this.normalizeListItem(file));
this.mergeListData(mapped);
cursor = result.list_complete ? null : result.cursor;
this.nextCursor = cursor;
if (Number.isFinite(result?.stats?.total)) {
this.totalCount = result.stats.total;
}
guard += 1;
const params = this.buildListQueryParams({
cursor: this.nextCursor,
limit: this.getListRequestLimit(true),
includeStats: true,
});
const result = await fetch(`./api/manage/list?${params.toString()}`, {
method: 'GET',
credentials: 'include',
}).then((r) => r.json());
const mapped = (result.keys || []).map((file) => this.normalizeListItem(file));
this.mergeListData(mapped);
this.nextCursor = result.list_complete ? null : result.cursor;
if (Number.isFinite(result?.stats?.total)) {
this.totalCount = result.stats.total;
}
this.updateStats();
this.writeFolderCache(this.getFolderCacheKey());
if (!this.nextCursor) {
const expandedSize = Math.max(
this.pageSize,
this.filteredTableData.length || this.tableData.length || this.getListRequestLimit(false)
);
this.pageSize = expandedSize;
this.currentPage = 1;
}
this.normalizeCurrentPage();
const loaded = Math.max(0, this.tableData.length - startCount);
if (this.nextCursor) {
this.$message.warning(`数据较多,本次已继续加载 ${loaded} 条,可再次点击加载更多`);
} else {
this.$message.success(`已加载剩余 ${loaded}`);
if (!silent) {
if (this.nextCursor) {
this.$message.success(`已补充 ${loaded} 条数据,可继续翻页`);
} else if (!auto) {
this.$message.success(`已加载剩余 ${loaded}`);
}
}
} catch {
this.$message.error('加载更多失败,请稍后重试');
if (!silent) {
this.$message.error('加载更多失败,请稍后重试');
}
} finally {
this.isLoadingMore = false;
}
@@ -2104,8 +2155,10 @@
},
calculatePageSize() { // 设置页面大小
const config = {
minSize: 100, // 桌面端每页最少展示数量
mobileMinSize: 60, // 移动端每页最少展示数量
desktopMinSize: 20,
desktopMaxSize: 25,
mobileMinSize: 10,
mobileMaxSize: 14,
cardWidth: 240,
ratio: 3/4, // 卡片高宽比
gap: 20, // 卡片间距
@@ -2121,17 +2174,26 @@
const cols = Math.max(1, Math.floor(width / (config.cardWidth + config.gap)));
const cardHeight = Math.max(120, (width / cols - config.gap) * config.ratio);
const rows = Math.max(1, Math.floor(height / (cardHeight + config.gap)));
const minSize = this.isMobileViewport ? config.mobileMinSize : config.minSize;
// 设置页面大小
this.pageSize = Math.max(rows * cols, minSize);
const estimated = rows * cols;
if (this.isMobileViewport) {
this.pageSize = Math.max(config.mobileMinSize, Math.min(config.mobileMaxSize, estimated));
} else {
this.pageSize = Math.max(config.desktopMinSize, Math.min(config.desktopMaxSize, estimated));
}
},
updateWindowWidth() { // 动态调整页面大小
this.windowWidth = window.innerWidth;
this.calculatePageSize();
},
normalizeCurrentPage() {
const totalPages = Math.max(1, Math.ceil((this.paginationTotal || 0) / this.pageSize));
if (this.currentPage > totalPages) this.currentPage = totalPages;
if (this.currentPage < 1) this.currentPage = 1;
},
handleWindowResize() {
this.updateViewportFlags();
this.updateWindowWidth();
this.normalizeCurrentPage();
if (this.selectedFiles.length > 0 && !this.isMobileViewport) {
this.$nextTick(() => this.snapBatchToolbarToBottom());
}
@@ -2280,6 +2342,7 @@
this.updateStats();
this.calculatePageSize();
this.sortData(this.tableData);
this.normalizeCurrentPage();
this.writeFolderCache(cacheKey);
if (syncFolders) {
await this.fetchFolders();