当前位置: 首页 > news >正文

图片裁剪与上传处理方案 —— 基于阿里云 OSS 处理用户资料

目录

01: 通用组件:input 构建方案分析

02: 通用组件:input 构建方案

03: 构建用户资料基础样式 

04: 用户基本资料修改方案

05: 处理不保存时的同步问题 

06: 头像修改方案流程分析

07: 通用组件:Dialog 构建方案分析

08: 通用组件:Dialog 构建方案

09: 应用 Dialog 展示头像

10: 头像裁剪构建方案 

11. 阿里云 OSS 与腾讯云 COS 对象存储方案分析 

腾讯云 COS

COS SDK(COS 的包) 

阿里云 OSS

OSS 基础概念

创建存储桶(Bucket)

使用 STS 临时访问凭证访问 OSS 

上传图片到 Bucket 的流程分析

配置 CORS 跨域处理

12. 使用临时凭证,上传裁剪图片到阿里云 OSS

13. 完成头像更新操作

14. 登录鉴权解决方案

15: 总结


 

01: 通用组件:input 构建方案分析

 

期望通用组件 input 至少满足 4 个功能:

        1. 支持单行文本输入 

        2. 支持多行文本输入

        3. 通过 v-model 实现双向数据绑定

        4. 支持最大文本输入

根据以上功能点,可判断出 input 组件要有 3 个 prop:

        1. v-model

        2. type:单行 or 多行

        3. max:支持最大字符数

02: 通用组件:input 构建方案

- src/libs
- - input
- - - index.vue
// src/libs/input/index.vue<template><div class="relative"><inputv-if="type === TYPE_TEXT"class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"type="text"v-model="text":maxlength="max"/><textareav-if="type === TYPE_TEXTAREA"v-model="text":maxlength="max"rows="5"class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"></textarea><spanv-if="max"class="absolute right-1 bottom-0.5 text-zinc-400 text-xs":class="{ 'text-red-700': currentNumber === parseInt(max) }">{{ currentNumber }} / {{ max }}</span></div>
</template><script>
const TYPE_TEXT = 'text'
const TYPE_TEXTAREA = 'textarea'
</script><script setup>
import { useVModel } from '@vueuse/core'
import { computed } from 'vue'const props = defineProps({modelValue: {required: true,type: String},type: {type: String,default: TYPE_TEXT,validator(value) {const arr = [TYPE_TEXT, TYPE_TEXTAREA]const result = arr.includes(value)if (!result) {throw new Error(`type 的值必须在可选范围内 [${arr.join('、')}]`)}return result}},max: {type: [String, Number]}
})// 事件声明
defineEmits(['update:modelValue'])// 输入的字符
const text = useVModel(props)// 输入的字符数
const currentNumber = computed(() => {return text.value?.length
})
</script><style lang="scss" scoped></style>

03: 构建用户资料基础样式 

tailwindcss 先构建移动端,再构建 PC 端更方便。

