写点什么

从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享

作者:JYeontu
  • 2023-10-28
    广东
  • 本文字数:8312 字

    阅读完需:约 27 分钟

从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享

说在前面

平时写文章或写代码的时候,都少不了需要将本地图片转成在线图片链接,大家都是使用什么工具进行转换的呢?相信很多人都有自己的图床工具,今天来给大家介绍一下,怎么基于 Gitee 和 Electron 来开发一个便捷的图床工具,支持图片的上传、删除、复制和快速生成 markdown 链接、快捷键唤起和隐藏面板,粘贴剪切板图片上传等……

框架选型

原本只是想写一个 Chrome 插件来实现简单功能,后面发现 Chrome 插件的局限性太大了,所以最后还是选择使用Electron来制作一个桌面程序。


存储方面我们可以直接使用 gitee 来用做图库存储,不需要额外去购买存储服务器。

准备工作

一、Gitee 创建图床仓库目录

1、Gitee 注册

直接到 Gitee 官网: Gitee - 基于 Git 的代码托管和研发协作平台 ,点击注册即可。


2、仓库创建

注册完账号后直接登录,在首页点击右上角的加号可以新建仓库



仓库信息自行填写即可



创建完仓库后我们可以新建一个文件夹用来存储图片:


3、生成授权码

打开设置里的私人令牌页面



点击生成新令牌,根据提示填写信息即可,注意保存好生成的令牌。


二、搭建 electron 项目

我们可以先搭建一个简单的 electron 项目:


  1. 安装 Node.js:确保你的电脑上已经安装了 Node.js。你可以从 Node.js 官方网站(https://nodejs.org)下载并安装最新版本的 Node.js。

  2. 创建项目目录:在你想要创建项目的位置,创建一个新的文件夹作为项目目录。

  3. 初始化项目:打开命令行终端,进入到项目目录,并执行以下命令初始化一个新的 npm 项目:


npm init -y
复制代码


  1. 安装 Electron:在命令行终端中执行以下命令来安装 Electron:


npm install electron
复制代码


  1. 创建主文件:在项目目录中创建一个名为 main.js 的文件,作为 Electron 应用的主文件。

  2. 编写主文件代码:在 main.js 文件中编写 Electron 应用的主要逻辑。例如,下面是一个简单的示例:


const { app, BrowserWindow } = require('electron');
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } });
win.loadFile('index.html');}
app.whenReady().then(() => { createWindow();
app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });});
app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit();});
复制代码


  1. 创建 HTML 文件:在项目目录中创建一个名为 index.html 的文件,作为 Electron 应用的初始页面。

  2. 编写 HTML 文件代码:在 index.html 文件中编写你的应用界面的 HTML 代码。

  3. package.json 文件中添加启动命令:打开 package.json 文件,在 "scripts" 部分添加以下内容:


"scripts": {  "start": "electron ."}
复制代码


  1. 启动 Electron 应用:在命令行终端中执行以下命令来启动 Electron 应用:


npm start
复制代码

功能实现

前面准备工作全都完成后,现在我们就有了一个简单 electron 项目架子和一个 gitee 仓库,可以开始来实现相关的功能了。

一、git 操作

gitee 提供了 api 文档,我们可以通过 gitee 的 api 文档来对我们的仓库进行上传图片和获取图片的操作。


gitee API 文档地址:https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no


这里我将需要使用到的功能写成了一个类:

1、初始化,获取配置信息

  • accessToken


用户授权码,也就是我们前面生成的私人令牌。



  • username


仓库所属空间地址(企业、组织或个人的地址 path),如下图:



  • repo


仓库路径(path),如下图:



  • dirPath


图片存放目录地址,如下图:



  • branchName


