Skip to content

vue2+github api+axios 接口上传github图片

约 3219 字大约 11 分钟

vue2gitaxios

2025-02-26

一、功能概述

可在前端实现通过接口把图片上传到对应 GitHub 仓库。

二、创建 Token

访问:https://github.com/settings/tokens ,点击 Generate new token 按钮,创建一个新 Token:

  1. 设置过期时间,可选择“不会过期”。
  2. 勾选 repo 所有权限。
  3. 点击生成按钮,获取生成的 Token。

三、代码实现

3.1 创建 github.js

import axios from 'axios'
import { Loading, Message } from 'element-ui'

// 设置 axios 请求超时时间为 10000 毫秒
axios.defaults.timeout = 10000

// 响应拦截器,处理响应数据
axios.interceptors.response.use(response => {
  return response
}, (err) => {
  // 返回状态码不为 200 时的错误处理,显示错误消息
  Message.error(err.toString())
  return Promise.reject(err)
})

// 仓库名称
const repo = 'zpfzpf123/images' 
// Token 前半部分
const cutToken = 'ghp_lUbD67XeagTCo' 
// Token 后半部分
const tailToken = 'kcXfLOOXA01XYysbz0hGN73' 

// 上传图片的异步函数
const uploader = async(content, files) => {
  // 启动全屏加载提示
  const loadingInstance = Loading.service({ fullscreen: true, text: '正在上传...' }) 
  try {
    // 构建图片上传的 API URL
    const imageUrl = 'https://api.github.com/repos/' + repo + '/contents/' + files + '.png'
    // 上传请求的主体数据
    const body = { message: 'upload', content }
    // 请求头信息,包含授权信息和接受的数据格式
    const headers = {
      Authorization: `token ${cutToken}${tailToken}`,
      Accept: 'application/vnd.github.v3+json'
    }
    // 发送 PUT 请求上传图片
    await axios.put(imageUrl, body, { headers })
    // 上传成功,关闭加载提示并显示成功消息,返回文件名
    loadingInstance.close()
    Message.success('上传 github 成功!!回显由于网络延迟,显示不出来或显示较缓慢属于正常现象')
    return files + '.png'
  } catch (error) {
    // 上传失败,关闭加载提示并显示错误消息,返回 null
    loadingInstance.close()
    Message.error('上传失败: ' + error.toString())
    return null
  }
}

export default {
  uploader
}

3.2 Vue 代码

3.2.1 模板部分

