Skip to content

Commit b9507b6

Browse files
authored
feat: paginate kv list (cf-pages#253)
1 parent 61f5185 commit b9507b6

6 files changed

Lines changed: 206 additions & 111 deletions

File tree

admin-imgtc.html

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,16 @@
240240
</template>
241241
</template>
242242
</div>
243-
<div class="pagination-container">
244-
<el-pagination
245-
background layout="prev, pager, next"
246-
:total="filteredTableData.length" :page-size="pageSize"
247-
@current-change="handlePageChange" :current-page.sync="currentPage" />
248-
</div>
249-
<el-footer class="footer">
243+
<div class="pagination-container">
244+
<el-pagination
245+
background layout="prev, pager, next"
246+
:total="filteredTableData.length" :page-size="pageSize"
247+
@current-change="handlePageChange" :current-page.sync="currentPage" />
248+
</div>
249+
<div style="text-align:center;margin-top:16px;">
250+
<el-button v-if="nextCursor" @click="loadMore">加载更多</el-button>
251+
</div>
252+
<el-footer class="footer">
250253
<div>Powered By</div>
251254
<a href="https://github.com/cf-pages/Telegraph-Image" target="_blank" rel="noopener noreferrer">
252255
<div><i class="fa-brands fa-github"></i> Telegraph-Image</div>
@@ -294,7 +297,8 @@
294297
maxSize: 20 * 1024 * 1024, // 最大上传20MB
295298
maxConcurrent: 3
296299
},
297-
tableData: [],
300+
tableData: [],
301+
nextCursor: null,
298302
search: '',
299303
currentPage: 1,
300304
pageSize: 15,
@@ -373,13 +377,39 @@
373377
refreshDashboard() {location.reload();}, // 刷新页面
374378
handleLogout() { window.location.href = '/'; }, // 退出登录
375379
handlePageChange(page) { this.currentPage = page; }, // 切换页面
376-
handleUpload() { this.$refs.fileInput.click(); }, // 打开文件选择对话框
377-
sort(command) { this.sortOption = command; }, // 切换排序方式
378-
filter(command) { this.filterOption = command; }, // 切换筛选方式
379-
sortData(data) {
380-
return this.sortOption === 'nameAsc' ? data.sort((a, b) => a.name.localeCompare(b.name)) :
381-
this.sortOption === 'sizeDesc' ? data.sort((a, b) => b.metadata.fileSize - a.metadata.fileSize) :
382-
data.sort((a, b) => b.metadata.TimeStamp - a.metadata.TimeStamp);
380+
handleUpload() { this.$refs.fileInput.click(); }, // 打开文件选择对话框
381+
sort(command) { this.sortOption = command; }, // 切换排序方式
382+
filter(command) { this.filterOption = command; }, // 切换筛选方式
383+
loadMore() {
384+
const opts = { method: 'GET', credentials: 'include' };
385+
const url = this.nextCursor
386+
? `./api/manage/list?cursor=${encodeURIComponent(this.nextCursor)}`
387+
: `./api/manage/list?limit=100`;
388+
389+
fetch(url, opts)
390+
.then(r => r.json())
391+
.then(result => {
392+
const files = result.keys || [];
393+
const mapped = files.map(file => ({
394+
...file,
395+
selected: false,
396+
metadata: {
397+
...file.metadata,
398+
liked: file.metadata.liked ?? false,
399+
fileName: file.metadata.fileName ?? file.name,
400+
fileSize: file.metadata.fileSize ?? 0
401+
}
402+
}));
403+
this.tableData = this.tableData.concat(mapped);
404+
this.nextCursor = result.list_complete ? null : result.cursor;
405+
this.updateStats();
406+
})
407+
.catch(() => this.$message.error('同步数据时出错,请检查网络连接'));
408+
},
409+
sortData(data) {
410+
return this.sortOption === 'nameAsc' ? data.sort((a, b) => a.name.localeCompare(b.name)) :
411+
this.sortOption === 'sizeDesc' ? data.sort((a, b) => b.metadata.fileSize - a.metadata.fileSize) :
412+
data.sort((a, b) => b.metadata.TimeStamp - a.metadata.TimeStamp);
383413
},
384414
formattedFileDetails(item) {
385415
const metadata = item.metadata;
@@ -504,16 +534,27 @@
504534
}
505535
event.target.value = '';
506536
},
507-
refreshFileList() { // 不刷新页面,仅更新数据
508-
fetch("./api/manage/list", { method: 'GET', credentials: 'include' })
509-
.then(response => response.json())
510-
.then(result => {
511-
this.tableData = result.map(file => ({ ...file, selected: false }));
512-
this.updateStats();
513-
this.sortData(this.tableData);
514-
})
515-
.catch(() => this.$message.error('刷新文件列表失败,请检查网络连接'));
516-
},
537+
refreshFileList() { // 不刷新页面,仅更新数据
538+
fetch("./api/manage/list?limit=100", { method: 'GET', credentials: 'include' })
539+
.then(response => response.json())
540+
.then(result => {
541+
const files = result.keys || [];
542+
this.tableData = files.map(file => ({
543+
...file,
544+
selected: false,
545+
metadata: {
546+
...file.metadata,
547+
liked: file.metadata.liked ?? false,
548+
fileName: file.metadata.fileName ?? file.name,
549+
fileSize: file.metadata.fileSize ?? 0
550+
}
551+
}));
552+
this.nextCursor = result.list_complete ? null : result.cursor;
553+
this.updateStats();
554+
this.sortData(this.tableData);
555+
})
556+
.catch(() => this.$message.error('刷新文件列表失败,请检查网络连接'));
557+
},
517558
toggleSelect(index, name) {
518559
const fileIndex = this.tableData.findIndex(file => file.name === name);
519560
this.tableData[fileIndex].selected = !this.tableData[fileIndex].selected;
@@ -830,28 +871,30 @@
830871
.catch(() => this.$message.error('同步数据时出错,请检查网络连接'));
831872

832873
// 获取文件列表数据
833-
fetch("./api/manage/list", { method: 'GET', credentials: 'include' })
834-
.then(response => response.json())
835-
.then(result => {
836-
console.log("result: ", result);
837-
this.tableData = result.map(file => ({
838-
...file,
839-
selected: false,
840-
metadata: {
841-
...file.metadata,
842-
liked: file.metadata.liked ?? false,
843-
fileName: file.metadata.fileName ?? file.name,
844-
fileSize: file.metadata.fileSize ?? 0
845-
}
846-
}));
847-
this.updateStats();
848-
// 恢复设置
849-
this.sortOption = localStorage.getItem('sortOption') || this.sortOption;
850-
this.sortData(this.tableData);
851-
this.fileType = localStorage.getItem('fileType') || this.fileType;
852-
this.switchFileType(this.fileType);
853-
})
854-
.catch(() => this.$message.error('同步数据时出错,请检查网络连接'));
874+
fetch("./api/manage/list?limit=100", { method: 'GET', credentials: 'include' })
875+
.then(response => response.json())
876+
.then(result => {
877+
console.log("result: ", result);
878+
const files = result.keys || [];
879+
this.nextCursor = result.list_complete ? null : result.cursor;
880+
this.tableData = files.map(file => ({
881+
...file,
882+
selected: false,
883+
metadata: {
884+
...file.metadata,
885+
liked: file.metadata.liked ?? false,
886+
fileName: file.metadata.fileName ?? file.name,
887+
fileSize: file.metadata.fileSize ?? 0
888+
}
889+
}));
890+
this.updateStats();
891+
// 恢复设置
892+
this.sortOption = localStorage.getItem('sortOption') || this.sortOption;
893+
this.sortData(this.tableData);
894+
this.fileType = localStorage.getItem('fileType') || this.fileType;
895+
this.switchFileType(this.fileType);
896+
})
897+
.catch(() => this.$message.error('同步数据时出错,请检查网络连接'));
855898

856899
if (localStorage.getItem('quickWebsites')) {
857900
this.quickWebsites = JSON.parse(localStorage.getItem('quickWebsites'));

admin-waterfall.html

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,35 @@
5858
</style>
5959
</head>
6060
<body>
61-
<div class="box">
62-
</div>
61+
<div class="box"></div>
62+
<button id="loadMore" style="display:block;margin:16px auto;" onclick="loadMore()">加载更多</button>
6363
<!-- 放大遮罩层 -->
6464
<div id="bigimg" onclick="closeBigImg();"></div>
6565
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js"></script>
6666
<script>
67-
var imgList
68-
$.ajax({
69-
url: "./api/manage/list",
70-
async: false,
71-
dataType: "json",
72-
success: function(data) {
73-
imgList = data; // 将请求的数据赋值给全局变量
74-
}
75-
});
76-
var targetElement = document.querySelector('.box');
77-
let str ='';
78-
for (let i = 0; i < imgList.length; i++){
79-
if(imgList[i].name.indexOf(".mp4")>0)continue;
80-
str += '<img onclick="showBigImg(this)" data-original='+"/file/"+imgList[i].name+' src='+"/file/"+imgList[i].name +'>';
81-
}
82-
targetElement.innerHTML = str
67+
var cursor = null;
68+
function loadMore(){
69+
$.ajax({
70+
url: cursor
71+
? "./api/manage/list?cursor=" + encodeURIComponent(cursor)
72+
: "./api/manage/list?limit=100",
73+
dataType: "json",
74+
success: function(data){
75+
cursor = data.list_complete ? null : data.cursor;
76+
var images = data.keys || [];
77+
var targetElement = document.querySelector('.box');
78+
let str = '';
79+
for (let i = 0; i < images.length; i++){
80+
if(images[i].name.indexOf('.mp4')>0) continue;
81+
str += '<img onclick="showBigImg(this)" data-original="/file/'+images[i].name+'" src="/file/'+images[i].name+'">';
82+
}
83+
targetElement.insertAdjacentHTML('beforeend', str);
84+
if(!cursor){ document.getElementById('loadMore').style.display='none'; }
85+
},
86+
error: function(){ alert("同步失败,请检查网络"); }
87+
});
88+
}
89+
loadMore();
8390
</script>
8491
</body>
8592
<script>

admin.html

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@
108108
<el-button size="mini" type="danger" @click="handleDelete(scope.$index,scope.row.name)">删除</el-button>
109109
</template>
110110
</el-table-column>
111-
</el-table>
112-
</template>
113-
</el-main>
111+
</el-table>
112+
<el-button v-if="nextCursor" style="margin-top:16px" @click="loadMore">加载更多</el-button>
113+
</template>
114+
</el-main>
114115
</el-container>
115116
</div>
116117
</body>
@@ -126,9 +127,10 @@
126127
data: {
127128
Number: 0,
128129
WhiteList: 0,
129-
BlackList: 0,
130-
showLogoutButton: false,
131-
tableData: [],
130+
BlackList: 0,
131+
showLogoutButton: false,
132+
tableData: [],
133+
nextCursor: null,
132134
dialogFormVisible: false,
133135
formLabelWidth: '120px',
134136
form: {
@@ -231,6 +233,21 @@
231233
type: 'success'
232234
});
233235
},
236+
loadMore() {
237+
const opts = { method: 'GET', redirect: 'follow', credentials: 'include' };
238+
const url = this.nextCursor
239+
? `./api/manage/list?cursor=${encodeURIComponent(this.nextCursor)}`
240+
: `./api/manage/list?limit=100`;
241+
242+
fetch(url, opts)
243+
.then(r => r.json())
244+
.then(result => {
245+
this.tableData = this.tableData.concat(result.keys || []);
246+
this.nextCursor = result.list_complete ? null : result.cursor;
247+
this.Number = this.tableData.length;
248+
})
249+
.catch(() => alert("同步失败,请检查网络"));
250+
},
234251
},
235252