分支名,默认为master,我们可以修改成指定分支:



  init(config = {}) {    // 设置 Gitee 仓库信息和目录路径    this.username = config.username;    this.repo = config.repo;    this.accessToken = config.accessToken;    this.branchName = config.branchName || "master";    this.apiUrl = "https://gitee.com/api/v5/repos/";    this.dirPath = config.dirPath;  }
复制代码


将以上配置信息在程序的配置里设置好即可:


2、上传图片到 gitee 图床目录下

根据 API 文档进行请求即可:


  async uploadToGitee(base64Data) {    try {      const formData = new FormData();      formData.append("content", base64Data);      formData.append("access_token", this.accessToken);      formData.append("message", "上传图片");
const timeStamp = new Date().getTime(); Toast.showLoading("正在上传"); const response = await fetch( `${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}${timeStamp}.jpg`, { method: "POST", body: formData, } ); Toast.hide(); if (!response.ok) { throw new Error("上传图片失败"); }
const data = await response.json(); Toast.showToast("图片上传成功!"); return data.content.download_url; } catch (error) { console.error(error); Toast.showToast("图片上传失败!"); throw error; } }
复制代码

3、获取 gitee 图床目录下的所有图片

根据 API 文档进行请求即可:


async getImg() {    try {      const response = await fetch(        `${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}`,        {          headers: {            Authorization: `token ${this.accessToken}`,          },        }      );
if (!response.ok) { throw new Error("获取图片列表失败"); }
const data = await response.json();
// 筛选出图片文件 const imageFiles = data.filter( (file) => file.type === "file" && file.name.match(/\.(jpg|jpeg|png|gif)$/i) );
return imageFiles; } catch (error) { console.error(error); throw error; } }
复制代码

4、删除 gitee 图床目录下指定图片

根据 API 文档进行请求即可:


  async deleteImg(fileName, sha, cb) {    try {      const response = await fetch(        `${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}/${fileName}?access_token=${this.accessToken}&ref=${this.branchName}`,        {          method: "DELETE",          headers: {            "Content-Type": "application/json;charset=utf-8",          },          body: JSON.stringify({            message: "删除图片",            sha,            prune: true,          }),        }      );
if (!response.ok) { throw new Error("删除图片失败"); }
Toast.showToast("删除成功"); cb && cb(); } catch (error) { console.error(error); Toast.showToast("删除失败"); throw error; } }
复制代码

二、拖拽点击、粘贴、选择文件夹上传图片

我们可以通过三种方式来上传我们本地的图片



1、拖拽或点击上传图片

前面有写了一篇实现拖拽或点击上传图片的文章,这里就不详细再赘述了,有兴趣的可以去看看:《文件拖拽上传功能已经烂大街了,你还不会吗?》

2、粘贴上传

平时我们经常会使用到截图,所以我们希望可以直接将截图粘贴到工具中进行上传,这里我们可以通过监听页面上的paste事件,直接读取剪切板的图片展示到页面上并进行上传。


document.addEventListener("paste", function (e) {  const items = e.clipboardData.items;
for (const item of items) { if (item.type.indexOf("image") !== -1) { const blob = item.getAsFile(); showPreview(blob); } }});
复制代码

3、选择文件夹上传文件夹中的所有图片

一张图片一张图片上传太慢了,所以我们也支持直接选择一个文件夹,将文件夹里的所有图片一次性上传到 gitee 上,


  • 创建一个 HTML 文件,包含一个用于选择文件夹的 <input> 元素和一个按钮用于触发上传操作。代码如下:


<input    type="file"    style="display: none"    id="folderInput"    onchange="uploadImages()"    webkitdirectory    multiple  />  <button class="upload-btn" onclick="selectFolder()" style="background: #FFD04C;">    选择文件夹上传  </button>
复制代码


webkitdirectory:告诉浏览器文件选择器应该允许选择文件夹(目录),而不仅仅是单个文件。


multiple:告诉浏览器文件选择器应该允许选择多个文件或文件夹。


  • 2、获取到选择的文件并做相关处理


function blobToBase64(blob) {  return new Promise((resolve, reject) => {    const reader = new FileReader();    reader.onloadend = () => {      const base64String = reader.result.split(",")[1];      resolve(base64String);    };    reader.onerror = reject;    reader.readAsDataURL(blob);  });}function selectFolder() {  const folderInput = document.getElementById("folderInput");  folderInput.click();}
async function uploadImages() { const folderInput = document.getElementById("folderInput"); let files = folderInput.files || []; files = [...files].filter((file) => file.type.startsWith("image/"));
for (let i = 0; i < files.length; i++) { Toast.showLoading(`上传中,${i}/${files.length}`); const file = files[i]; const base64Data = await blobToBase64(file); await gitOperate.uploadToGitee(base64Data,false); } Toast.hide(); Toast.showToast(`已全部上传`); waterfall.init();}
复制代码

三、瀑布流展示图片


上传完图片后,我们还希望可以看到之前上传的图片,这里我们需要对图片列表做一个瀑布流展示。



之前我也有写过一个瀑布流组件详细的实现步骤,有兴趣的同学可以看看:《Vue封装一个瀑布流图片容器组件》


这里我们可以通过原生 JavaScrip 来快速实现一个,具体代码如下:


class WaterfallContent {  constructor(config = {}) {    this.init(config);  }  init(config = {}) {    this.imgList = config.imgList || [];    this.column = config.column || 8;    this.imgMargin = config.imgMargin || 0.5;    this.domId = config.domId || "waterfall-container";    this.minHeight = [];    this.arr = [];    const ul = document.getElementById(this.domId);    ul.innerHTML = "";  }  async create(imgList = this.imgList, cb) {    this.init();    this.imgList = imgList;    const ul = document.getElementById(this.domId);    ul.innerHTML = "";    let trueWidth = Math.floor(      (100 - this.column * this.imgMargin * 2) / this.column    );    let trueWidthPx = 0;    for (let i = 0; i < this.column; i++) {      let li = document.createElement("li");      li.style.listStyle = "none";      li.style.float = "left";      li.style.width = `${trueWidth}%`;      li.style.margin = `0 ${this.imgMargin}%`;      li.classList.add("git-img");
ul.appendChild(li); this.arr.push(li); this.minHeight.push(0); trueWidthPx = li.offsetWidth; } this.loadHandler(trueWidthPx, cb); } getBase64(file) { const reader = new FileReader(); reader.readAsDataURL(file); return new Promise((resolve) => { reader.onload = () => { resolve(reader.result); }; }); } getImgPx(img, maxWidth) { const image = new Image(); image.src = img; return new Promise((resolve) => { image.onload = () => { const width = image.width; const height = image.height; image.width = maxWidth; image.height = image.height * (maxWidth / width); resolve({ width, height, image }); }; }); } async loadHandler(trueWidth, cb) { for (let i = 0; i < this.imgList.length; i++) { const imgItem = this.imgList[i]; const src = imgItem.download_url; const res = await this.getImgPx(src, trueWidth); const { image } = res; const minHeight = this.minHeight; const arr = this.arr; // 高度数组的最小值 const min = Math.min.apply(null, minHeight); // 高度数组的最小值索引 const minIndex = minHeight.indexOf(min); // 克隆一份图片 const im = image.cloneNode(true); im.setAttribute("data-sha", imgItem.sha); im.onclick = this.imgClick; // 将图片假如对应最小值索引的容器中 arr[minIndex].appendChild(im); // 更新最小值索引的容器的高度 minHeight[minIndex] += im.height; if (i === 0 && cb) { cb(); } } }}
复制代码

四、自定义鼠标右键菜单栏


查看图片的时候我们希望可以对图片进行操作,这里的操作我们通过鼠标右键点击弹出,所以我们可以实现一个自定义鼠标右键菜单栏。


具体代码如下:


class MouseMenu {  constructor(config) {    this.menuClass = config.menuClass || "j-mouse-menu";    this.menuId = config.menuId || "JMouseMenu";    this.contentId = config.contentId || "j-mouse-content";    this.init();  }  init() {    const dom = document.getElementById(this.contentId);    dom.oncontextmenu = (e) => {      const clickItem = e.path[0];      if (clickItem.localName !== "img") return;      const menu = document.getElementById(this.menuId);      this.clickItem = clickItem;      this.hideAllMenu();      // 自定义body元素的鼠标事件处理函数      e = e || window.event;      e.preventDefault();      let scrollTop =        document.documentElement.scrollTop || document.body.scrollTop; // 获取垂直滚动条位置      let scrollLeft =        document.documentElement.scrollLeft || document.body.scrollLeft; // 获取水平滚动条位置      menu.style.display = "block";      let left = e.clientX + scrollLeft;      let top = e.clientY + scrollTop;      if (menu.offsetHeight + top > window.innerHeight) {        top = window.innerHeight - menu.offsetHeight - 5;      }      if (menu.offsetWidth + left > window.innerWidth) {        left = window.innerWidth - menu.offsetWidth - 5;      }      menu.style.left = left + "px";      menu.style.top = top + "px";      document.onclick = () => {        this.hideAllMenu();      };    };  }  hideAllMenu() {    const jMenu = document.getElementsByClassName("j-mouse-menu");    for (const item of jMenu) {      item.style.display = "none";    }  }}
复制代码

五、Toast 提示功能


交互少不了 Toast 弹窗提示,这里我们使用 JavaScrip 简单实现一个,具体代码如下:


class ToastC {  constructor(config) {    this.config = config;    this.init();  }
init() { const body = document.body; const toastContainer = document.createElement("div"); toastContainer.id = "toastContainer"; const styleObj = { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "rgba(0, 0, 0, 0.8)", color: "#ffffff", fontSize: "16px", opacity: 0.7, transition: "opacity 0.3s ease-in-out", padding: "10px", "border-radius": "5px", display: "none", textAlign: "center", }; for (const key in styleObj) toastContainer.style[key] = styleObj[key]; body.appendChild(toastContainer);
const loader = document.createElement("div"); loader.id = "toastLoader"; const loaderStyleObj = { border: "4px solid #f3f3f3", borderTop: "4px solid #3498db", borderRadius: "50%", width: "20px", height: "20px", animation: "spin 1s linear infinite", margin: "0 auto 10px", display: "none", }; for (const key in loaderStyleObj) loader.style[key] = loaderStyleObj[key]; toastContainer.appendChild(loader);
const text = document.createElement("div"); text.id = "toastText"; const textStyleObj = { marginTop: "5px", }; for (const key in textStyleObj) text.style[key] = textStyleObj[key]; toastContainer.appendChild(text);
const keyframes = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; const style = document.createElement("style"); style.innerHTML = keyframes; document.head.appendChild(style); }
showToast(text) { const textElem = document.getElementById("toastText");
// 设置Toast提示文本 textElem.innerText = text;
// 显示Toast提示 toastContainer.style.display = "block";
// 3秒后隐藏Toast提示 setTimeout(() => this.hide(), 3000); }
showLoading(text = "加载中...") { const loader = document.getElementById("toastLoader"); const textElem = document.getElementById("toastText");
// 设置Toast提示文本为加载中 textElem.innerText = text;
// 显示Toast提示和加载动画 loader.style.display = "block"; this.show(); }
show() { const toastContainer = document.getElementById("toastContainer");
// 显示Toast提示 toastContainer.style.display = "block"; }
hide() { try { const toastContainer = document.getElementById("toastContainer"); const loader = document.getElementById("toastLoader");
// 隐藏Toast提示和加载动画 toastContainer.style.display = "none"; loader.style.display = "none"; } catch (err) {} }}
复制代码

六、快捷键打开隐藏窗口

我们可以设置快捷键快速唤起和隐藏窗口,在根目录下的 main.js 文件中注册快捷键,这里我设置的是alt + x,大家也可以改成自己喜欢的快捷键,具体代码如下:


  // 注册快捷键  globalShortcut.register("Alt+X", () => {    if (mainWindow.isVisible()) {      mainWindow.hide();    } else {      mainWindow.show();    }  });
复制代码

工具使用

一、源码下载

直接到 gitee 上下载即可。


二、依赖安装

下载完源码之后,我们到 gitImgBed 目录下运行npm i安装依赖,等待依赖安装完成。


三、程序打包

依赖安装完成之后,我们可以在 gitImgBed 目录下运行npm run build进行打包



打包完成后我们可以在当前目录下看到一个叫jyeontuGitImgBed-win32-x64文件夹,打开文件夹,找到里面一个叫jyeontuGitImgBed的应用程序,双击启动即可


四、配置填写

将之前准备工作时间的 gitee 仓库相关信息填写到配置中。



输入正确信息后保存,便可以上传和查看 gitee 图床中的图片了。




源码

一、gitee

gitee 地址:https://gitee.com/zheng_yongtao/electron_program

二、公众号

关注公众号『前端也能这么有趣』发送 图床即可获取源码。

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

发布于: 2023-10-28阅读数: 4
用户头像

JYeontu

关注

还未添加个人签名 2023-02-07 加入

还未添加个人简介

评论

发布
暂无评论
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享_前端_JYeontu_InfoQ写作社区