<template>
  <div id="btn-table">
    <!-- 顶部搜索表单 -->
    <el-card class="top">
      <div class="btn">
        <el-form ref="inquire" label-width="80px" :model="inquire" inline>
          <el-form-item label="文章标题" prop="name">
            <el-input v-model="inquire.name_like" />
          </el-form-item>
          <el-form-item label="文章类型" prop="type">
            <el-select v-model="inquire.type" placeholder="请选择文章类型">
              <el-option
                v-for="item of noteTypeList"
                :key="item.label"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item style="margin-left: 20px">
            <button class="pan-btn green-btn" @click.prevent="reset">重置</button>
          </el-form-item>
          <el-form-item
            v-if="environment === 'development'"
            style="margin-left: 20px"
          >
            <button class="pan-btn tiffany-btn" @click="addNote">
              新增文章
            </button>
          </el-form-item>
        </el-form>
      </div>
    </el-card>
    <!-- 文章列表表格 -->
    <el-card style="margin-top: 20px; width: 100%">
      <div class="table">
        <el-table
          :data="noteList"
          :column="column"
          height="64vh"
          :default-sort="{ prop: 'recommendationIndex', order: 'descending' }"
        >
          <el-table-column
            type="index"
            label="序号"
            align="center"
            width="50"
          />
          <el-table-column
            v-for="item in column"
            :key="item.label"
            :prop="item.prop"
            :label="item.label"
            align="center"
          />
          <el-table-column
            width="200px"
            label="推荐指数"
            sortable
            prop="recommendationIndex"
          >
            <template slot-scope="scope">
              <el-rate
                v-model="scope.row.recommendationIndex"
                disabled
                show-score
                text-color="#ff9900"
                :max="5"
                :min="1"
              />
            </template>
          </el-table-column>
          <el-table-column label="操作" align="center" width="300">
            <template slot-scope="scope">
              <el-button
                type="primary"
                @click="viewInfo(scope.$index, scope.row)"
              >预览
              </el-button>
              <el-button
                v-if="environment === 'development'"
                style="margin-left: 20px"
                type="warning"
                @click="editInfo(scope.$index, scope.row)"
              >修改
              </el-button>
              <el-popconfirm
                style="margin-left: 20px"
                title="确定要删除选择项吗?"
                @onConfirm="delInfo(scope.$index, scope.row)"
              >
                <el-button
                  v-if="environment === 'development'"
                  slot="reference"
                  type="danger"
                >删除文章
                </el-button>
              </el-popconfirm>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <!-- 新增/修改文章对话框 -->
    <el-dialog-com
      :visible="showAddNote"
      :title="status === 0 ? '新增文章' : '修改文章'"
      fullscreen
      @closeDialog="closeAddNote"
    >
      <template v-slot:content>
        <el-form ref="note" :model="note" label-width="120px" :rules="rules">
          <el-form-item label="文章标题" prop="name">
            <el-input v-model="note.name" />
          </el-form-item>
          <el-form-item label="文章类型" prop="type">
            <el-select v-model="note.type" placeholder="请选择文章类型">
              <el-option
                v-for="item of noteTypeList"
                :key="item.label"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item
            label="推荐指数"
            class="postInfo-container-item"
            prop="recommendationIndex"
          >
            <el-rate
              v-model="note.recommendationIndex"
              :max="5"
              :colors="['#99A9BF', '#F7BA2A', '#FF9900']"
              :low-threshold="1"
              :high-threshold="5"
              style="display: inline-block"
            />
          </el-form-item>
          <el-form-item label="文章内容" prop="content">
            <v-md-editor
              v-model="note.content"
              :disabled-menus="[]"
              :include-level="[1, 2, 3, 4]"
              height="400px"
              @upload-image="handleUploadImage"
            />
          </el-form-item>
        </el-form>
      </template>
      <template v-slot:footer>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </template>
    </el-dialog-com>
    <!-- 文章详情对话框 -->
    <el-dialog-com
      :visible="showDetail"
      title=""
      width="80vw"
      @closeDialog="closeDetail"
    >
      <template v-slot:content>
        <div class="detail">
          <v-md-editor
            v-if="showDetail"
            v-model="articleDetails"
            left-toolbar="fullscreen toc"
            right-toolbar=""
            :include-level="[1, 2, 3]"
            height="70vh"
            @copy-code-success="handleCopyCodeSuccess"
          />
        </div>
      </template>
    </el-dialog-com>
    <!-- 输入图片名称对话框 -->
    <el-dialog
      title="输入图片名称"
      :visible.sync="dialogVisible"
      width="30%"
      @close="decideOnAName"
      @open="focusInput"
    >
      <el-input
        ref="nameInput"
        v-model="imageName"
        placeholder="请输入图片名称"
        @keyup.enter.native="decideOnAName"
      />
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="decideOnAName">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

3.2.2 脚本部分

<script>
import Note from '@/api/note'
import elDialogCom from '@/components/Dialog/el-dialog-com.vue'
import db from '../../../static/db'
import github from '@/request/github'
import { Loading } from 'element-ui'

