diff --git a/admin-imgtc.html b/admin-imgtc.html index 3665e3ece..0ab703b37 100644 --- a/admin-imgtc.html +++ b/admin-imgtc.html @@ -1,308 +1,381 @@ - - - + + + - - - ImgTC | Admin - - - - - - - - -
- - -
- Dashboard -
- -
- - 记录总数量: {{ Number }} - -
- - - - + + + ImgTC | Admin + + + + + + + +
+ + +
+ Dashboard +
+ +
+ + + 记录数: {{ Number }} - - 按时间倒序 - 按名称升序 - - - - - - - - - - - + +
+ + + + + + + 按时间倒序 + 按名称升序 + + + + + + + + + + + + +
-
- - -
- +
+
+ + +
+ +
Powered By
+ +
Telegraph-Image
+
+
+
+ +
+ @@ -311,15 +384,14 @@ new Vue({ el: '#app', data: { + baseURL: document.location.origin, Number: 0, - showLogoutButton: false, tableData: [], search: '', currentPage: 1, pageSize: 15, selectedFiles: [], sortOption: 'dateDesc', - isUploading: false }, computed: { filteredTableData() { @@ -332,7 +404,7 @@ return sortedData.slice(start, end); }, sortIcon() { - return this.sortOption === 'dateDesc' ? 'fas fa-sort-amount-down' : 'fas fa-sort-alpha-up'; + return this.sortOption === 'dateDesc' ? 'fas fa-sort-amount-down' : 'fas fa-sort-alpha-up'; } }, watch: { @@ -350,6 +422,178 @@ refreshDashboard() { location.reload(); }, + calculatePageSize() { + const minPageSize = 15; + const cardMinWidth = 240; // 卡片最小宽度 + const cardAspectRatio = 3 / 4; // 卡片的高宽比 + const gap = 20; // 卡片间的间隙 + + const contentElement = document.querySelector('.content'); + const containerWidth = contentElement ? contentElement.clientWidth : 800; + const columns = Math.floor(containerWidth / (cardMinWidth + gap)); + + const headerElement = document.querySelector('.header-content'); + const headerHeight = headerElement ? headerElement.offsetHeight : 60; // 使用默认值60px + const containerHeight = window.innerHeight - headerHeight; + const cardHeight = (containerWidth / columns - gap) * cardAspectRatio; + const rows = Math.floor(containerHeight / (cardHeight + gap)); + + this.pageSize = Math.max(rows * columns, minPageSize); + }, + updateWindowWidth() { + this.windowWidth = window.innerWidth; + this.calculatePageSize(); + }, + handleUpload() { + this.$refs.fileInput.click(); + }, + uploadFiles(event) { + const files = event.target.files; + if (files.length === 0) return; + + // 过滤文件类型和大小 + const validFiles = []; + const invalidTypeFiles = []; + const oversizedFiles = []; + + Array.from(files).forEach(file => { + const isValidType = file.type.startsWith('image/') || file.type.startsWith('video/'); + const isValidSize = file.size <= 5 * 1024 * 1024; // 5MB + + if (!isValidType) { + invalidTypeFiles.push(file.name); + } else if (!isValidSize) { + oversizedFiles.push(file.name); + } else { + validFiles.push(file); + } + }); + + // 显示错误消息 + if (invalidTypeFiles.length > 0) { + this.$message.error(`以下文件类型不支持: ${invalidTypeFiles.join(', ')}`); + } + if (oversizedFiles.length > 0) { + this.$message.error(`以下文件超过5MB: ${oversizedFiles.join(', ')}`); + } + + if (validFiles.length === 0) { + this.$message.info('没有符合条件的文件'); + return; + } + + console.log("ValidFiles: ", validFiles); + this.$confirm(`确定要上传这 ${validFiles.length} 个文件吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + // 显示上传进度 + const loadingMessage = this.$message({ + message: '上传中...', + duration: 0, + showClose: false, + }); + + const maxConcurrent = 3; // 最大并发数 + let current = 0; + let successCount = 0; + let failedUploads = []; + + const uploadNext = () => { + if (current >= validFiles.length) { + // 检查是否所有文件都已处理 + if (successCount + failedUploads.length === validFiles.length) { + loadingMessage.close(); + if (successCount > 0) { + this.$message.success(`成功上传 ${successCount} 个文件!`); + } + if (failedUploads.length > 0) { + this.$message.error(`上传失败: ${failedUploads.join(', ')}`); + } + // 刷新文件列表 + this.refreshFileList(); + } + return; + } + + const file = validFiles[current]; + current++; + + const formData = new FormData(); + formData.append('file', file); + + fetch(this.baseURL + '/upload', { + method: 'POST', + body: formData, + credentials: 'include' + }).then(response => { + if (!response.ok) { + throw new Error(`上传失败: ${file.name}`); + } + return response.json(); + }).then(result => { + if (Array.isArray(result) && result.length > 0 && result[0].error) { + // 响应内容中包含错误 + throw new Error(result[0].error || `上传失败: ${file.name}`); + } + + // 确保 result[0].src 存在 + if (!result[0].src) { + throw new Error(`上传成功但未返回文件路径: ${file.name}`); + } + + successCount++; + + // 处理成功上传的文件 + const previewUrl = `${this.baseURL}${result[0].src}`; + if (result[0].src) { + fetch(previewUrl, { method: 'GET', credentials: 'include' }) + .then(response => { + if (!response.ok) { + console.error(`无法获取文件: ${previewUrl}`); + } else { + this.tableData.unshift({ + name: result[0].src.replace(/^\/file\//, ''), + selected: false, + metadata: { TimeStamp: Date.now() } + }); + } + }).catch(error => { + console.error(`获取文件时出错: ${previewUrl}`, error); + }); + } + }).catch(error => { + failedUploads.push(file.name); // 记录失败的文件名称 + console.error(error.message); + }).finally(() => { + uploadNext(); // 继续上传下一个文件 + }); + }; + + // 启动初始的并发上传任务 + for (let i = 0; i < maxConcurrent && i < validFiles.length; i++) { + uploadNext(); + } + + }).catch(() => { + this.$message.info('已取消上传'); + }); + + // 清空文件输入,以便可以重复选择相同的文件 + event.target.value = ''; + }, + refreshFileList() { + // 重新获取文件列表 + fetch("./api/manage/list", { method: 'GET', credentials: 'include' }) + .then(response => response.json()) + .then(result => { + this.tableData = result.map(file => ({ ...file, selected: false })); + this.updateStats(); + this.sortData(this.tableData); + }) + .catch(() => this.$message.error('刷新文件列表失败,请检查网络连接')); + }, handleDelete(index, key) { this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', { confirmButtonText: '确定', @@ -360,7 +604,7 @@ .then(response => response.ok ? this.tableData.splice(index, 1) : Promise.reject()) .then(() => { this.updateStats(); - this.$message.success('删除成功!'); + this.$message.success('删除成功!'); }) .catch(() => this.$message.error('删除失败,请检查网络连接')); }).catch(() => this.$message.info('已取消删除')); @@ -411,13 +655,54 @@ window.location.href = '/'; }, handleCopy(index, key) { - const text = `${document.location.origin}/file/${key}`; + const text = `${this.baseURL}/file/${key}`; navigator.clipboard ? navigator.clipboard.writeText(text).then(() => this.$message.success('复制文件链接成功~')) : this.copyToClipboardFallback(text); }, handlePageChange(page) { this.currentPage = page; }, + // 处理收藏事件 + toggleLike(index, key) { + console.log(`Toggling like for image: ${key}`); + + // 乐观更新收藏状态 + if (this.tableData[index].metadata.liked === undefined) { + this.tableData[index].metadata.liked = false; + } + this.tableData[index].metadata.liked = !this.tableData[index].metadata.liked; + + // 发送请求更新服务器数据 + var requestOptions = { + method: 'GET', + redirect: 'follow', + credentials: 'include' + }; + + fetch(`./api/manage/toggleLike/${key}`, requestOptions) + .then(response => response.json()) + .then(result => { + if (!result.success) { + // 如果服务器更新失败,将状态还原 + this.tableData[index].metadata.liked = !this.tableData[index].metadata.liked; + this.$message({ + message: '更新收藏状态失败,请稍后重试', + type: 'error' + }); + } else { + this.$message.success('更新收藏状态成功'); + } + }) + .catch(error => { + console.error("An error occurred while synchronizing data with the server", error); + // 如果服务器响应错误,将状态还原 + this.tableData[index].metadata.liked = !this.tableData[index].metadata.liked; + this.$message({ + message: '同步服务器失败,请检查网络连接', + type: 'error' + }); + }); + }, updateStats() { this.Number = this.tableData.length; }, @@ -430,6 +715,8 @@ } }, mounted() { + window.addEventListener('resize', this.calculatePageSize); + this.updateWindowWidth(); fetch("./api/manage/check", { method: 'GET', credentials: 'include' }) .then(response => response.text()) .then(result => { @@ -446,7 +733,15 @@ fetch("./api/manage/list", { method: 'GET', credentials: 'include' }) .then(response => response.json()) .then(result => { - this.tableData = result.map(file => ({ ...file, selected: false })); + console.log("result: ", result); + this.tableData = result.map(file => ({ + ...file, + selected: false, + metadata: { + ...file.metadata, + liked: file.metadata.liked !== undefined ? file.metadata.liked : false + } + })); this.updateStats(); const savedSortOption = localStorage.getItem('sortOption'); if (savedSortOption) { @@ -455,9 +750,10 @@ this.sortData(this.tableData); }) .catch(() => this.$message.error('同步数据时出错,请检查网络连接')); + }, + beforeDestroy() { + window.removeEventListener('resize', this.calculatePageSize); } }); - - - \ No newline at end of file + \ No newline at end of file diff --git a/functions/api/manage/list.js b/functions/api/manage/list.js index d62f4b09c..88e023ea6 100644 --- a/functions/api/manage/list.js +++ b/functions/api/manage/list.js @@ -1,30 +1,30 @@ export async function onRequest(context) { - // Contents of context object - const { - request, // same as existing Worker API - env, // same as existing Worker API - params, // if filename includes [id] or [[path]] - waitUntil, // same as ctx.waitUntil in existing Worker API - next, // used for middleware or to fetch assets - data, // arbitrary space for passing data between middlewares - } = context; - console.log(env) - const value = await env.img_url.list(); + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + console.log(env) + const value = await env.img_url.list(); - console.log(value) - //let res=[] - //for (let i in value.keys){ - //add to res - //"metadata":{"TimeStamp":19876541,"ListType":"None","rating_label":"None"} - //let tmp = { - // name: value.keys[i].name, - // TimeStamp: value.keys[i].metadata.TimeStamp, - // ListType: value.keys[i].metadata.ListType, - // rating_label: value.keys[i].metadata.rating_label, - //} - //res.push(tmp) + console.log(value) + //let res=[] + //for (let i in value.keys){ + //add to res + //"metadata":{"TimeStamp":19876541,"ListType":"None","rating_label":"None"} + //let tmp = { + // name: value.keys[i].name, + // TimeStamp: value.keys[i].metadata.TimeStamp, + // ListType: value.keys[i].metadata.ListType, + // rating_label: value.keys[i].metadata.rating_label, //} - const info = JSON.stringify(value.keys); - return new Response(info); + //res.push(tmp) + //} + const info = JSON.stringify(value.keys); + return new Response(info); - } \ No newline at end of file +} \ No newline at end of file diff --git a/functions/api/manage/toggleLike/[id].js b/functions/api/manage/toggleLike/[id].js new file mode 100644 index 000000000..857d235d0 --- /dev/null +++ b/functions/api/manage/toggleLike/[id].js @@ -0,0 +1,22 @@ +export async function onRequest(context) { + const { params, env } = context; + + console.log("Request ID:", params.id); + + // 获取元数据 + const value = await env.img_url.getWithMetadata(params.id); + console.log("Current metadata:", value); + + // 如果记录不存在 + if (!value.metadata) return new Response(`Image metadata not found for ID: ${params.id}`, { status: 404 }); + + // 切换 liked 状态并更新 + value.metadata.liked = !value.metadata.liked; + await env.img_url.put(params.id, "", { metadata: value.metadata }); + + console.log("Updated metadata:", value.metadata); + + return new Response(JSON.stringify({ success: true, liked: value.metadata.liked }), { + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/functions/file/[id].js b/functions/file/[id].js index ba2c6a711..48f8f586d 100644 --- a/functions/file/[id].js +++ b/functions/file/[id].js @@ -1,11 +1,8 @@ -export async function onRequest(context) { // Contents of context object +export async function onRequest(context) { const { - request, // same as existing Worker API - env, // same as existing Worker API - params, // if filename includes [id] or [[path]] - waitUntil, // same as ctx.waitUntil in existing Worker API - next, // used for middleware or to fetch assets - data, // arbitrary space for passing data between middlewares + request, + env, + params, } = context; const url = new URL(request.url); @@ -32,95 +29,78 @@ export async function onRequest(context) { // Contents of context object method: request.method, headers: request.headers, body: request.body, - }) - console.log(response.ok); // true if the response status is 2xx - console.log(response.status); // 200 + }); + + // Log response details + console.log(response.ok, response.status); + + // If the response is OK, proceed with further checks if (response.ok) { - // Referer header equal to the admin page - console.log(url.origin + "/admin") - if (request.headers.get('Referer') == url.origin + "/admin") { - //show the image + // Allow the admin page to directly view the image + if (request.headers.get('Referer') === `${url.origin}/admin`) { return response; } - if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { } else { - //check the record from kv + // Fetch KV metadata if available + if (env.img_url) { const record = await env.img_url.getWithMetadata(params.id); - console.log("record") - console.log(record) - if (record.metadata === null) { - - } else { - - //if the record is not null, redirect to the image - if (record.metadata.ListType == "White") { + console.log("Record:", record); + + // Ensure metadata exists and add default values for missing properties + if (record && record.metadata) { + const metadata = { + ListType: record.metadata.ListType || "None", + Label: record.metadata.Label || "None", + TimeStamp: record.metadata.TimeStamp || Date.now(), + liked: record.metadata.liked !== undefined ? record.metadata.liked : false + }; + + // Handle based on ListType and Label + if (metadata.ListType === "White") { return response; - } else if (record.metadata.ListType == "Block") { - console.log("Referer") - console.log(request.headers.get('Referer')) - if (typeof request.headers.get('Referer') == "undefined" || request.headers.get('Referer') == null || request.headers.get('Referer') == "") { - return Response.redirect(url.origin + "/block-img.html", 302) - } else { - return Response.redirect("https://static-res.pages.dev/teleimage/img-block-compressed.png", 302) - } - - } else if (record.metadata.Label == "adult") { - if (typeof request.headers.get('Referer') == "undefined" || request.headers.get('Referer') == null || request.headers.get('Referer') == "") { - return Response.redirect(url.origin + "/block-img.html", 302) - } else { - return Response.redirect("https://static-res.pages.dev/teleimage/img-block-compressed.png", 302) - } + } else if (metadata.ListType === "Block" || metadata.Label === "adult") { + const referer = request.headers.get('Referer'); + const redirectUrl = referer ? "https://static-res.pages.dev/teleimage/img-block-compressed.png" : `${url.origin}/block-img.html`; + return Response.redirect(redirectUrl, 302); } - //check if the env variables WhiteList_Mode are set - console.log("env.WhiteList_Mode:", env.WhiteList_Mode) - if (env.WhiteList_Mode == "true") { - //if the env variables WhiteList_Mode are set, redirect to the image - return Response.redirect(url.origin + "/whitelist-on.html", 302); - } else { - //if the env variables WhiteList_Mode are not set, redirect to the image - return response; + + // Check if WhiteList_Mode is enabled + if (env.WhiteList_Mode === "true") { + return Response.redirect(`${url.origin}/whitelist-on.html`, 302); } + } else { + // If metadata does not exist, initialize it in KV with default values + await env.img_url.put(params.id, "", { + metadata: { ListType: "None", Label: "None", TimeStamp: Date.now(), liked: false }, + }); } - } - //get time - let time = new Date().getTime(); - - let apikey = env.ModerateContentApiKey - - if (typeof apikey == "undefined" || apikey == null || apikey == "") { + // If no metadata or further actions required, moderate content and add to KV if needed + const time = Date.now(); + if (env.ModerateContentApiKey) { + const moderateResponse = await fetch(`https://api.moderatecontent.com/moderate/?key=${env.ModerateContentApiKey}&url=https://telegra.ph${url.pathname}${url.search}`); + const moderateData = await moderateResponse.json(); + console.log("Moderate Data:", moderateData); - if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { - console.log("Not enbaled KV") - - } else { - //add image to kv + if (env.img_url) { await env.img_url.put(params.id, "", { - metadata: { ListType: "None", Label: "None", TimeStamp: time }, + metadata: { ListType: "None", Label: moderateData.rating_label, TimeStamp: time, liked: false }, }); - } - } else { - await fetch(`https://api.moderatecontent.com/moderate/?key=` + apikey + `&url=${fileUrl}`). - then(async (response) => { - let moderate_data = await response.json(); - console.log(moderate_data) - console.log("---env.img_url---") - console.log(env.img_url == "true") - if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { } else { - //add image to kv - await env.img_url.put(params.id, "", { - metadata: { ListType: "None", Label: moderate_data.rating_label, TimeStamp: time }, - }); - } - if (moderate_data.rating_label == "adult") { - return Response.redirect(url.origin + "/block-img.html", 302) - } - }); + if (moderateData.rating_label === "adult") { + return Response.redirect(`${url.origin}/block-img.html`, 302); + } + } else if (env.img_url) { + // Add image to KV with default metadata if ModerateContentApiKey is not available + console.log("KV not enabled for moderation, adding default metadata."); + await env.img_url.put(params.id, "", { + metadata: { ListType: "None", Label: "None", TimeStamp: time, liked: false }, + }); } } + return response; }