- src/views
- - profile
- - - index.vue
// 路由信息{path: '/profile',name: 'profile',component: () => import('@/views/profile/index.vue'),// 标记当前的页面只有用户登录之后才可以进入meta: {user: true}
},
// src/views/profile/index.vue<template><divclass="h-full bg-zinc-200 dark:bg-zinc-800 duration-400 overflow-auto xl:pt-1"><divclass="relative max-w-screen-lg mx-auto bg-white dark:bg-zinc-900 duration-400 xl:rounded-sm xl:border-zinc-200 xl:dark:border-zinc-600 xl:border-[1px] xl:px-4 xl:py-2"><!-- 移动端 navbar --><m-navbar sticky v-if="isMobileTerminal" :clickLeft="onNavbarLeftClick">个人资料</m-navbar><!-- pc 端 --><div v-else class="text-lg font-bold text-center mb-4 dark:text-zinc-300">个人资料</div><div class="h-full w-full px-1 pb-4 text-sm mt-2 xl:w-2/3 xl:pb-0"><!-- 头像 --><div class="py-1 xl:absolute xl:right-[16%] xl:text-center"><spanclass="w-8 inline-block mb-2 font-bold text-sm dark:text-zinc-300 xl:block xl:mx-auto">我的头像</span><!-- 头像部分 --><divclass="relative w-[80px] h-[80px] group xl:cursor-pointer xl:left-[50%] xl:translate-x-[-50%]"@click="onAvatarClick"><imgv-lazy:src="$store.getters.userInfo.avatar"alt=""class="rounded-[50%] w-full h-full xl:inline-block"/><divclass="absolute top-0 rounded-[50%] w-full h-full bg-[rgba(0,0,0,.4)] hidden xl:group-hover:block"><m-svg-iconname="change-header-image"class="w-2 h-2 m-auto mt-2"></m-svg-icon><divclass="text-xs text-white dark:text-zinc-300 scale-90 mt-0.5">点击更换头像</div></div></div><!-- 隐藏域 --><inputv-show="false"ref="inputFileTarget"type="file"accept=".png, .jpeg, .jpg, .gif"@change="onSelectImgHandler"/><p class="mt-1 text-zinc-500 dark:text-zinc-400 text-xs xl:w-10">支持 jpg、png、jpeg 格式大小 5M 以内的图片</p></div><!-- 用户名 --><div class="py-1 xl:flex xl:items-center xl:my-1"><span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">用户名</span><m-inputv-model="userInfo.nickname"class="w-full"type="text"max="20"></m-input></div><!-- 职位 --><div class="py-1 xl:flex xl:items-center xl:my-1"><span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">职位</span><m-inputv-model="userInfo.title"class="w-full"type="text"></m-input></div><!-- 公司 --><div class="py-1 xl:flex xl:items-center xl:my-1"><span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">公司</span><m-inputv-model="userInfo.company"class="w-full"type="text"></m-input></div><!-- 个人主页 --><div class="py-1 xl:flex xl:items-center xl:my-1"><span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">个人主页</span><m-inputv-model="userInfo.homePage"class="w-full"type="text"></m-input></div><!-- 个人介绍 --><div class="py-1 xl:flex xl:my-1"><span class="w-8 block mb-1 font-bold dark:text-zinc-300 xl:mb-0">个人介绍</span><m-inputv-model="userInfo.introduction"class="w-full"type="textarea"max="50"></m-input></div><!-- 保存修改 --><m-buttonclass="w-full mt-2 mb-4 dark:text-zinc-300 dark:bg-zinc-800 xl:w-[160px] xl:ml-[50%] xl:translate-x-[-50%]":loading="loading"@click="onChangeProfile">保存修改</m-button><!-- 移动端退出登录 --><m-buttonv-if="isMobileTerminal"class="w-full dark:text-zinc-300 dark:bg-zinc-800 xl:w-[160px] xl:ml-[50%] xl:translate-x-[-50%]"@click="onLogoutClick">退出登录</m-button></div></div><!-- PC 端 --><m-dialog v-if="!isMobileTerminal" v-model="isDialogVisible"><change-avatar-vue:blob="currentBolb"@close="isDialogVisible = false"></change-avatar-vue></m-dialog><!-- 移动端 --><m-popupv-else:class="{ 'h-screen': isDialogVisible }"v-model="isDialogVisible"><change-avatar-vue:blob="currentBolb"@close="isDialogVisible = false"></change-avatar-vue></m-popup></div>
</template><script>
export default {name: 'profile'
}
</script><script setup>
import { isMobileTerminal } from '@/utils/flexible'
import { putProfile } from '@/api/sys'
import { message, confirm } from '@/libs'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { ref, watch } from 'vue'
import changeAvatarVue from './components/change-avatar.vue'const store = useStore()
const router = useRouter()// 隐藏域
const inputFileTarget = ref(null)
// 头像 dialog 展示
const isDialogVisible = ref(false)
// 选中的图片
const currentBolb = ref('')
/*** 更换头像点击事件*/
const onAvatarClick = () => {inputFileTarget.value.click()
}/*** 头像选择之后的回调*/
const onSelectImgHandler = () => {// 获取选中的文件const imgFile = inputFileTarget.value.files[0]// 生成 blob 对象const blob = URL.createObjectURL(imgFile)// 获取选中的图片currentBolb.value = blob// 展示 DialogisDialogVisible.value = true
}/*** 监听 dialog 关闭*/
watch(isDialogVisible, (val) => {if (!val) {// 防止 change 不重复触发inputFileTarget.value.value = null}
})/*** 数据本地的双向同步,增加一个单层深拷贝*/
const userInfo = ref({ ...store.getters.userInfo })
// const changeStoreUserInfo = (key, value) => {
//   store.commit('user/setUserInfo', {
//     ...store.getters.userInfo,
//     [key]: value
//   })
// }/*** 修改个人信息*/
const loading = ref(false)
const onChangeProfile = async () => {loading.value = trueawait putProfile(userInfo.value)message('success', '用户信息修改成功')// 更新 vuexstore.commit('user/setUserInfo', userInfo.value)loading.value = false
}/*** 移动端后退处理*/
const onNavbarLeftClick = () => {// 配置跳转方式store.commit('app/changeRouterType', 'back')router.back()
}/*** 移动端:退出登录*/
const onLogoutClick = () => {confirm('确定要退出登录吗?').then(() => {store.dispatch('user/logout')})
}
</script><style lang="scss" scoped></style>

04: 用户基本资料修改方案

// 第一种方式:直接修改 vuex state 数据,违背 vue 理念。<m-input v-model="$store.getters.userInfo.nickname" />// 第二种方式:优化第一种方式。<m-input :modelValue="$store.getters.userInfo.nickname"@update:modelValue="changeStoreUserInfo('nickname', $event)"
/>const changeStoreUserInfo = (key, value) => {store.commit('user/setUserInfo', {...store.getters.userInfo,[key]: value})
}// 第三种方式:解决用户信息未提交,但提前缓存问题。推荐。<m-input v-model="userInfo.nickname" />/*** 数据本地的双向同步,增加一个单层深拷贝*/
const userInfo = ref({ ...store.getters.userInfo })/*** 修改个人信息*/
const onChangeProfile = async () => {// 发送修改请求 代码省略// 更新 vuexstore.commit('user/setUserInfo', userInfo.value)
}

05: 处理不保存时的同步问题 

讲解一下 v-model 拆解的问题。

给不理解的同学讲解一下:为什么不能用 v-model 直接绑定 vuex 中的数据?以及绑定之后会产生什么样的问题?

思路及代码在上一小节之中。

06: 头像修改方案流程分析

接下来我们需要处理头像修改的业务逻辑。

对于该功能而言,分为 PC 端和 移动端 两种情况,我们需要分别进行处理:

1. PC 端:

        1. 点击更换头像。

        2. 选择对应文件。

        3. 通过 Dialog 展示图片剪裁。

        4. 剪裁后图片上传。

        5. 功能完成。

2. 移动端:

        1. 点击更换头像。

        2. 选择对应文件。

        3. 通过 popup 展示图片剪裁。

        4. 剪裁后图片上传。

        5. 功能完成。

由此可以发现,两者之间需要通过不同的组件进行裁剪展示。

因此我们后续的开发流程为:

        1. 通用组件:Dialog。

        2.  处理图片剪裁。

        3. 处理剪裁后上传。

07: 通用组件:Dialog 构建方案分析

首先我们来处理 Dialog 通用组件。

对于 Dialog 通用组件而言,我们可以参考 confirm 组件的构建过程。

它们两个构建方案非常相似,唯二不同的地方是:

        1. Dialog 无需通过方法调用的形式展示。

        2. Dialog 的内容区可以渲染任意的内容。

排除这两点之后,其余与 confirm 完全相同。

08: 通用组件:Dialog 构建方案

- src/libs
- - dialog
- - - index.vue
<template><div><!-- 蒙版 --><transition name="fade"><divv-if="isVisable"@click="close"class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div></transition><!-- 内容 --><transition name="up"><divv-if="isVisable"class="max-w-[80%] max-h-[80%] overflow-auto fixed top-[10%] left-[50%] translate-x-[-50%] z-50 px-2 py-1.5 rounded-sm border dark:border-zinc-600 cursor-pointer bg-white dark:bg-zinc-800 xl:min-w-[35%]"><!-- 标题 --><divclass="text-lg font-bold text-zinc-900 dark:text-zinc-200 mb-2"v-if="title">{{ title }}</div><!-- 内容 --><div class="text-base text-zinc-900 dark:text-zinc-200 mb-2"><slot /></div><!-- 按钮 --><div class="flex justify-end" v-if="cancelHandler || confirmHandler"><m-button type="info" class="mr-2" @click="onCancelClick">{{cancelText}}</m-button><m-button type="primary" @click="onConfirmClick">{{confirmText}}</m-button></div></div></transition></div>
</template><script setup>
import { useVModel } from '@vueuse/core'const props = defineProps({// 控制开关modelValue: {type: Boolean,required: true},// 标题title: {type: String},// 取消按钮文本cancelText: {type: String,default: '取消'},// 确定按钮文本confirmText: {type: String,default: '确定'},// 取消按钮点击事件cancelHandler: {type: Function},// 确定按钮点击事件confirmHandler: {type: Function},// 关闭的回调close: {type: Function}
})defineEmits(['update:modelValue'])// 控制显示处理
const isVisable = useVModel(props)/*** 取消按钮点击事件*/
const onCancelClick = () => {if (props.cancelHandler) {props.cancelHandler()}close()
}/*** 确定按钮点击事件*/
const onConfirmClick = () => {if (props.confirmHandler) {props.confirmHandler()}close()
}const close = () => {isVisable.value = falseif (props.close) {props.close()}
}
</script><style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {transition: all 0.3s;
}.fade-enter-from,
.fade-leave-to {opacity: 0;
}.up-enter-active,
.up-leave-active {transition: all 0.3s;
}.up-enter-from,
.up-leave-to {opacity: 0;transform: translate3d(-50%, 100px, 0);
}
</style>

09: 应用 Dialog 展示头像

// src/views/profile/index.vue<template><img :src="currentBolb" />
</template><script setup>// 选中的图片
const currentBolb = ref('')/*** 头像选择之后的回调*/
const onSelectImgHandler = () => {// 获取选中的文件const imgFile = inputFileTarget.value.files[0]// 生成 blob 对象const blob = URL.createObjectURL(imgFile)// 获取选中的图片currentBolb.value = blob// 展示 DialogisDialogVisible.value = true
}/*** 当 input file 两次选择文件,是同一个的时候,change 的回调不会被再次触发。* 想要解决这个问题,只需要在每次选择的图片不再被使用之后,清空掉 input file 的 value。* 监听 dialog 关闭*/
watch(isDialogVisible, (val) => {if (!val) {// 防止 change 不重复触发inputFileTarget.value.value = null}
})
</script>
- src/views/profile
- - components
- - - change-avatar.vue
// src/views/profile/components/change-avatar.vue<template><div class="overflow-auto relative flex flex-col items-center"><m-svg-iconv-if="isMobileTerminal"name="close"class="w-3 h-3 p-0.5 m-1 ml-auto"fillClass="fill-zinc-900 dark:fill-zinc-200 "@click="close"></m-svg-icon><img class="" ref="imageTarget" :src="blob" /><m-buttonclass="mt-4 w-[80%] xl:w-1/2":loading="loading"@click="onConfirmClick">确定</m-button></div>
</template><script>
const EMITS_CLOSE = 'close'// 移动端配置对象
const mobileOptions = {// 将裁剪框限制在画布的大小viewMode: 1,// 移动画布,裁剪框不动dragMode: 'move',// 裁剪框固定纵横比:1:1aspectRatio: 1,// 裁剪框不可移动cropBoxMovable: false,// 不可调整裁剪框大小cropBoxResizable: false
}// PC 端配置对象
const pcOptions = {// 裁剪框固定纵横比:1:1aspectRatio: 1
}
</script><script setup>
import { isMobileTerminal } from '@/utils/flexible'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { ref, onMounted } from 'vue'
import { getOSSClient } from '@/utils/sts'
import { message } from '@/libs'
import { useStore } from 'vuex'
import { putProfile } from '@/api/sys'defineProps({blob: {type: String,required: true}
})const emits = defineEmits([EMITS_CLOSE])/*** 图片裁剪处理*/
const imageTarget = ref(null)
let cropper = null
onMounted(() => {/*** 接收两个参数:* 1. 需要裁剪的图片 DOM* 2. options 配置对象*/cropper = new Cropper(imageTarget.value,isMobileTerminal.value ? mobileOptions : pcOptions)
})/*** 确定按钮点击事件*/
const loading = ref(false)
const onConfirmClick = () => {loading.value = true// 获取裁剪后的图片cropper.getCroppedCanvas().toBlob((blob) => {// 裁剪后的 blob 地址// console.log(URL.createObjectURL(blob))putObjectToOSS(blob)})
}/*** 进行 OSS 上传*/
let ossClient = null
let store = useStore()
const putObjectToOSS = async (file) => {if (!ossClient) {ossClient = await getOSSClient()}try {// 因为当前凭证只具备 images 文件夹下的访问权限,所以图片需要上传到 images/xxx.xx 。否则你将得到一个 《AccessDeniedError: You have no right to access this object because of bucket acl.》 的错误const fileTypeArr = file.type.split('/')const fileName = `${store.getters.userInfo.username}/${Date.now()}.${fileTypeArr[fileTypeArr.length - 1]}`// 文件存放路径,文件const res = await ossClient.put(`images/${fileName}`, file)// 通知服务器onChangeProfile(res.url)} catch (e) {message('error', e)}
}/*** 上传新头像到服务器*/
const onChangeProfile = async (avatar) => {// 更新本地数据store.commit('user/setUserInfo', {...store.getters.userInfo,avatar})// 更新服务器数据await putProfile(store.getters.userInfo)// 通知用户message('success', '用户头像修改成功')// 关闭 loadingloading.value = false// 关闭 dialogclose()
}/*** 关闭事件*/
const close = () => {emits(EMITS_CLOSE)
}
</script><style lang="scss" scoped></style>

10: 头像裁剪构建方案 

        接下来我们需要在 src/views/profile/components/change-avatar.vue 中处理对应的图片裁剪功能。 

        想要处理图片裁剪,我们需要使用到 cropperjs,它是一个 Javascript 的库,同时支持 PC 端 和 移动端。

        目前 cropperjs 的最新发布版本为 1.6.2,V2 级别的版本还是 RC 阶段,所以我们还是使用它的 V1 版本。

1. 安装 cropperjs

npm install cropperjs@1.5.12 --save

2. 在 src/views/profile/components/change-acatar.vue 中进行导入

import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'

3. 使用 new Cropper 进行初始化,区分 PC端 和 移动端:所有配置项

// 移动端配置对象
const mobileOptions = {// 将裁剪框限制在画布的大小viewMode: 1,// 移动画布,裁剪框不动dragMode: 'move',// 裁剪框固定纵横比:1:1aspectRatio: 1,// 裁剪框不可移动cropBoxMovable: false,// 不可调整裁剪框大小cropBoxResizable: false
}// PC 端配置对象
const pcOptions = {// 裁剪框固定纵横比:1:1aspectRatio: 1
}

4. 

/*** 图片裁剪处理*/
const imageTarget = ref(null)
let cropper = null
onMounted(() => {/*** 接收两个参数:* 1. 需要裁剪的图片 DOM* 2. options 配置对象*/cropper = new Cropper(imageTarget.value,isMobileTerminal.value ? mobileOptions : pcOptions)
})

5.

/*** 确定按钮点击事件*/
const loading = ref(false)
const onConfirmClick = () => {loading.value = true// 获取裁剪后的图片cropper.getCroppedCanvas().toBlob((blob) => {// 裁剪后的 blob 地址// console.log(URL.createObjectURL(blob))putObjectToOSS(blob)})
}

11. 阿里云 OSS 与腾讯云 COS 对象存储方案分析 

当图片裁剪处理完成之后,接下来我们就可以处理裁剪之后的图片上传了。

        通常情况下,在企业开发中,图片或文件的管理都会通过 对象存储 的方案进行。目前国内常见的对象存储云方案主要有两个平台: 

        1. 阿里云 OSS

        2. 腾讯云 COS

腾讯云 COS

腾讯云 COS 目前提供了 实名认证赠送 6个月 对象存储的服务,点击跳转

 

        这个服务对大家而言是非常好的一个练习服务,大家可以直接使用该服务来实现 裁剪图片上传到腾讯云。

以下为腾讯云 COS 使用流程:

        1. 注册 腾讯云 账号,并完成 实名认证。 

        2. 选择免费产品。

        3. 选择腾讯云 COS。

        4. 点击立即开通。

        5. 创建存储桶。

        6. 点击创建。

        7. 选择 公有读、私有写

        8. 一路下一步,创建即可。

此时你可以得到对应的存储桶,所有的文件都会被上传到该存储桶之中。

接下来就可以按照以下步骤进行图片上传:

COS SDK(COS 的包) 

对应文档地址 

1. 下载依赖包:

npm i cos-js-sdk-v5 --save

2. 构建 cos 实例:初始化 cos 对象参数 

名称描述
SecretId开发者拥有的项目身份识别 ID,用以身份认证,可在 API 密钥管理 页面获取
SecretKey开发者拥有的项目身份密钥,可在 API 密钥管理 页面获取
import COS from 'cos-js-sdk-v5'const cos = new COS({SecretId: '',SecretKey: ''
})

3. 执行上传操作

cos.putObject({Bucket: '',  // 填入您自己的存储痛,必须字段Region: '',  // 存储桶所在地域,例如 ap-beijing,必须字段Key: params.file.name,  // 存储在桶里的对象键(例如 1.jpg a/b/test.txt),必须字段StorageClass: 'STANDARD',Body: ,  // 上传文件对象onProgress: function (progressData) {console.log(JSON.stringify(progressData)}
}, function (err, data) {// 上传成功返回的数据,data.location 为图片的地址console.log(err || data)
})

4. 图片上传成功,在存储桶中即可查看上传的图片。 

阿里云 OSS

目前国内企业,使用最多的云服务为 阿里云,所以说咱们文章将会以 阿里云 为例进行开发。

阿里云的 OSS 服务,有新人三个月的免费试用。

我们将使用 OSS 进行图片的上传。

阿里云 OSS 使用流程:

        1. 注册登录 阿里云服务。

        2. 找到 阿里云 OSS 对象存储服务。

        3. 点击 立即开通。

        4. 选择 立即开通。

        5. 勾选服务,点击立即开通。

        6. 提示开通成功之后,可以进行两个操作。

                1. 点击 管理控制台 进入 OSS 控制台。

                2. 点击 对象存储新手入门 查看文档。

OSS 基础概念

OSS 中包含了很多的基础概念,可以点击 这里 直接进行访问。

需要大家了解的基础概念有:

        1. Bucket:存储空间。

        2. Object:存储文件。

创建存储桶(Bucket)

控制台 左侧点击 Bucket 列表,然后点击 创建 Bucket(读写权限为 私有

使用 STS 临时访问凭证访问 OSS 

在 Bucket 构建完成之后,接下来我们就需要去处理 访问凭证。 

注意:构建访问凭证,会涉及到服务端的配置。在实际开发中,不需要 前端工程师来处理访问凭证相关的内容。

具体的构建流程可以点击 这里 进行查看。

名词解释:

        RAM(Resource Access Management)

        STS(Security Token Service)

        ARN是指云服务所定义的资源(Aliyun Resource Name)

上传图片到 Bucket 的流程分析

1. 想要上传文件到 Bucket,我们需要使用 ali-sdk ali-oss。

2. 利用 ali-oss 生成 OSS 对象。

3. 在生成 OSS 对象时,需要传递 文件上传凭证

        1. accessKeyId。

        2. accessKeySecret。

        3. stsToken。

4. 需要通过接口 /user/sts 获取 文件上传凭证

整体的文件上传流程为:

        1. 通过接口 /user/sts 获取 文件上传凭证

        2. 通过 npm i ali-oss 安装依赖包。

        3. 使用凭证中的数据构建 OSS 对象。点击这里查看文档。

配置 CORS 跨域处理

当我们尝试使用 put 方法上传文件时,会得到一个跨域的错误。

想要处理这个问题,则需要 配置跨域规则。点击这里查看配置方案。

12. 使用临时凭证,上传裁剪图片到阿里云 OSS

本小节我们将讲解如何将裁剪后的图片上传到阿里云 OSS。

具体步骤如下:

        1. 安装 ali-oss 依赖。

        2. 通过接口获取临时访问凭证,生成 OSS 实例。

        3. 利用 ossClient.put 方法,完成对应上传。

接下来我们一步一步去做:

1. 安装 ali-oss 依赖。

npm i --save ali-oss@6.17.0

2. 创建 src/utils/sts.js 模块,用来生成 OSS 实例。

import OSS from 'ali-oss'
import { REGION, BUCKET } from '@/constants'
import { getSts } from '@/api/sys'export const getOSSClient = async () => {const res = await getSts()return new OSS({// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。region: REGION,// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。accessKeyId: res.Credentials.AccessKeyId,accessKeySecret: res.Credentials.AccessKeySecret,// 从STS服务获取的安全令牌(SecurityToken)。stsToken: res.Credentials.SecurityToken,// 填写Bucket名称。bucket: BUCKET,// 刷新 token,在 token 过期后自动调用(但是并不生效,可能会在后续的版本中修复)refreshSTSToken: async () => {// 向您搭建的STS服务获取临时访问凭证。const res = await getSts()return {accessKeyId: res.Credentials.AccessKeyId,accessKeySecret: res.Credentials.AccessKeySecret,stsToken: res.Credentials.SecurityToken}},// 刷新临时访问凭证的时间间隔,单位为毫秒。refreshSTSTokenInterval: 5 * 1000})
}

3. 在 constants 中定义 REGION,BUCKET

// STS 上传数据
export const REGION = 'oss-cn-beijing'
export const BUCKET = 'imooc-front'

4. 

/*** 进行 OSS 上传*/
let ossClient = null
let store = useStore()
const putObjectToOSS = async (file) => {if (!ossClient) {ossClient = await getOSSClient()}try {// 因为当前凭证只具备 images 文件夹下的访问权限,所以图片需要上传到 images/xxx.xx 。否则你将得到一个 《AccessDeniedError: You have no right to access this object because of bucket acl.》 的错误const fileTypeArr = file.type.split('/')const fileName = `${store.getters.userInfo.username}/${Date.now()}.${fileTypeArr[fileTypeArr.length - 1]}`// 文件存放路径,文件const res = await ossClient.put(`images/${fileName}`, file)// 通知服务器onChangeProfile(res.url)} catch (e) {message('error', e)}
}/*** 上传新头像到服务器*/
const onChangeProfile = async (avatar) => {// 更新本地数据store.commit('user/setUserInfo', {...store.getters.userInfo,avatar})// 更新服务器数据await putProfile(store.getters.userInfo)// 通知用户message('success', '用户头像修改成功')// 关闭 loadingloading.value = false// 关闭 dialogclose()
}/*** 关闭事件*/
const close = () => {emits(EMITS_CLOSE)
}

13. 完成头像更新操作

        为了代码连贯性,故代码写在上一小节中。

14. 登录鉴权解决方案

// src/permission.jsimport router from '@/router'
import store from '@/store'
import { message } from '@/libs'/*** 处理需登录页面的访问权限*/
router.beforeEach((to, from) => {// 无需登录的页面访问if (!to.meta.user) {return}// 已登录,可进入if (store.getters.token) {return true}// 未登录,警告然后返回首页message('warn', '登录失效,请重新登录!')return '/'
})

15: 总结

在本篇文章中,我们接触到了两个新的通用组件:

        1. input

        2. dialog

        除此之外,我们还接触到了头像裁剪、OSS、COS 的概念。这些概念可能对有些同学而言会比较新奇。大家也可以自己申请一个对应的 OSS 或者 COS 的账号(推荐 COS)。走一遍完整的流程,这样大家会对这个操作更加的熟悉。 

相关文章:

图片裁剪与上传处理方案 —— 基于阿里云 OSS 处理用户资料

目录 01: 通用组件&#xff1a;input 构建方案分析 02: 通用组件&#xff1a;input 构建方案 03: 构建用户资料基础样式 04: 用户基本资料修改方案 05: 处理不保存时的同步问题 06: 头像修改方案流程分析 07: 通用组件&#xff1a;Dialog 构建方案分析 08: 通用组件&…...

迷你主机Esxi 6.7挂载新硬盘

背景 硬件&#xff1a;零刻SER Pro 6 系统&#xff1a;vmware Exsi 6.7.0 Update 3 现有的硬盘槽位占满了&#xff0c;但空间不够用&#xff0c;想要通过USB外接移动硬盘来进行扩容。使用了一块250G的硬盘做测试。 步骤 TL;DR # 停止usbarbitrator服务 /etc/init.d/usbarbi…...

解决VSCode右键没有Open In Default Browser问题

在VSCode进行Web小程序测试时&#xff0c;我们在新建的HTML文件中输入 !会自动生成页面代码骨架&#xff0c;写入内容后&#xff0c;我们想要右键在浏览器中预览。发现右键没有“Open In Default Browser”选项。原因是没有安装插件。 下面是解决方案&#xff1a;首先在VSCode找…...

httpsok-v1.12.0支持LB证书自动部署

&#x1f525;httpsok-v1.12.0支持LB证书自动部署 介绍 httpsok 是一个便捷的 HTTPS 证书自动续签工具&#xff0c;基于全新的设计理念&#xff0c;专为 Nginx 、OpenResty 服务器设计。已服务众多中小企业&#xff0c;稳定、安全、可靠。 一行命令&#xff0c;一分钟轻松搞…...

基于Pytorch框架的深度学习EfficientNetV2神经网络中草药识别分类系统源码

第一步&#xff1a;准备数据 5种中草药数据&#xff1a;self.class_indict ["百合", "党参", "山魈", "枸杞", "槐花", "金银花"] &#xff0c;总共有900张图片&#xff0c;每个文件夹单独放一种数据 第二步&a…...

网络协议。

一、流程案例 接下来揭秘我要说的大事情&#xff0c;“双十一”。这和我们要讲的网络协议有什么关系呢&#xff1f; 在经济学领域&#xff0c;有个伦纳德里德&#xff08;Leonard E. Read&#xff09;创作的《铅笔的故事》。这个故事通过一个铅笔的诞生过程&#xff0c;来讲述…...

Excel单元格格式无法修改的原因与解决方法

Excel单元格格式无法更改可能由多种原因造成。以下是一些可能的原因及相应的解决方法&#xff1a; 单元格或工作表被保护&#xff1a; 如果单元格或工作表被设置为只读或保护状态&#xff0c;您将无法更改其中的格式。解决方法&#xff1a;取消单元格或工作表的保护。在Excel中…...

CasaOS玩客云安装全平台高速下载器Gopeed并实现远程访问

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

JAVA学习-练习试用Java实现“最长回文子串”

问题&#xff1a; 给定一个字符串 s&#xff0c;找到 s 中最长的回文子串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab" 解释&#xff1a;"aba" 同样是符合题意的答案。 示例 2&#xff1a; 输入&#xff1a;s …...

深入探索Qt框架系列之信号槽原理(三)

前面两篇分别介绍了QObject::connect和QMetaObject::Connection&#xff0c;那么信号槽机制的基础已经介绍完了&#xff0c;本文将介绍信号槽机制是如何从信号到槽的&#xff0c;以及多线程下是如何工作的。 信号槽机制源码解析 1. 信号的触发 以该系列的第一篇文章中的示例举…...

npm镜像源管理、nvm安装多版本node异常处理

查看当前使用的镜像源 npm config get registry --locationglobal 设置使用官方源 npm config set registry https://registry.npmjs.org/ --locationglobal 设置淘宝镜像源 npm config set registry https://registry.npm.taobao.org/ --locationglobal 需要更改淘宝镜像源地址…...

异步编程的魔力:如何显著提升系统性能

异步编程的魔力:如何显著提升系统性能 今天我们来聊聊一个对开发者非常重要的话题——异步编程。异步编程是提升系统性能的一种强大手段,尤其在需要高吞吐量和低时延的场景中,异步设计能够显著减少线程等待时间,从而提升整体性能。 异步设计如何提升系统性能? 我们通过…...

优选算法一:双指针算法与练习(移动0)

目录 双指针算法讲解 移动零 双指针算法讲解 常见的双指针有两种形式&#xff0c;一种是对撞指针&#xff0c;一种是快慢指针。 对撞指针&#xff1a;一般用于顺序结构中&#xff0c;也称左右指针。 对撞指针从两端向中间移动。一个指针从最左端开始&#xff0c;另一个从最…...

数据结构第二篇【关于java线性表(顺序表)的基本操作】

【关于java线性表&#xff08;顺序表&#xff09;的基本操作】 线性表是什么&#xff1f;&#x1f435;&#x1f412;&#x1f98d;顺序表的定义&#x1f9a7;&#x1f436;&#x1f435;创建顺序表新增元素,默认在数组最后新增在 pos 位置新增元素判定是否包含某个元素查找某个…...

人工智能和大模型的区别

人工智能&#xff08;AI&#xff09;和大模型是两个相关但有区别的概念。理解它们之间的区别有助于更好地掌握现代科技的发展动态。 人工智能&#xff08;AI&#xff09; 人工智能&#xff08;Artificial Intelligence, AI&#xff09;是一个广义的概念&#xff0c;指的是通过…...

k8s处于pending状态的原因有哪些

k8s处于pending状态的原因 资源不足&#xff1a;集群中的资源&#xff08;如CPU、内存&#xff09;不足以满足Pod所需的资源请求&#xff0c;导致Pod无法调度。 调度器问题&#xff1a;调度器无法为Pod找到合适的节点进行调度&#xff0c;可能是由于节点资源不足或调度策略配置…...

【C++】入门(一):命名空间、缺省参数、函数重载

目录 一、关键字 二、命名空间 问题引入(问题代码)&#xff1a; 域的问题 1.::域作用限定符 的 用法&#xff1a; 2.域的分类 3.编译器的搜索原则 命名空间的定义 命名空间的使用 举个&#x1f330;栗子&#xff1a; 1.作用域限定符指定命名空间名称 2. using 引入…...

深入分析 Android Activity (四)

文章目录 深入分析 Android Activity (四)1. Activity 的生命周期详解1.1 onCreate1.2 onStart1.3 onResume1.4 onPause1.5 onStop1.6 onDestroy1.7 onRestart 2. Activity 状态的保存与恢复2.1 保存状态2.2 恢复状态 3. Activity 的启动优化3.1 延迟初始化3.2 使用 ViewStub3.…...

Java实现顺序表

Java顺序表 前言一、线性表介绍常见线性表总结图解 二、顺序表概念顺序表的分类顺序表的实现throw具体代码 三、顺序表会出现的问题 前言 推荐一个网站给想要了解或者学习人工智能知识的读者&#xff0c;这个网站里内容讲解通俗易懂且风趣幽默&#xff0c;对我帮助很大。我想与…...

刷题笔记1:如何科学的限制数字溢出问题

LCR 192. 把字符串转换成整数 (atoi) - 力扣&#xff08;LeetCode&#xff09; 我们以力扣的此题目为例&#xff0c;简述在诸如大数运算等问题中如何限制数字溢出问题。 先来直接看看自己的处理方式&#xff1a; class Solution { public:int myAtoi(string str) {int pcur0;…...

社区供稿丨GPT-4o 对实时互动与 RTC 的影响

以下文章来源于共识粉碎机 &#xff0c;作者AI芋圆子 前面的话&#xff1a; GPT-4o 发布当周&#xff0c;我们的社区伙伴「共识粉碎机」就主办了一场主题为「GPT-4o 对实时互动与 RTC 的影响」讨论会。涉及的话题包括&#xff1a; GPT-4o 如何降低延迟&#xff08;VAD 模块可…...

基于Linux的文件操作(socket操作)

基于Linux的文件操作&#xff08;socket操作&#xff09; 1. 文件描述符基本概念文件描述符的定义&#xff1a;标准文件描述符&#xff1a;文件描述符的分配&#xff1a; 2. 文件描述符操作打开文件读取文件中的数据 在linux中&#xff0c;socket也被认为是文件的一种&#xff…...

C++面试题记录(网络)

TCP与UDP区别 1. TCP面向连接&#xff0c;UDP无连接&#xff0c;所以UDP数据传输效率更高 2.UDP可以支持一对一、一对多、多对一、多对多通信&#xff0c;TCP只能一对一 3. TCP需要在端系统维护连接状态&#xff0c;包括缓存&#xff0c;序号&#xff0c;确认号&#xff0c;…...

YoloV8改进策略:卷积篇|基于PConv的二次创新|附结构图|性能和精度得到大幅度提高(独家原创)

摘要 在PConv的基础上做了二次创新,创新后的模型不仅在精度和速度上有了质的提升,还可以支持Stride为2的降采样。 改进方法简单高效,需要发论文的同学不要错过! 论文指导 PConv在论文中的描述 论文: 下面我们展示了可以通过利用特征图的冗余来进一步优化成本。如图3所…...

图论(从数据结构的三要素出发)

文章目录 逻辑结构物理结构邻接矩阵定义性能分析性质存在的问题 邻接表定义性能分析存在的问题 十字链表(有向图)定义性能分析 邻接多重表(无向图)定义性能分析 数据的操作图的基本操作图的遍历广度优先遍历&#xff08;BFS&#xff09;算法思想和实现性能分析深度优先最小生成…...

spark相关知识

1.Spark的特点 Spark的设计遵循“一个软件栈满足不同应用场景”的理念&#xff0c;逐渐形成了一套完整的生态系统&#xff0c;既能够提供内存计算框架&#xff0c;也可以支持SQL即席查询、实时流式计算、机器学习和图计算等。 运行速度快&#xff0c;易使用&#xff0c;强大的技…...

K8S认证|CKA题库+答案| 12. 查看Pod日志

目录 12、查看Pod日志 CKA v1.29.0模拟系统 下载试用 题目&#xff1a; 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、提取错误日志 3&#xff09;、验证提取结果 12、查看Pod日志 CKA v1.29.0模拟系统 下载试用 题目&#xff1a; 您必须在以下C…...

【Java SE】 String、StringBuff和StringBuilder

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. 字符串不可变性1.1 设计不可变1.2 修改字符串创建新对象1.3 为什么字符串不可变1.4 String类设计不可变的…...

产品经理-需求分析(三)

1. 需求分析 从业务的需要出发&#xff0c;确定业务目的和目标&#xff0c;将业务需求转为产品需求 1.1 业务需求 业务需求 业务动机 业务目标 就是最根本的动机和目标成果&#xff0c;通过这个需求解决特定的问题 1.2 产品需求 产品需求 解决方案 产品结构 产品流程…...

Linux 编译器gcc/g++使用

gcc/g同理 编译器运行过程 1. 预处理&#xff08;进行宏替换) gcc -E a.c -o a.i 预处理后还是c语言 -E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面 告诉gcc&#xff0c;从现在开始进行程序的翻译&#xff0c;将预处理工作做完停下 2. 编译&#x…...