export default {
  name: 'StudyNote',
  components: {
    elDialogCom
  },
  data() {
    return {
      dialogVisible: false,
      timer: null,
      imageName: '',
      status: 0, // 0 新增,1 修改
      noteList: [],
      initList: [],
      showAddNote: false,
      showDetail: false,
      articleDetails: '',
      noteTypeList: [
        {
          label: 'vue',
          value: 'vue'
        },
        {
          label: 'js',
          value: 'js'
        },
        {
          label: 'css',
          value: 'css'
        },
        {
          label: 'uniapp',
          value: 'uniapp'
        },
        {
          label: 'ai',
          value: 'ai'
        },
        {
          label: 'component',
          value: 'component'
        },
        {
          label: '公司',
          value: '公司'
        },
        {
          label: 'git',
          value: 'git'
        }
      ],
      // 文章信息
      note: {
        name: '',
        type: '',
        recommendationIndex: 0,
        content: ''
      },
      // 查询条件
      inquire: {
        name_like: '',
        type: ''
      },
      // 表单验证规则
      rules: {
        name: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
        type: [
          { required: true, message: '请选择文章类型', trigger: 'change' }
        ]
      },
      // 表格列配置
      column: [
        {
          prop: 'name',
          label: '标题'
        },
        {
          prop: 'type',
          label: '类型'
        }
      ]
    }
  },
  computed: {
    // 计算查询条件
    condition() {
      return { name: this.inquire.name_like, type: this.inquire.type }
    },
    // 获取当前环境
    environment() {
      return process.env.NODE_ENV
    }
  },
  watch: {
    // 监听查询条件变化,触发查询文章列表
    condition: {
      handler(val) {
        this.queryTheArticle()
      }
    }
  },
  mounted() {
    // 组件挂载时初始化数据
    this.init()
  },
  methods: {
    // 输入图片名称对话框打开时聚焦输入框
    focusInput() {
      this.$nextTick(() => {
        this.$refs.nameInput.focus()
      })
    },
    // 处理图片上传
    async handleUploadImage(event, insertImage, files) {
      const reader = new FileReader()

      function getBase64(file) {
        return new Promise((resolve) => {
          reader.onload = function(event) {
            const fileContent = event.target.result
            resolve(fileContent.split(',')[1])
          }
          reader.readAsDataURL(file)
        })
      }

      const content = await getBase64(files[0]) 
      try {
        this.dialogVisible = true
        this.timer = setInterval(async() => {
          if (!this.dialogVisible) {
            clearInterval(this.timer)
            var path = await github.uploader(content, this.imageName)
            if (path) {
              // 插入图片到文章内容
              insertImage({
                url: `https://github.com/zpfzpf123/images/blob/master/${path}?raw=true`,
                desc: `${path}`,
                width: 'auto',
                height: 'auto'
              })
              this.imageName = ''
            }
          }
        }, 1000)
      } catch (e) {
        console.log(e,'e')
        this.$message.error(e.message)
        this.imageName = ''
        clearInterval(this.timer)
      }
    },
    // 初始化数据
    init() {
      if (this.environment === 'development') {
        Note.getNote()
          .then((list) => {
            this.noteList = []
            this.noteList.push(...list)
          })
      } else {
        this.noteList = []
        this.noteList.push(...db.note)
        this.initList = []
        this.initList.push(...db.note)
      }
    },
    // 新增文章
    addNote() {
      this.status = 0
      this.showAddNote = true
      this.note = Object.assign(
        {},
        {
          name: '',
          type: '',
          recommendationIndex: 0,
          content: '',
          id: ''
        }
      )
      this.$nextTick(() => {
        this.$refs.note.resetFields()
      })
    },
    // 查询文章列表
    queryTheArticle() {
      const condition = Object.fromEntries(
        Object.entries(this.inquire).filter(
          ([key, value]) => value != null && value !== ''
        )
      )
      if (this.environment === 'development') {
        Note.getNote(condition)
          .then((list) => {
            this.noteList = []
            this.noteList.push(...list)
          })
      } else {
        this.noteList = this.initList.filter((item) => {
          return (
            item.name.includes(this.inquire.name_like) &&
            item.type.includes(this.inquire.type)
          )
        })
      }
    },
    // 重置查询条件
    reset() {
      this.inquire = Object.assign(
        {},
        {
          name_like: '',
          type: ''
        }
      )
    },
    // 关闭新增/修改文章对话框
    closeAddNote() {
      this.showAddNote = false
    },
    // 提交文章表单
    submitForm() {
      this.$refs.note.validate((valid) => {
        if (!valid) {
          return false
        }
        if (!this.note.recommendationIndex) {
          this.$message.warning('请选择推荐指数')
          return false
        }
        if (!this.note.content) {
          this.$message.warning('请填写文章内容')
          return false
        }
        if (this.status === 0) {
          const id = this.noteList.length + 1
          Note.postNote({
            ...this.note,
            id
          }).then((_) => {
            this.$message.success('添加文章成功!')
            this.init()
            this.showAddNote = false
          })
        } else if (this.status === 1) {
          Note.putNote(this.note.id, this.note).then((res) => {
            this.$message.success('修改文章成功!')
            this.init()
            this.showAddNote = false
          })
        }
      })
    },
    // 关闭文章详情对话框
    closeDetail() {
      this.showDetail = false
    },
    // 查看文章详情
    viewInfo(index, val) {
      this.showDetail = true
      this.$nextTick(() => {
        this.articleDetails = val.content
        // 点击目录图标展开目录(假设 v-md-editor 有这样的图标和功能)
        document.querySelector('.v-md-icon-toc').click() 
      })
    },
    // 确定图片名称并关闭对话框
    decideOnAName() {
      this.dialogVisible = false
    },
    // 编辑文章
    editInfo(index, val) {
      this.status = 1
      this.showAddNote = true
      this.$nextTick(() => {
        this.$refs.note.resetFields()
        // 将当前文章数据赋值给编辑表单
        this.note = Object.assign(
          {},
          {
            name: val.name,
            type: val.type,
            recommendationIndex: val.recommendationIndex,
            content: val.content,
            id: val.id
          }
        )
      })
    },
    // 删除文章
    delInfo(index, val) {
      Note.delNote(val.id).then((res) => {
        this.$message.success('删除成功!')
      })
    },
    // 复制代码成功提示
    handleCopyCodeSuccess() {
      this.$message.success('复制成功!')
    }
  }
}
</script>

