diff --git a/README-EN.md b/README-EN.md index f485cc706..74f32c5a0 100644 --- a/README-EN.md +++ b/README-EN.md @@ -4,6 +4,24 @@ Free Image Hosting solution, Flickr/imgur alternative. Using Cloudflare Pages an English|[中文](readme-zh.md) +Use tg channel/chat for storage + +How to use? + +First, you need to create a new telegram bot to obtain the token and a telegram channel to obtain Chat_ID +## How to Obtain `Bot_Token` and `Chat_ID` for Telegram + +If you don't have a Telegram account yet, please create one first. Then, follow these steps to get the `Bot_Token` and `Chat_ID`: + +1. **Get the `Bot_Token`** + - In Telegram, send the command `/newbot` to [@BotFather](https://t.me/BotFather), and follow the prompts to input your bot's name and username. Once successfully created, you will receive a `Bot_Token`, which is used to interact with the Telegram API. + +2. **Set the bot as a channel administrator** + - Create a new channel and, after entering the channel, go to channel settings. Add the bot you just created as a channel administrator, so it can send messages. + +3. **Get the `Chat_ID`** + - Use [@GetTheirIDBot](https://t.me/GetTheirIDBot) to get your channel ID. Send a message to this bot and follow the instructions to receive your `Chat_ID` (the ID of your channel). + ## Deployment ### Preparation diff --git a/README.md b/README.md index 9c540cd59..98fca1e97 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,38 @@ [English](README-EN.md)|中文 +> [!IMPORTANT] +> +> 由于原有的Telegraph API接口被官方关闭,需要将上传渠道切换至Telegram Channel,请按照文档中的部署要求设置`TG_Bot_Token`和`TG_Chat_ID`,否则将无法正常使用上传功能。 + +## 如何获取Telegram的`Bot_Token`和`Chat_ID` + +如果您还没有Telegram账户,请先创建一个。接着,按照以下步骤操作以获取`BOT_TOKEN`和`CHAT_ID`: + +1. **获取`Bot_Token`** + - 在Telegram中,向[@BotFather](https://t.me/BotFather)发送命令`/newbot`,根据提示依次输入您的机器人名称和用户名。成功创建机器人后,您将会收到一个`BOT_TOKEN`,用于与Telegram API进行交互。 + +![202409071744569](https://github.com/user-attachments/assets/04f01289-205c-43e0-ba03-d9ab3465e349) + +2. **设置机器人为频道管理员** + - 创建一个新的频道(Channel),进入该频道后,选择频道设置。将刚刚创建的机器人添加为频道管理员,这样机器人才能发送消息。 + +![202409071758534](https://github.com/user-attachments/assets/cedea4c7-8b31-42e0-98a1-8a72ff69528f) + +![202409071758796](https://github.com/user-attachments/assets/16393802-17eb-4ae4-a758-f0fdb7aaebc4) + + +3. **获取`Chat_ID`** + - 通过[@VersaToolsBot](https://t.me/VersaToolsBot)获取您的频道ID。向该机器人发送消息,按照指示操作,最后您将得到`CHAT_ID`(即频道的ID)。 + - 或者通过[@GetTheirIDBot](https://t.me/GetTheirIDBot)获取您的频道ID。向该机器人发送消息,按照指示操作,最后您将得到`CHAT_ID`(即频道的ID)。 + + ![202409071751619](https://github.com/user-attachments/assets/59fe8b20-c969-4d13-8d46-e58c0e8b9e79) + +最后去Cloudflare Pages后台设置相关的环境变量(注:修改环境变量后,需要重新部署才能生效) +| 环境变量 | 示例值 | 说明 | +|-----------------|---------------------------|----------------------------------------------------------------------------------------| +| `TG_Bot_Token` | `123468:AAxxxGKrn5` | 从[@BotFather](https://t.me/BotFather)获取的Telegram Bot Token。 | +| `TG_Chat_ID` | `-1234567` | 频道的ID,确保TG Bot是该频道或群组的管理员。 | ## 如何部署 diff --git a/admin-imgtc.html b/admin-imgtc.html index 9aefb5811..0ab703b37 100644 --- a/admin-imgtc.html +++ b/admin-imgtc.html @@ -1,304 +1,381 @@ - - + + + - - - ImgTC | Admin - - - - - - - -
- - -
- Dashboard -
- -
- - 记录总数量: {{ Number }} - -
- - - - + + + ImgTC | Admin + + + + + + + +
+ + +
+ Dashboard +
+ +
+ + + 记录数: {{ Number }} - - 按时间倒序 - 按名称升序 - - - - - - - - - - - + +
+ + + + + + + 按时间倒序 + 按名称升序 + + + + + + + + + + + + +
-
- - -
- +
+
+ + +
+ +
Powered By
+ +
Telegraph-Image
+
+
+
+ +
+ @@ -307,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() { @@ -328,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: { @@ -346,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: '确定', @@ -356,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('已取消删除')); @@ -407,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; }, @@ -426,15 +715,33 @@ } }, mounted() { + window.addEventListener('resize', this.calculatePageSize); + this.updateWindowWidth(); fetch("./api/manage/check", { method: 'GET', credentials: 'include' }) .then(response => response.text()) - .then(result => result === "true" ? this.showLogoutButton = true : window.location.href = "./api/manage/login") + .then(result => { + if (result == "true") { + this.showLogoutButton = true; + } else if (result == "Not using basic auth.") { + // Do nothing + } else { + window.location.href = "./api/manage/login"; + } + }) .catch(() => this.$message.error('同步数据时出错,请检查网络连接')); 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) { @@ -443,8 +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/admin.html b/admin.html index 10719b682..943e84e04 100644 --- a/admin.html +++ b/admin.html @@ -1,64 +1,61 @@ + - + +
- -
Dashboard - - - - 网格视图 - - - 瀑布流 - - 退出登录
-
- - - - 记录总数量: - {{ Number }} - - - - -