vue3+ts+pinia+vite做一个你自己的图床

起因

事情是这样的,vue3出现已经很久了,而在我日常工作的过程中大多数仍然在用vue2版本,新技术的学习总是需要做一些项目然后不断踩坑才能巩固。再加上平常写作的过程中存,所使用的图片分散各地没办法有一个统一的图床来管理。

因此在不考虑项目稳定性的情况下,我选用了都是比较新的技术来实现这一效果,还用上了cdn的加速compressorjs的压缩,让图片展示的效果更好,当然你现在看的这篇文章中的图片也存放在这个项目中

废话不多说,先看效果

演示地址

  • 配置界面

    [图片上传失败...(image-e89d7e-1665283116848)]

    [图片上传失败...(image-9dfded-1665283116848)]
    在这里得先输入你的GitHub token 如何生成我已经写在链接中了,在此不在赘述,如果你只是想体验,或者懒得配置,也可以点不想配置获取一个我设置好的通用token

  • 操作相关
    选择一个仓库后就可以对图片进行简单的增删查改操作了,简单截几张图
    [图片上传失败...(image-26e5d4-1665283116848)]
    点击上传,这里- 支持点击、拖拽、复制图片上传,当然都是可以批量操作的
    [图片上传失败...(image-9db1dc-1665283116848)]

    这里可以选择原图或者经过compressorjs压缩过的图片,图片不会有太大失真
    [图片上传失败...(image-e89db2-1665283116848)]

    [图片上传失败...(image-32312c-1665283116848)]
    上传成功之后就可以点图片进行删除或者获取图片的markdown,cdn地址了,这对我们日常写作的帮助还是很大的

工程结构

以上就是简单的项目介绍,那么现在看看我们的项目工程

├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── README.md
├── components.d.ts
├── src
│   ├── App.vue
│   ├── assets
│   ├── axios
│   ├── components
│   ├── constant
│   ├── env.d.ts
│   ├── main.ts
│   ├── router
│   ├── store
│   ├── util
│   └── view
├── tsconfig.json
├── vite.config.bak
├── vite.config.ts

可以看到我们的模块还是很简单的,前端技术选型也是vue3作为主体,用pinia代替原有的vuex作为整个项目的全局信息管理工具,然后ui库选用来naiveui,最后的工程化工具选取了vite

这里就不细致到每一步的操作,如果有疑问可以联系我,或者直接阅读源码,下面就看下几个比较重要的模块

模块拆解

  • 插件的安装 项目初始化

    npm i vite-plugin-components -D # yarn add vite-plugin-components -D
    
    import Vue from '@vitejs/plugin-vue'
    import ViteComponents from 'vite-plugin-components'
    
    export default {
      plugins: [
        Vue(),
        ViteComponents()
      ],
    };
    

    这里用了vite-plugin-components这个插件能让那个我们在开发过程中,使用插件按需引入,且直接在模板使用即可