3.2.3 样式部分

<style scoped lang="scss">
#btn-table {
  //width: 100%;
  //height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  box-sizing: border-box;
  margin: 20px;

  .top {
    width: 100%;
    height: 10vh;
  }

  .btn {
    width: 100%;
    flex: 1;
    display: flex;
    column-gap: 20px;
    justify-items: center;
    align-items: center;
  }

  .table {
    width: 100%;
    flex: 6;

    ::v-deep .el-table {
      .el-table__body-wrapper {
        // 滚动条整体部分
        &::-webkit-scrollbar {
          width: 6px;
          height: 6px;
        }

        // 滚动条的轨道的两端按钮,允许通过点击微调小方块的位置。
        &::-webkit-scrollbar-button {
          display: none;
        }

        // 滚动条的轨道(里面装有Thumb)
        &::-webkit-scrollbar-track {
          background: transparent;
        }

        // 滚动条的轨道(里面装有Thumb)
        &::-webkit-scrollbar-track-piece {
          background-color: transparent;
        }

        // 滚动条里面的小方块,能向上向下移动(或往左往右移动,取决于是垂直滚动条还是水平滚动条)
        &::-webkit-scrollbar-thumb {
          background: #42b983;
          cursor: pointer;
          border-radius: 4px;
        }

        // 边角,即两个滚动条的交汇处
        &::-webkit-scrollbar-corner {
          display: none;
        }

        // 两个滚动条的交汇处上用于通过拖动调整元素大小的小控件
        &::-webkit-resizer {
          display: none;
        }
      }
    }
  }
}

.el-select {
  width: 100%;
}

.detail {
  // 以下样式用于调整 v-md-editor 在详情页的显示效果
  // 隐藏 v-md-editor 的一些默认样式
  ::v-deep .v-md-textarea-editor {
    display: none;
  }

  ::v-deep .v-md-editor__editor-wrapper {
    display: none;
  }
  // 为滚动区域设置滚动条样式
  ::v-deep .scrollbar{
    overflow-y: auto;
  }
  ::v-deep .v-md-editor__main{
    overflow-y: auto;
  }
  ::v-deep .scrollbar__thumb{
    background: #42b983;
  }
}

