Vue3 + cropper 实现裁剪头像的功能(裁剪效果可实时预览、预览图可下载、预览图可上传到SpringBoot后端、附完整的示例代码和源代码)

news2024/9/20 20:21:02

文章目录

  • 0. 前言
  • 1. 裁剪效果(可实时预览)
  • 2. 安装 cropper
  • 3. 引入 Vue Cropper
    • 3.1 局部引入(推荐使用)
    • 3.2 全局引入
  • 4. 在代码中使用
    • 4.1 template部分
    • 4.2 script部分
  • 5. 注意事项
  • 6. SpringBoot 后端接收图片
    • 6.1 UserController.java
    • 6.2 Result.java
  • 7. 完整的示例代码
    • 7.1 Homeview.vue
    • 7.2 request.js
    • 7.3 main.js
    • 7.4 vite.config.js
  • 8. 完整的源代码

0. 前言

裁剪头像的需求十分常见,主要目的是为了统一用户头像的尺寸,避免因为用户上传的图片尺寸大小不一致导致页面布局出现问题

高效实现需求的方法,就是避免重复造轮子,在这里推荐使用 cropper 实现头像裁剪功能 (原因是 cropper 功能强大、上手简单、文档详细)


cropper 的Gitee地址:vue-cropper

cropper Vue3在线示例:cropper Vue3在线示例

1. 裁剪效果(可实时预览)

在这里插入图片描述

2. 安装 cropper

# npm 安装
npm install vue-cropper@next
# yarn 安装
yarn add vue-cropper@next

3. 引入 Vue Cropper

3.1 局部引入(推荐使用)

哪个组件需要使用 Vue Cropper,就在哪个组件导入

import 'vue-cropper/dist/index.css'
import { VueCropper }  from 'vue-cropper'

3.2 全局引入

main.js 文件

import VueCropper from 'vue-cropper'
import 'vue-cropper/dist/index.css'

const app = createApp(App)
app.use(VueCropper)
app.mount('#app')

4. 在代码中使用

注意事项:

要为 <vue-cropper></vue-cropper> 组件设置宽和高,并用一个外层容器包裹 <vue-cropper></vue-cropper> 组件

4.1 template部分

<vue-cropper
    class="crop"
    ref="cropper"
    :autoCrop="option.autoCrop"
    :autoCropHeight="option.autoCropHeight"
    :autoCropWidth="option.autoCropWidth"
    :canMove="option.canMove"
    :canScale="option.canScale"
    :centerBox="option.centerBox"
    :fixed="option.fixed"
    :fixedBox="option.fixedBox"
    :fixedNumber="option.fixedNumber"
    :img="option.img"
    :info-true="option.infoTrue"
    :mode="option.mode"
    :origin="option.origin"
    :outputSize="option.outputSize"
    :outputType="option.outputType"
    @realTime="realTime"
></vue-cropper>

4.2 script部分

const option = ref({
  autoCrop: true, // 是否默认生成截图框
  autoCropHeight: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25
  autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25
  canMove: true, // 上传图片是否可以移动
  canScale: true, // 图片是否允许滚轮缩放
  centerBox: true, // 截图框是否被限制在图片里面
  fixed: true, // 是否固定截图框的宽高比例
  fixedBox: true, // 是否固定截图框大小
  fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])
  img: 'https://img2.baidu.com/it/u=2339635883,2403687892&fm=253&fmt=auto&app=138&f=JPEG', // 裁剪图片的地址(可选值:url 地址, base64, blob)
  infoTrue: true, // infoTrue为 true 时显示预览图片的宽高信息,infoTrue为 false 时表示显示裁剪框的宽高信息
  mode: 'contain', // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)
  origin: false, // 上传的图片是否按照原始比例渲染
  outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)
  outputType: 'png', // 裁剪生成图片的格式(可选值:png, jpeg, webp)
})

// 实时预览
const realTime = (data) => {
  // console.log('realTime data =', data)
  previews.value = data
}