工具类的配置

  import axios from 'axios';
  import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
  import {FormatErrorMessage} from "@/util/util";
  //将axios封装到类中
  class Request {
      instance: AxiosInstance; //axios的实例将被保存到这里
      constructor(config: AxiosRequestConfig) {
          //构造器里的config包括baseURL,timeout等
          this.instance = axios.create(config); //创建axios实例
          this.instance.interceptors.request.use(
              //实例中的请求拦截器
              (config: AxiosRequestConfig) => {
                  //请求成功的拦截
                  let token = localStorage.getItem('github_token');
                  if (token) {
                      config.headers = {
                          Authorization: `token ${token}`,
                      };
                  }
                  return config;
              },
              (error) => {
                  //请求失败的拦截
                  return Promise.reject(error);
              },
          );
          this.instance.interceptors.response.use(
              //实例中的响应拦截器
              (response: AxiosResponse) => {
                  //响应成功的拦截
                  return response;
              },
              (error) => {
                  window.$message.error(FormatErrorMessage(error.response.data.message),)
                  //响应失败的拦截
                  return Promise.reject(error);
              },
          );
      }

图片压缩

const uploadHelper = (file: File, folder: string) => {
    return new Promise(async (resolve) => {
        const CompressorFile = new Promise((resolve, reject) => {
            new Compressor(file, {
                quality: 0.6,
                strict: true,
                success(result) {
                    resolve(result);
                },
                error(err) {
                    reject(err.message);
                },
            });
        });

        const compressFile = await CompressorFile as File;
        const base64File: any = await fileByBase64(compressFile); // 压缩过的图
        const original_base64: any = await fileByBase64(file); // 原始图

        resolve({
            name: `${file.name.substring(
                0,
                file.name.lastIndexOf('.'),
            )}_${createHash(6)}`,
            original_size: file.size,
            compress_size: compressFile.size||0,
            base64data: base64File.replace(/^data:image/\w+;base64,/, ''),
            base64: base64File,
            original_base64data: original_base64.replace(
                /^data:image/\w+;base64,/,
                '',
            ),
            original_base64: original_base64,
            folder: folder,
            ext: getFileExtendingName(file.name), // 获取文件后缀名
        });
    });
};

拖拽,粘贴事件的监听

<div
    class="upload-modal transition"
    id="drop-area"
    @paste="PasteUpload"
    :class="{ isOpen: props.isOpen }"
>
//......
</div>
// 在vue 文件中
onMounted(() => {
    // 拖拽接听
    let drop_area:Document= document.getElementById('drop-area');
    drop_area.addEventListener('drop', DropUpload, false);
    let timer: any = '';
    drop_area.addEventListener('dragleave', (e: Event) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            e.stopPropagation();
            e.preventDefault();
            drop_active.value = false;
        }, 200);
    });

    drop_area.addEventListener('dragover', (e: Event) => {
        e.stopPropagation();
        e.preventDefault();
        drop_active.value = true;
    });
});

// 拖拽上传
const DropUpload = (e: any) => {
    if (!folder.value) {
        window.$message.error('请选择文件夹');
        return;
    }
    let files = e.dataTransfer.files;
    drop_active.value = false;
    e.stopPropagation();
    e.preventDefault();
    let image_files = [] as any;
    // 搜索剪切板items
    for (let i = 0; i <= files.length - 1; i++) {
        console.log(files[i].type.indexOf('image') !== -1);
        if (files[i].type.indexOf('image') !== -1) {
            image_files.push(files[i]);
        } else {
            window.$message.error(`${files[i].name} 不是图片文件`);
        }
    }
    AddImageToList(image_files); //添加文件上传
};
// 监听粘贴操作
const PasteUpload = (e: any) => {
    if (!folder.value) {
        window.$message.error('请选择文件夹');
        return;
    }
    const items = e.clipboardData.items; //  获取剪贴板中的数据
    let files: any = null; //  用来保存 files 对象
    if (items.length > 0) {
        //  判断剪贴板中是否是文件
        if (items[0].kind == 'file') {
            files = items[0].getAsFile(); //  获取文件
            //  判断是否是图片
            if (files.type.indexOf('image') >= 0) {
                AddImageToList([files]);
            } else {
                window.$message.error('请粘贴图片文件');
            }
        } else {
            window.$message.error('请粘贴图片文件');
        }
    }
};

用户信息的保留

├── store
│   ├── index.ts
│   ├── info.ts
// index.ts
import { createPinia } from 'pinia'
const store = createPinia()
export default store
// info.ts
import { defineStore } from 'pinia';
export const useInfoStore = defineStore({
    id: 'info', // id必填,且需要唯一
    state: () => {
        return {
            github_token: '', //初始化变量
        };
    },
    actions: {
        updateToken(token: string) {
            this.github_token = token;
        }
        getToken(){
            return  this.github_token
        }
        //......
    },
});

这里就是我们使用pinia构建的store,用来存放全局的数据,例如用户信息等等

// vue文件中使用
const infoStore = useInfoStore(); // 声明store
infoStore.updateToken('value') // 更新token值
infoStore.getToken() // 获取最新的token值

pinia的用法比较简单,只要先声明完成后,就可以在在vue文件中调用香港的方法了

vite图片引用

// 获取assets静态资源
const GetAssetsFile = (url: string) => {
    return new URL(`../assets/${url}`, import.meta.url).href;
};
// 使用
<n-avatar
    class="meAvatar"
    round
    size="large"
    :src="GetAssetsFile('me.jpeg')"
/>

vite可能出现本地读取正常,打包后,或者放到服务器上后读取不到图片资源的问题,可以用这个方法来解决。

后记

项目代码也是随手一写,存有很多瑕疵,大家凑合着看,希望可以帮助到大家。也希望大家多去项目地址看看,帮助我做出一些优化和改进。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容