236253
mounted() {
@@ -273,7 +290,7 @@
273290
};
274291

275292

276-
fetch("./api/manage/list", requestOptions)
293+
fetch("./api/manage/list?limit=100", requestOptions)
277294
//判断是否需要登录
278295
.then(response => {
279296
if (response.status == 401) {
@@ -284,8 +301,13 @@
284301
return response;
285302
}
286303
})
287-
.then(response => response.text())
288-
.then(result => { this.tableData = JSON.parse(result); console.log(result); this.Number = this.tableData.length })
304+
.then(response => response.json())
305+
.then(result => {
306+
this.tableData = result.keys || [];
307+
this.nextCursor = result.list_complete ? null : result.cursor;
308+
console.log(result);
309+
this.Number = this.tableData.length;
310+
})
289311
.catch(error => { alert("An error occurred while synchronizing data with the server, please check the network connection"); console.log('error', error) });
290312

291313
}

functions/api/manage/list.js

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
11
export async function onRequest(context) {
2-
// Contents of context object
3-
const {
4-
request, // same as existing Worker API
5-
env, // same as existing Worker API
6-
params, // if filename includes [id] or [[path]]
7-
waitUntil, // same as ctx.waitUntil in existing Worker API
8-
next, // used for middleware or to fetch assets
9-
data, // arbitrary space for passing data between middlewares
10-
} = context;
11-
console.log(env)
12-
const value = await env.img_url.list();
2+
const { request, env } = context;
3+
const url = new URL(request.url);
134

14-
console.log(value)
15-
//let res=[]
16-
//for (let i in value.keys){
17-
//add to res
18-
//"metadata":{"TimeStamp":19876541,"ListType":"None","rating_label":"None"}
19-
//let tmp = {
20-
// name: value.keys[i].name,
21-
// TimeStamp: value.keys[i].metadata.TimeStamp,
22-
// ListType: value.keys[i].metadata.ListType,
23-
// rating_label: value.keys[i].metadata.rating_label,
24-
//}
25-
//res.push(tmp)
26-
//}
27-
const info = JSON.stringify(value.keys);
28-
return new Response(info);
5+
const raw = url.searchParams.get("limit");
6+
let limit = parseInt(raw || "100", 10);
7+
if (!Number.isFinite(limit) || limit <= 0) limit = 100;
8+
if (limit > 1000) limit = 1000;
299

10+
const cursor = url.searchParams.get("cursor") || undefined;
11+
const value = await env.img_url.list({ limit, cursor });
12+
13+
return new Response(JSON.stringify(value), {
14+
headers: { "Content-Type": "application/json" }
15+
});
3016
}