5. 注意事项

  1. cropper 对象的 getCropBlob 方法和 getCropData 方法都是异步方法
  2. 虽然 getCropBlob 获取的的 Blob 对象在控制台打印时只有 size 和 type 属性,但是仍然可以使用window.URL.createObjectURL(blob)来生成 url ,从 Java 的角度来说,相当于重写了 Blob 类的 toString 方法
  3. 前端用 formData 上传文件时, key 要与后端接口中 @RequestParam(“avatar”) 指定的参数名一致

在这里插入图片描述

6. SpringBoot 后端接收图片

后端环境:

  • JDK:17.0.7
  • SpringBoot:3.0.2

6.1 UserController.java

import cn.edu.scau.controller.vo.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/updateAvatar")
    public Result<Object> updateAvatar(@RequestParam("avatar") MultipartFile avatar) {
        System.err.println("文件名:" + avatar.getOriginalFilename());
        System.err.println("文件大小(KB):" + avatar.getSize() / 1024);

        try {
            // 拿到图片文件后,可以将图片上传到阿里云、腾讯云、minio等第三方存储服务,然后返回图片的访问地址
            // 这里直接保存到本地

            String fileName = UUID.randomUUID().toString();
            String suffix = Objects.requireNonNull(avatar.getOriginalFilename()).substring(avatar.getOriginalFilename().lastIndexOf("."));
            avatar.transferTo(new File("F:\\Blog\\crop-avatar\\" + fileName + suffix));
        } catch (IOException ioException) {
            throw new RuntimeException(ioException);
        }

        return Result.success();
    }

}

6.2 Result.java

import java.io.Serializable;

/**
 * 后端统一返回结果
 *
 * @param <T>
 */
public class Result<T> implements Serializable {

    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.code = 200;
        result.message = "success";
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<>();
        result.data = object;
        result.code = 200;
        result.message = "success";
        return result;
    }

    public static <T> Result<T> fail(String message) {
        Result<T> result = new Result<>();
        result.message = message;
        result.code = 500;
        return result;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Result{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }

}

7. 完整的示例代码

7.1 Homeview.vue

<template>
  <div class="wrapper">
    <div class="blank-line"></div>
    <div class="top">
      <p class="title">裁剪头像</p>
    </div>
    <div class="blank-line"></div>
    <div class="main">
      <div class="crop-container">
        <vue-cropper
            class="crop"
            ref="cropper"
            :autoCrop="option.autoCrop"
            :autoCropHeight="option.autoCropHeight"
            :autoCropWidth="option.autoCropWidth"
            :canMove="option.canMove"
            :canScale="option.canScale"
            :centerBox="option.centerBox"
            :fixed="option.fixed"
            :fixedBox="option.fixedBox"
            :fixedNumber="option.fixedNumber"
            :img="option.img"
            :info="option.info"
            :info-true="option.infoTrue"
            :mode="option.mode"
            :origin="option.origin"
            :outputSize="option.outputSize"
            :outputType="option.outputType"
            :rounded="true"
            @realTime="realTime"
        ></vue-cropper>

        <input
            id="input"
            ref="input"
            type="file"
            accept="image/png, image/jpeg, image/gif, image/jpg"
            @change="uploadAvatar($event)"
            v-show="false">

        <div class="action-buttons">
          <el-button :size="'default'" type="primary" @click="handleUploadAvatar">上传图片</el-button>
          <el-button :size="'default'" type="danger" plain :icon="ZoomIn" @click="changeScale(1)">
            放大(向上滚动鼠标滑轮)
          </el-button>
          <el-button :size="'default'" type="danger" plain :icon="ZoomOut" @click="changeScale(-1)">
            缩小(向下滚动鼠标滑轮)
          </el-button>
          <el-button :size="'default'" type="primary" @click="rotateLeft">向左旋转</el-button>
          <el-button :size="'default'" type="primary" @click="rotateRight">向右旋转</el-button>
          <el-button :size="'default'" type="primary" @click="downloadPreView">下载预览图</el-button>
          <el-button :size="'default'" type="primary" @click="updateAvatar">确定修改</el-button>
        </div>
      </div>

      <div class="preview-container">
        <div>
          <p class="preview-title">实时预览</p>
        </div>
        <div :style="getPreviewStyle">
          <div :style="previews.div">
            <img :src="previews.url" :style="previews.img" alt="" class="preview-img">
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import 'vue-cropper/dist/index.css'
import {VueCropper} from 'vue-cropper'
import {computed, ref} from 'vue'
import {ElMessage} from 'element-plus'
import {ZoomIn, ZoomOut} from '@element-plus/icons-vue'
import request from '@/util/request.js'