.top {
  ::v-deep .el-card__body {
    padding: 0 !important;
    height: 100%;

    .btn {
      height: 100%;
      display: flex;
      align-items: center;

      .el-form {
        height: 100%;
        padding: 10px;

        .el-form-item {
          margin-bottom: 10px;
        }
      }
    }
  }
}
</style>

四、属性说明

所属部分属性名类型描述
github.jsrepoString目标 GitHub 仓库名称,用于构建上传图片的 API URL
github.jscutTokenStringGitHub Token 的前半部分,用于授权请求
github.jstailTokenStringGitHub Token 的后半部分,与 cutToken 拼接完成授权信息
github.jsuploaderFunction用于上传图片到 GitHub 仓库的异步函数
Vue 组件dialogVisibleBoolean控制输入图片名称对话框的显示与隐藏
Vue 组件timerNumber定时器 ID,用于在图片上传时的定时操作
Vue 组件imageNameString输入的图片名称,用于上传图片时命名
Vue 组件statusNumber表示当前操作状态,0 为新增文章,1 为修改文章
Vue 组件noteListArray文章列表数据,用于展示文章列表
Vue 组件initListArray初始文章列表数据,用于开发环境与生产环境的数据处理
Vue 组件showAddNoteBoolean控制新增/修改文章对话框的显示与隐藏
Vue 组件showDetailBoolean控制文章详情对话框的显示与隐藏
Vue 组件articleDetailsString文章详情内容,用于展示文章详情
Vue 组件noteTypeListArray文章类型列表数据,用于选择文章类型
Vue 组件noteObject当前操作的文章对象,包含文章的各项信息
Vue 组件inquireObject查询文章列表的条件对象,包含文章标题和类型的查询条件
Vue 组件rulesObject文章表单的验证规则对象
Vue 组件columnArray文章列表表格的列配置对象数组
Vue 组件conditionComputed计算属性,返回当前查询条件对象
Vue 组件environmentComputed计算属性,返回当前运行环境(development 或 production)

五、方法说明

所属部分方法名参数返回值描述
github.jsuploadercontent(图片内容,base64 格式字符串), files(图片名称)Promise上传图片到 GitHub 仓库的异步函数,成功返回图片文件名,失败返回 null
Vue 组件focusInput输入图片名称对话框打开时,聚焦输入框
Vue 组件handleUploadImageevent(事件对象), insertImage(插入图片的函数), files(文件对象数组)处理图片上传操作,将图片转换为 base64 格式后调用 github.uploader 上传,并插入到文章内容中
Vue 组件init组件挂载时初始化数据,根据环境从接口或本地获取文章列表数据
Vue 组件addNote新增文章操作,初始化文章对象并显示新增文章对话框
Vue 组件queryTheArticle根据查询条件查询文章列表数据,开发环境从接口获取,生产环境从本地数据筛选
Vue 组件reset重置查询条件对象,清空文章标题和类型的查询条件
Vue 组件closeAddNote关闭新增/修改文章对话框
Vue 组件submitForm提交文章表单,根据当前状态(新增或修改)调用相应接口保存文章数据
Vue 组件closeDetail关闭文章详情对话框
Vue 组件viewInfoindex(文章列表索引), val(文章对象)查看文章详情,显示文章详情对话框并填充文章内容
Vue 组件decideOnAName确定图片名称并关闭输入图片名称对话框
Vue 组件editInfoindex(文章列表索引), val(文章对象)编辑文章,设置当前操作状态为修改,填充文章数据到编辑表单并显示对话框
Vue 组件delInfoindex(文章列表索引), val(文章对象)删除文章,调用接口删除指定文章并提示删除成功
Vue 组件handleCopyCodeSuccess复制代码成功时的提示方法,显示复制成功消息

六、注意事项

如果接口总是上传失败或者图片不回显,需要修改一下本地host配置,如下:

# GitHub Start 
20.205.243.166      github.com
185.199.108.133     raw.githubusercontent.com
# GitHub End

这样配置的目的是将 GitHub 相关域名正确解析到对应的 IP 地址,解决可能出现的网络连接问题,确保图片上传功能正常运行以及图片能正确回显。