From 6360dff1104431e4d4850a51ef60519e44b5efd8 Mon Sep 17 00:00:00 2001 From: ktianc Date: Tue, 8 Aug 2023 11:08:30 +0800 Subject: [PATCH] =?UTF-8?q?editor=20=E5=AF=8C=E6=96=87=E6=9C=AC=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87=EF=BC=8C?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=8A=9F=E8=83=BD=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ContentDetailWrap.vue | 4 +- .../src/components/Editor/src/Editor.vue | 46 ++++++++++++++++++- kinit-admin/src/hooks/web/useTable.ts | 2 +- kinit-admin/src/types/editor.d.ts | 3 ++ kinit-api/application/settings.py | 2 +- kinit-api/apps/vadmin/auth/crud.py | 2 - kinit-api/apps/vadmin/system/views.py | 14 +++++- kinit-api/utils/file/aliyun_oss.py | 45 ++++++++++++++---- kinit-api/utils/file/file_base.py | 2 +- kinit-api/utils/ip_manage.py | 2 + 10 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 kinit-admin/src/types/editor.d.ts diff --git a/kinit-admin/src/components/ContentDetailWrap/src/ContentDetailWrap.vue b/kinit-admin/src/components/ContentDetailWrap/src/ContentDetailWrap.vue index 22aeb83..87e93f0 100644 --- a/kinit-admin/src/components/ContentDetailWrap/src/ContentDetailWrap.vue +++ b/kinit-admin/src/components/ContentDetailWrap/src/ContentDetailWrap.vue @@ -2,7 +2,7 @@ import { ElCard, ElButton } from 'element-plus' import { propTypes } from '@/utils/propTypes' import { useDesign } from '@/hooks/web/useDesign' -import { ref, onMounted, defineEmits } from 'vue' +import { ref, onMounted } from 'vue' import { Sticky } from '@/components/Sticky' import { useI18n } from '@/hooks/web/useI18n' const { t } = useI18n() @@ -15,9 +15,11 @@ defineProps({ title: propTypes.string.def(''), message: propTypes.string.def('') }) + const emit = defineEmits(['back']) const offset = ref(85) const contentDetailWrap = ref() + onMounted(() => { offset.value = contentDetailWrap.value.getBoundingClientRect().top }) diff --git a/kinit-admin/src/components/Editor/src/Editor.vue b/kinit-admin/src/components/Editor/src/Editor.vue index 5f73164..98ecab4 100644 --- a/kinit-admin/src/components/Editor/src/Editor.vue +++ b/kinit-admin/src/components/Editor/src/Editor.vue @@ -6,6 +6,9 @@ import { propTypes } from '@/utils/propTypes' import { isNumber } from '@/utils/is' import { ElMessage } from 'element-plus' import { useLocaleStore } from '@/store/modules/locale' +import { uploadImageToOSS, uploadVideoToOSS } from '@/api/vadmin/system/files' + +// editor 官方文档:https://www.wangeditor.com/v5/getting-started.html const localeStore = useLocaleStore() @@ -79,7 +82,48 @@ const editorConfig = computed((): IEditorConfig => { }, autoFocus: false, scroll: true, - uploadImgShowBase64: true + uploadImgShowBase64: true, + MENU_CONF: { + uploadImage: { + // 自定义上传图片 + // 官方文档:https://www.wangeditor.com/v5/menu-config.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%8A%E4%BC%A0 + async customUpload(file: File, insertFn: InsertImageType) { + // 上传图片前的检查 + if (!['image/jpeg', 'image/gif', 'image/png'].includes(file.type)) { + return ElMessage.error(`${file.name}上传失败:上传图片只能是 JPG/GIF/PNG/ 格式!`) + } + if (!(file.size / 1024 / 1024 < 2)) { + return ElMessage.error(`${file.name}上传失败:上传图片大小不能超过 2MB!`) + } + + // 自己实现上传,并得到图片 url + const formData = new FormData() + formData.append('file', file) + formData.append('path', 'editor/image') + const res = await uploadImageToOSS(formData) + insertFn(res.data, '', res.data) + } + }, + uploadVideo: { + // 自定义上传视频 + // 官方文档:https://www.wangeditor.com/v5/menu-config.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%9F%E8%83%BD-1 + async customUpload(file: File, insertFn: InsertVideoType) { + // 上传视频前的检查 + if (!['video/mp4', 'video/mpeg'].includes(file.type)) { + return ElMessage.error(`${file.name}上传失败:上传视频只能是 mp4/mpeg/ 格式!`) + } + if (!(file.size / 1024 / 1024 < 5)) { + return ElMessage.error(`${file.name}上传失败:上传视频大小不能超过 5MB!`) + } + // 自己实现上传,并得到视频 url + const formData = new FormData() + formData.append('file', file) + formData.append('path', 'editor/video') + const res = await uploadVideoToOSS(formData) + insertFn(res.data, '') + } + } + } }, props.editorConfig || {} ) diff --git a/kinit-admin/src/hooks/web/useTable.ts b/kinit-admin/src/hooks/web/useTable.ts index 7fa9a0c..a5fa61f 100644 --- a/kinit-admin/src/hooks/web/useTable.ts +++ b/kinit-admin/src/hooks/web/useTable.ts @@ -173,7 +173,7 @@ export const useTable = (config?: UseTableConfig) => { // 如果为 false,则说明是点击按钮,则获取当前选择行数据进行删除 delListApi: async ( multiple: boolean, - ids: string[] | number[] | number = [], + ids: string[] | number[] | number | string = [], message = true ) => { const tableRef = await getTable() diff --git a/kinit-admin/src/types/editor.d.ts b/kinit-admin/src/types/editor.d.ts new file mode 100644 index 0000000..42dee5a --- /dev/null +++ b/kinit-admin/src/types/editor.d.ts @@ -0,0 +1,3 @@ +type InsertImageType = (url: string, alt: string, href: string) => void + +type InsertVideoType = (url: string, poster: string = '') => void diff --git a/kinit-api/application/settings.py b/kinit-api/application/settings.py index 0e951f2..6a62ba7 100644 --- a/kinit-api/application/settings.py +++ b/kinit-api/application/settings.py @@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer """ 系统版本 """ -VERSION = "1.10.3" +VERSION = "1.10.4" """安全警告: 不要在生产中打开调试运行!""" DEBUG = True diff --git a/kinit-api/apps/vadmin/auth/crud.py b/kinit-api/apps/vadmin/auth/crud.py index f3695b2..75ac002 100644 --- a/kinit-api/apps/vadmin/auth/crud.py +++ b/kinit-api/apps/vadmin/auth/crud.py @@ -282,8 +282,6 @@ class UserDal(DalBase): 更新当前用户头像 """ result = await AliyunOSS(BucketConf(**settings.ALIYUN_OSS)).upload_image("avatar", file) - if not result: - raise CustomException(msg="上传失败", code=status.HTTP_ERROR) user.avatar = result await self.flush(user) return result diff --git a/kinit-api/apps/vadmin/system/views.py b/kinit-api/apps/vadmin/system/views.py index 8df3986..3517119 100644 --- a/kinit-api/apps/vadmin/system/views.py +++ b/kinit-api/apps/vadmin/system/views.py @@ -113,8 +113,18 @@ async def get_dict_detail(data_id: int, auth: Auth = Depends(AllUserAuth())): @app.post("/upload/image/to/oss", summary="上传图片到阿里云OSS") async def upload_image_to_oss(file: UploadFile, path: str = Form(...)): result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(path, file) - if not result: - return ErrorResponse(msg="上传失败") + return SuccessResponse(result) + + +@app.post("/upload/video/to/oss", summary="上传视频到阿里云OSS") +async def upload_video_to_oss(file: UploadFile, path: str = Form(...)): + result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_video(path, file) + return SuccessResponse(result) + + +@app.post("/upload/file/to/oss", summary="上传文件到阿里云OSS") +async def upload_file_to_oss(file: UploadFile, path: str = Form(...)): + result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_file(path, file) return SuccessResponse(result) diff --git a/kinit-api/utils/file/aliyun_oss.py b/kinit-api/utils/file/aliyun_oss.py index 9d67fc0..8c62ba4 100644 --- a/kinit-api/utils/file/aliyun_oss.py +++ b/kinit-api/utils/file/aliyun_oss.py @@ -11,7 +11,9 @@ from fastapi import UploadFile from pydantic import BaseModel import oss2 # 安装依赖库:pip install oss2 from oss2.models import PutObjectResult +from core.exception import CustomException from core.logger import logger +from utils import status from utils.file.compress.cpressJPG import compress_jpg_png from utils.file.file_manage import FileManage from utils.file.file_base import FileBase @@ -45,15 +47,19 @@ class AliyunOSS(FileBase): self.bucket = oss2.Bucket(auth, bucket.endpoint, bucket.bucket) self.baseUrl = bucket.baseUrl - async def upload_image(self, path: str, file: UploadFile, compress: bool = False) -> str: + async def upload_image(self, path: str, file: UploadFile, compress: bool = False, max_size: int = 10) -> str: """ 上传图片 :param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。 :param file: 文件对象 :param compress: 是否压缩该文件 + :param max_size: 图片文件最大值,单位 MB,默认 10MB :return: 上传后的文件oss链接 """ + # 验证图片类型 + await self.validate_file(file, max_size, self.IMAGE_ACCEPT) + # 生成文件路径 path = self.generate_path(path, file.filename) if compress: # 压缩图片 @@ -63,14 +69,23 @@ class AliyunOSS(FileBase): file_data = f.read() else: file_data = await file.read() - result = self.bucket.put_object(path, file_data) - assert isinstance(result, PutObjectResult) - if result.status != 200: - logger.error(f"图片上传到OSS失败,状态码:{result.status}") - print("图片上传路径", path) - print(f"图片上传到OSS失败,状态码:{result.status}") - return "" - return self.baseUrl + path + return await self.__upload_file_to_oss(path, file_data) + + async def upload_video(self, path: str, file: UploadFile, max_size: int = 100) -> str: + """ + 上传视频 + + :param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。 + :param file: 文件对象 + :param max_size: 视频文件最大值,单位 MB,默认 100MB + :return: 上传后的文件oss链接 + """ + # 验证图片类型 + await self.validate_file(file, max_size, self.VIDEO_ACCEPT) + # 生成文件路径 + path = self.generate_path(path, file.filename) + file_data = await file.read() + return await self.__upload_file_to_oss(path, file_data) async def upload_file(self, path: str, file: UploadFile) -> str: """ @@ -82,9 +97,19 @@ class AliyunOSS(FileBase): """ path = self.generate_path(path, file.filename) file_data = await file.read() + return await self.__upload_file_to_oss(path, file_data) + + async def __upload_file_to_oss(self, path: str, file_data: bytes) -> str: + """ + 上传文件到OSS + + :param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。 + :param file_data: 文件数据 + :return: 上传后的文件oss链接 + """ result = self.bucket.put_object(path, file_data) assert isinstance(result, PutObjectResult) if result.status != 200: logger.error(f"文件上传到OSS失败,状态码:{result.status}") - return "" + raise CustomException("上传文件失败", code=status.HTTP_ERROR) return self.baseUrl + path diff --git a/kinit-api/utils/file/file_base.py b/kinit-api/utils/file/file_base.py index 5e995a8..e3d32f6 100644 --- a/kinit-api/utils/file/file_base.py +++ b/kinit-api/utils/file/file_base.py @@ -17,7 +17,7 @@ from utils import status class FileBase: IMAGE_ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/x-icon"] - VIDEO_ACCEPT = ["audio/mp4", "video/mp4", "video/mpeg"] + VIDEO_ACCEPT = ["video/mp4", "video/mpeg"] ALL_ACCEPT = [*IMAGE_ACCEPT, *VIDEO_ACCEPT] @classmethod diff --git a/kinit-api/utils/ip_manage.py b/kinit-api/utils/ip_manage.py index 5db6e06..b1c5587 100644 --- a/kinit-api/utils/ip_manage.py +++ b/kinit-api/utils/ip_manage.py @@ -16,10 +16,12 @@ https://api.ip138.com/ip/?ip=58.16.180.3&datatype=jsonp&token=cc87f3c77747bccbaa aiohttp 异步请求文档:https://docs.aiohttp.org/en/stable/client_quickstart.html """ from aiohttp import TCPConnector + from application.settings import IP_PARSE_TOKEN, IP_PARSE_ENABLE import aiohttp from core.logger import logger from pydantic import BaseModel +from typing import Optional class IPLocationOut(BaseModel):