const previews = ref({})
const previewBlob = ref()
const previewBase64 = ref()

const cropper = ref()
const input = ref()
const option = ref({
  autoCrop: true, // 是否默认生成截图框
  autoCropHeight: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25
  autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25
  canMove: true, // 上传图片是否可以移动
  canScale: true, // 图片是否允许滚轮缩放
  centerBox: true, // 截图框是否被限制在图片里面
  fixed: true, // 是否固定截图框的宽高比例
  fixedBox: true, // 是否固定截图框大小
  fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])
  img: 'https://img1.baidu.com/it/u=3450282427,2041051230&fm=253', // 裁剪图片的地址(可选值:url 地址, base64, blob)
  info: false, // 是否显示裁剪框的宽高信息
  infoTrue: true, // infoTrue为 true 时裁剪框显示的是预览图片的宽高信息,infoTrue为 false 时裁剪框显示的是裁剪框的宽高信息
  mode: 'contain', // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)
  origin: false, // 上传的图片是否按照原始比例渲染
  outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)
  outputType: 'png', // 裁剪生成图片的格式(可选值:png, jpeg, webp)
})

// 实时预览
const realTime = (data) => {
  // console.log('realTime data =', data)
  previews.value = data
}

const downloadPreView = () => {
  let aLink = document.createElement('a')
  aLink.download = '预览图.png'

  cropper.value.getCropBlob((blob) => {
    aLink.href = window.URL.createObjectURL(blob)
    aLink.click()
  })
}

const uploadAvatar = (event) => {
  let file = event.target.files[0]
  // console.log('uploadAvatar file=', file)

  if (!/\.(gif|jpg|jpeg|png|bmp)$/i.test(event.target.value)) {
    ElMessage.error('图片类型必须是.gif、jpeg、jpg、png、bmp中的一种')
    return false
  }

  let fileReader = new FileReader()
  fileReader.onload = (event) => {
    let data
    if (typeof event.target.result === 'object') {
      // 把 Array Buffer 转化为 blob
      data = window.URL.createObjectURL(new Blob([event.target.result]))
    } else {
      // 如果是 base64 ,不需要转换
      data = event.target.result
    }
    option.value.img = data
  }

  // 转化为base64
  // fileReader.readAsDataURL(file)

  // 转化为blob
  fileReader.readAsArrayBuffer(file)
}

const handleUploadAvatar = () => {
  input.value.click()
}

const getPreviewStyle = computed(() => {
  return {
    'width': previews.value.w + 'px',
    'height': previews.value.h + 'px',
    'overflow': 'hidden',
    // 'border-radius': '50%'
  }
})

const rotateLeft = () => {
  cropper.value.rotateLeft()
}

const rotateRight = () => {
  cropper.value.rotateRight()
}

const changeScale = (scaleSize) => {
  cropper.value.changeScale(scaleSize)
}

// 注意:getCropData是一个异步方法
const getBase64 = () => {
  cropper.value.getCropData((base64) => {
    previewBase64.value = base64
    console.log('previewBase64 =', previewBase64.value)
  })
}

// 注意:getCropBlob是一个异步方法
const getBlob = () => {
  cropper.value.getCropBlob((blob) => {
    previewBlob.value = blob
    // 虽然 getCropBlob 方法获取的的 Blob 对象在控制台打印时只有 size 和 type 属性,但是仍然可以使用 window.URL.createObjectURL(blob) 生成 url
    // 从 Java 的角度来说,相当于重写了 Blob 类的 toString 方法
    console.log('previewBlob =', previewBlob.value)
  })
}