test/pagination.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const assert = require('assert');
2+
3+
describe('KV list pagination', function () {
4+
async function getOnRequest() {
5+
return (await import('../functions/api/manage/list.js')).onRequest;
6+
}
7+
8+
const sampleKeys = [{ name: 'a' }, { name: 'b' }];
9+
function mockEnv() {
10+
return {
11+
img_url: {
12+
list: ({ limit, cursor }) => {
13+
const start = cursor ? parseInt(cursor, 10) : 0;
14+
const end = Math.min(start + limit, sampleKeys.length);
15+
const keys = sampleKeys.slice(start, end);
16+
const next = end < sampleKeys.length ? String(end) : undefined;
17+
return Promise.resolve({
18+
keys,
19+
list_complete: end >= sampleKeys.length,
20+
cursor: next
21+
});
22+
}
23+
}
24+
};
25+
}
26+
27+
it('limits results and returns cursor when more data', async function () {
28+
const onRequest = await getOnRequest();
29+
const env = mockEnv();
30+
const request = new Request('https://example.com/api/manage/list?limit=1');
31+
const res = await onRequest({ request, env });
32+
const data = JSON.parse(await res.text());
33+
assert.ok(data.keys.length <= 1);
34+
assert.strictEqual(data.list_complete, false);
35+
assert.ok(data.cursor);
36+
});
37+
38+
it('second page completes listing', async function () {
39+
const onRequest = await getOnRequest();
40+
const env = mockEnv();
41+
const firstRes = await onRequest({ request: new Request('https://example.com/api/manage/list?limit=1'), env });
42+
const firstData = JSON.parse(await firstRes.text());
43+
const secondRes = await onRequest({ request: new Request(`https://example.com/api/manage/list?cursor=${firstData.cursor}`), env });
44+
const secondData = JSON.parse(await secondRes.text());
45+
assert.strictEqual(secondData.list_complete, true);
46+
assert.ok(!secondData.cursor);
47+
});
48+
});

0 commit comments

Comments
 (0)