const updateAvatar = async () => {
  cropper.value.getCropBlob((blob) => {
    let avatar = new File([blob], 'avatar.png')
    let formData = new FormData()
    formData.append('avatar', avatar)

    request
        .post('/user/updateAvatar', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
        .then((response) => {
          if (response.code === 200) {
            ElMessage.success('修改头像成功')
          } else {
            ElMessage.error('修改头像失败')
          }
        })
        .catch((error) => {
          console.log('error =', error)
          ElMessage.error('修改头像失败')
        })
  })
}
</script>

<style scoped>
.title {
  font-size: 40px;
  text-align: center;
}

.main {
  display: flex;
  justify-content: space-around;
}

.crop {
  width: 925px;
  height: 500px;
}

.action-buttons {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}

.blank-line {
  height: 20px;
  width: 100%;
}

.preview-img {
  border: 5px solid black;
}

.preview-title {
  font-size: 20px;
  margin-bottom: 10px;
  text-align: center;
}
</style>

7.2 request.js

import axios from 'axios'

const request = axios.create({
  baseURL: '/api',
  timeout: 60000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

request.interceptors.request.use(

)

request.interceptors.response.use(response => {
  if (response.data) {
    return response.data
  }
  return response
}, (error) => {
  return Promise.reject(error)
})

export default request

7.3 main.js

import '@/assets/main.css'

import {createApp} from 'vue'
import {createPinia} from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import App from './App.vue'
import router from './router'
import 'default-passive-events'

const app = createApp(App)

app.use(createPinia())
app.use(ElementPlus, {locale: zhCn})
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.use(router)

app.mount('#app')

7.4 vite.config.js

import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue()
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8001',
        changeOrigin: true,
        rewrite: (path) => {
          return path.replace('/api', '')
        }
      }
    }
  }
})

8. 完整的源代码

前端:cropper-avatar-frontend

后端:cropper-avatar-backend

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1974581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无线蓝牙耳机哪个品牌好?甄选四款专业蓝牙耳机品牌推荐

随着市场上品牌和型号众多&#xff0c;挑选出最适合自己的蓝牙耳机却变成了一项不小的挑战&#xff0c;不同的用户有着不同的需求——有的人追求音质、有的人注重续航、有的人在意舒适度&#xff0c;还有的人看重的是设计与功能性&#xff0c;那么无线蓝牙耳机哪个品牌好&#…

springboot物流寄查系统-计算机毕业设计源码95192

目 录 1 绪论 1.1 研究背景 1.2选题背景 1.3论文结构与章节安排 2 springboot物流寄查系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分析 2…

Maven实战(三)- Maven仓库

Maven实战&#xff08;三&#xff09;- Maven仓库 文章目录 Maven实战&#xff08;三&#xff09;- Maven仓库1.Maven仓库概念2.仓库布局3.仓库分类3.1.本地仓库3.2.远程仓库3.3.中央仓库3.4.私服 4.远程仓库的配置5.远程仓库认证6.部署构件至远程仓库7.从仓库解析依赖8.镜像 1…

牛客JS题(十九)继承

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 构造函数实现类ES6类的写法原型链的应用 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charset"utf-8" /></head><body><script type"text…

Python数值计算(17)——Hermite插值

这次介绍一下使用差商表构造Hermite多项式的方法。 前面介绍到了两种很经典的插值多项式&#xff0c;即Lagrange和Newton插值多项式&#xff0c;并在前一篇中阐述了如何通过Lagrange插值方式构造Hermite多项式&#xff0c;这次通过牛顿差商法构造Hermite多项式。 1. 数学原理 …

学生党蓝牙耳机哪个牌子好用性价比高?四大顶尖精品蓝牙耳机揭秘

近年来&#xff0c;市面上的蓝牙耳机品牌如雨后春笋般涌现&#xff0c;各大厂商纷纷跨界合作&#xff0c;推出外观时尚、设计新颖的产品&#xff0c;以吸引各位学生党的目光。然而&#xff0c;在这繁华背后&#xff0c;不少产品却忽视了音质、舒适度及适用性等核心要素&#xf…

2024年【广东省安全员A证第四批(主要负责人)】新版试题及广东省安全员A证第四批(主要负责人)考试技巧

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 广东省安全员A证第四批&#xff08;主要负责人&#xff09;新版试题参考答案及广东省安全员A证第四批&#xff08;主要负责人&#xff09;考试试题解析是安全生产模拟考试一点通题库老师及广东省安全员A证第四批&…

1. 什么是操作系统

文章目录 1.1 从功能上来看操作系统1.2 硬件资源 1.1 从功能上来看操作系统 对用户来说&#xff0c;操作系统是一个控制软件&#xff0c;可以用来管理应用程序&#xff0c;它可以限制不同的程序来占用的资源。对内部的软件来说&#xff0c;操作系统是一个管理外设和分配资源的…

LLM 大模型文档语义分块、微调数据集生成

1、LLM 大模型文档语义分块 参考: https://blog.csdn.net/m0_59596990/article/details/140280541 根据上下句的语义相关性,相关就组合成一个分块,不相关就当场两个快 语义模型用的bert-base-chinese: https://huggingface.co/google-bert/bert-base-chinese 代码: 对…

武汉流星汇聚:亚马逊赋能中国卖家全球化战略深化,业绩斐然赢未来

在全球电商的浩瀚星空中&#xff0c;亚马逊无疑是最耀眼的星辰之一&#xff0c;其强大的平台影响力和全球覆盖能力&#xff0c;为无数商家特别是中国卖家提供了前所未有的发展机遇。近年来&#xff0c;中国卖家在亚马逊平台上的表现尤为亮眼&#xff0c;不仅销量持续攀升&#…

Python面试宝典第26题:最长公共子序列

题目 一个字符串的子序列是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些字符&#xff08;也可以不删除任何字符&#xff09;后组成的新字符串。比如&#xff1a;"ace" 是 "abcde" 的子序列&#xff0c;但 "…

基于redis实现优惠劵秒杀下单功能(结合黑马视频总结)

基础业务逻辑 初步实现 Override public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return R…

YOLOv8改进 | 注意力机制 | 反向残差注意力机制【内含创新技巧思维】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

【Docker系列】Docker 镜像管理:删除无标签镜像的技巧

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

【Webpack 踩坑】CSS加载缓慢

问题&#xff1a;使用webpack5&#xff0c;单独index.scss在assets/css目录下&#xff0c;但是不管是production还是development环境下&#xff0c;都会出现dom加载完后再渲染样式 本意是想要将样式单独打包到一个文件夹&#xff0c;还有压缩css 于是用了mini-css-extract-plug…

【LeetCode】219.存在重复元素II

1. 题目 2. 分析 3. 代码 class Solution:def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:num2index defaultdict(list)for idx,num in enumerate(nums):num2index[num].append(idx)for key, val in num2index.items():if len(val) > 2:for i i…

书生大模型实战营--L1关卡-XTuner 微调个人小助手认知

一、为什么要做模型微调&#xff0c;有些场景下大模型无法更好的回答用户问题 二、准备模型以及训练语料 准备工作详细参考&#xff0c;这里有很详细的介绍 https://github.com/InternLM/Tutorial/blob/camp3/docs/L1/XTuner/readme.md 三、微调模型后的回答&#xff0c;符合…

python 爬虫入门实战——爬取维基百科“百科全书”词条页面内链

1. 简述 本次爬取维基百科“百科全书”词条页面内链&#xff0c;仅发送一次请求&#xff0c;获取一个 html 页面&#xff0c;同时不包含应对反爬虫的知识&#xff0c;仅包含最基础的网页爬取、数据清洗、存储为 csv 文件。 爬取网址 url 为 “https://zh.wikipedia.org/wiki/…

历届奥运会奖牌数据(1896年-2024年7月)

奥运会&#xff0c;全称奥林匹克运动会&#xff08;Olympic Games&#xff09;&#xff0c;是国际奥林匹克委员会主办的世界规模最大的综合性体育赛事&#xff0c;每四年一届&#xff0c;会期不超过16天。这项历史悠久的赛事起源于古希腊&#xff0c;现代奥运会则始于1896年的希…

opencascade AIS_ViewCube源码学习小方块

opencascade AIS_ViewCube 小方块 前言 用于显示视图操控立方体的交互对象。 视图立方体由多个部分组成&#xff0c;负责不同的相机操作&#xff1a; 立方体的各个面代表主视图&#xff1a;顶部、底部、左侧、右侧、前侧和后侧。 边表示主视图之一的旋转45度。 顶点表示主视…