From 54ef87c46a05e2489d45f1eb24fac2a33d458016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A6=99=E7=A0=81=E7=94=9F=E8=8A=B1?= <18523774412@qq.com> Date: Sat, 12 Mar 2022 20:57:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0=E3=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E4=B8=AA=E4=BA=BA=E8=B5=84=E6=96=99=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- app/admin/controller/Ajax.php | 28 +++ app/admin/controller/routine/AdminInfo.php | 7 + app/admin/lang/en.php | 42 ++-- app/admin/lang/zh-cn.php | 42 ++-- app/api/lang/en.php | 2 + app/api/lang/zh-cn.php | 2 + app/api/middleware.php | 3 +- app/common/library/Upload.php | 248 ++++++++++++++++++++ app/common/model/Attachment.php | 37 +++ config/upload.php | 17 ++ web/src/api/common.ts | 17 ++ web/src/utils/axios.ts | 9 +- web/src/views/backend/routine/adminInfo.vue | 27 ++- web/types/global.d.ts | 9 + 15 files changed, 450 insertions(+), 42 deletions(-) create mode 100644 app/admin/controller/Ajax.php create mode 100644 app/api/lang/en.php create mode 100644 app/api/lang/zh-cn.php create mode 100644 app/common/library/Upload.php create mode 100644 app/common/model/Attachment.php create mode 100644 config/upload.php diff --git a/.gitignore b/.gitignore index 181767b0..f2147327 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ dist-ssr /public/*.lock /public/index.html /public/assets -/public/uploads/* +/public/storage/* .DS_Store composer.lock /.env diff --git a/app/admin/controller/Ajax.php b/app/admin/controller/Ajax.php new file mode 100644 index 00000000..8ce3534f --- /dev/null +++ b/app/admin/controller/Ajax.php @@ -0,0 +1,28 @@ +request->file('file'); + try { + $upload = new Upload($file); + $attachment = $upload->upload(null, $this->auth->id); + $attachment['fullurl'] = full_url($attachment['url']); + unset($attachment['createtime'], $attachment['quote']); + } catch (Exception | FileException $e) { + $this->error($e->getMessage()); + } + + $this->success(__('File uploaded successfully'), [ + 'file' => $attachment + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/routine/AdminInfo.php b/app/admin/controller/routine/AdminInfo.php index daa028e0..d93bd807 100644 --- a/app/admin/controller/routine/AdminInfo.php +++ b/app/admin/controller/routine/AdminInfo.php @@ -47,6 +47,13 @@ class AdminInfo extends Backend $this->error(__('Parameter %s can not be empty', [''])); } + if (isset($data['avatar']) && $data['avatar']) { + $row->avatar = $data['avatar']; + if ($row->save()) { + $this->success('头像修改成功!'); + } + } + // 数据验证 if ($this->modelValidate) { $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php index 4926c01a..72fa291c 100644 --- a/app/admin/lang/en.php +++ b/app/admin/lang/en.php @@ -1,21 +1,27 @@ 'User Name', - 'Password' => 'Password', - 'Nickname' => 'Nickname', - 'Email' => 'Email', - 'Mobile' => 'Mobile', - 'Captcha' => 'Captcha', - 'CaptchaId' => 'Captcha Id', - 'Please enter the correct verification code' => 'Please enter the correct verification code!', - 'Parameter %s can not be empty' => 'Parameter %s can not be empty', - 'Record not found' => 'Record not found', - 'No rows were added' => 'No rows were added', - 'No rows were deleted' => 'No rows were deleted', - 'No rows updated' => 'No rows updated', - 'Update successful' => 'Update successful!', - 'Added successfully' => 'Added successfully!', - 'Deleted successfully' => 'Deleted successfully!', - 'Parameter error' => 'Parameter error!', - 'Invalid collation because the weights of the two targets are equal' => 'Invalid collation because the weights of the two targets are equal', + 'Username' => 'User Name', + 'Password' => 'Password', + 'Nickname' => 'Nickname', + 'Email' => 'Email', + 'Mobile' => 'Mobile', + 'Captcha' => 'Captcha', + 'CaptchaId' => 'Captcha Id', + 'Please enter the correct verification code' => 'Please enter the correct verification code!', + 'Parameter %s can not be empty' => 'Parameter %s can not be empty', + 'Record not found' => 'Record not found', + 'No rows were added' => 'No rows were added', + 'No rows were deleted' => 'No rows were deleted', + 'No rows updated' => 'No rows updated', + 'Update successful' => 'Update successful!', + 'Added successfully' => 'Added successfully!', + 'Deleted successfully' => 'Deleted successfully!', + 'Parameter error' => 'Parameter error!', + 'Invalid collation because the weights of the two targets are equal' => 'Invalid collation because the weights of the two targets are equal', + 'File uploaded successfully' => 'File uploaded successfully', + 'No files were uploaded' => 'No files were uploaded', + 'The uploaded file format is not allowed' => 'The uploaded file format is not allowed', + 'The uploaded image file is not a valid image' => 'The uploaded image file is not a valid image', + 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB', + 'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the upload limit of the server', ]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index 307219f6..8accb706 100644 --- a/app/admin/lang/zh-cn.php +++ b/app/admin/lang/zh-cn.php @@ -1,21 +1,27 @@ '用户名', - 'Password' => '密码', - 'Nickname' => '昵称', - 'Email' => '邮箱', - 'Mobile' => '手机号', - 'Captcha' => '验证码', - 'CaptchaId' => '验证码ID', - 'Please enter the correct verification code' => '请输入正确的验证码!', - 'Parameter %s can not be empty' => '参数%s不能为空', - 'Record not found' => '记录未找到', - 'No rows were added' => '未添加任何行', - 'No rows were deleted' => '未删除任何行', - 'No rows updated' => '未更新任何行', - 'Update successful' => '更新成功!', - 'Added successfully' => '添加成功!', - 'Deleted successfully' => '删除成功!', - 'Parameter error' => '参数错误!', - 'Invalid collation because the weights of the two targets are equal' => '无效排序整理,因为两个目标权重是相等的', + 'Username' => '用户名', + 'Password' => '密码', + 'Nickname' => '昵称', + 'Email' => '邮箱', + 'Mobile' => '手机号', + 'Captcha' => '验证码', + 'CaptchaId' => '验证码ID', + 'Please enter the correct verification code' => '请输入正确的验证码!', + 'Parameter %s can not be empty' => '参数%s不能为空', + 'Record not found' => '记录未找到', + 'No rows were added' => '未添加任何行', + 'No rows were deleted' => '未删除任何行', + 'No rows updated' => '未更新任何行', + 'Update successful' => '更新成功!', + 'Added successfully' => '添加成功!', + 'Deleted successfully' => '删除成功!', + 'Parameter error' => '参数错误!', + 'Invalid collation because the weights of the two targets are equal' => '无效排序整理,因为两个目标权重是相等的', + 'File uploaded successfully' => '文件上传成功!', + 'No files were uploaded' => '没有文件被上传', + 'The uploaded file format is not allowed' => '上传的文件格式未被允许', + 'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像', + 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM', + 'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!', ]; \ No newline at end of file diff --git a/app/api/lang/en.php b/app/api/lang/en.php new file mode 100644 index 00000000..a1b26e62 --- /dev/null +++ b/app/api/lang/en.php @@ -0,0 +1,2 @@ +config = Config::get('upload'); + if ($config) { + $this->config = array_merge($this->config, $config); + } + + if ($file) { + $this->setFile($file); + } + } + + /** + * 设置文件 + * @param UploadedFile $file + */ + public function setFile($file) + { + if (empty($file)) { + throw new Exception(__('No files were uploaded'), 10001); + } + + $suffix = strtolower($file->extension()); + $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; + $fileInfo['suffix'] = $suffix; + $fileInfo['type'] = $file->getOriginalMime(); + $fileInfo['size'] = $file->getSize(); + $fileInfo['name'] = $file->getOriginalName(); + $fileInfo['sha1'] = $file->sha1(); + + $this->file = $file; + $this->fileInfo = $fileInfo; + } + + /** + * 检查文件类型 + * @return bool + * @throws Exception + */ + protected function checkMimetype() + { + $mimetypeArr = explode(',', strtolower($this->config['mimetype'])); + $typeArr = explode('/', $this->fileInfo['type']); + //验证文件后缀 + if ($this->config['mimetype'] === '*' + || in_array($this->fileInfo['suffix'], $mimetypeArr) || in_array('.' . $this->fileInfo['suffix'], $mimetypeArr) + || in_array($this->fileInfo['type'], $mimetypeArr) || in_array($typeArr[0] . "/*", $mimetypeArr)) { + return true; + } + throw new Exception(__('The uploaded file format is not allowed'), 10002); + } + + /** + * 是否是图片并设置好相关属性 + * @return bool + * @throws Exception + */ + protected function checkIsImage() + { + if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) { + $imgInfo = getimagesize($this->file->getPathname()); + if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) { + throw new Exception(__('The uploaded image file is not a valid image')); + } + $this->fileInfo['width'] = $imgInfo[0]; + $this->fileInfo['height'] = $imgInfo[1]; + $this->isImage = true; + return true; + } + return false; + } + + /** + * 上传的文件是否为图片 + * @return bool + */ + public function isImage() + { + return $this->isImage; + } + + /** + * 检查文件大小 + * @throws Exception + */ + protected function checkSize() + { + preg_match('/([0-9\.]+)(\w+)/', $this->config['maxsize'], $matches); + $size = $matches ? $matches[1] : $this->config['maxsize']; + $type = $matches ? strtolower($matches[2]) : 'b'; + $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3]; + $size = (int)($size * pow(1024, $typeDict[$type] ?? 0)); + if ($this->fileInfo['size'] > $size) { + throw new Exception(__('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [ + round($this->fileInfo['size'] / pow(1024, 2), 2), + round($size / pow(1024, 2), 2) + ])); + } + } + + /** + * 获取文件后缀 + * @return mixed|string + */ + public function getSuffix() + { + return $this->fileInfo['suffix'] ?: 'file'; + } + + /** + * 获取文件保存名 + * @param null $saveName + * @param null $filename + * @param null $sha1 + * @return array|mixed|string|string[] + */ + public function getSaveName($saveName = null, $filename = null, $sha1 = null) + { + if ($filename) { + $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; + } else { + $suffix = $this->fileInfo['suffix']; + } + $filename = $filename ? $filename : ($suffix ? substr($this->fileInfo['name'], 0, strripos($this->fileInfo['name'], '.')) : $this->fileInfo['name']); + $sha1 = $sha1 ? $sha1 : $this->fileInfo['sha1']; + $replaceArr = [ + '{topic}' => $this->topic, + '{year}' => date("Y"), + '{mon}' => date("m"), + '{day}' => date("d"), + '{hour}' => date("H"), + '{min}' => date("i"), + '{sec}' => date("s"), + '{random}' => Random::build(), + '{random32}' => Random::build('alnum', 32), + '{filename}' => substr($filename, 0, 100), + '{suffix}' => $suffix, + '{.suffix}' => $suffix ? '.' . $suffix : '', + '{filesha1}' => $sha1, + ]; + $saveName = $saveName ? $saveName : $this->config['savename']; + $saveName = str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName); + return $saveName; + } + + /** + * 上传文件 + * @param null $saveName + * @return array + * @throws Exception + */ + public function upload($saveName = null, $adminId = 0, $userId = 0) + { + if (empty($this->file)) { + throw new Exception(__('No files have been uploaded or the file size exceeds the upload limit of the server')); + } + + $this->checkSize(); + $this->checkMimetype(); + $this->checkIsImage(); + + $saveName = $saveName ? $saveName : $this->getSaveName(); + $saveName = '/' . ltrim($saveName, '/'); + $uploadDir = substr($saveName, 0, strripos($saveName, '/') + 1); + $fileName = substr($saveName, strripos($saveName, '/') + 1); + + $destDir = root_path() . 'public' . str_replace('/', DIRECTORY_SEPARATOR, $uploadDir); + + $this->file->move($destDir, $fileName); + + $params = [ + 'topic' => $this->topic, + 'admin_id' => $adminId, + 'user_id' => $userId, + 'url' => $this->getSaveName(), + 'width' => $this->fileInfo['width'], + 'height' => $this->fileInfo['height'], + 'name' => substr(htmlspecialchars(strip_tags($this->fileInfo['name'])), 0, 100), + 'size' => $this->fileInfo['size'], + 'mimetype' => $this->fileInfo['type'], + 'storage' => 'local', + 'sha1' => $this->fileInfo['sha1'] + ]; + + $attachment = new Attachment(); + $attachment->data(array_filter($params)); + $res = $attachment->save(); + if (!$res) { + $attachment = Attachment::where([ + ['sha1', '=', $params['sha1']], + ['topic', '=', $params['topic']], + ['storage', '=', $params['storage']], + ])->find(); + } + + return $attachment->toArray(); + } +} \ No newline at end of file diff --git a/app/common/model/Attachment.php b/app/common/model/Attachment.php new file mode 100644 index 00000000..d8fe4df0 --- /dev/null +++ b/app/common/model/Attachment.php @@ -0,0 +1,37 @@ +where([ + ['sha1', '=', $model->sha1], + ['topic', '=', $model->topic], + ['storage', '=', $model->storage], + ])->find(); + if ($repeat) { + $repeat->quote++; + $repeat->lastuploadtime = time(); + $repeat->save(); + return false; + } + } + + protected static function onAfterInsert($row) + { + if (!$row->lastuploadtime) { + $row->quote = 1; + $row->lastuploadtime = time(); + $row->save(); + } + } +} \ No newline at end of file diff --git a/config/upload.php b/config/upload.php new file mode 100644 index 00000000..c1b56d2f --- /dev/null +++ b/config/upload.php @@ -0,0 +1,17 @@ + 'api/common/upload', + // cdn地址 + 'cdn' => '', + // 文件保存格式化方法 + 'savename' => '/storage/{topic}/{year}{mon}{day}/{filesha1}{.suffix}', + // 最大上传 + 'maxsize' => '10mb', + // 文件格式限制 + 'mimetype' => 'jpg,png,bmp,jpeg,gif,webp,zip,rar,xls,xlsx,doc,docx,wav,mp4,mp3,pdf,txt', +]; \ No newline at end of file diff --git a/web/src/api/common.ts b/web/src/api/common.ts index 46f7f0b0..44ac2327 100644 --- a/web/src/api/common.ts +++ b/web/src/api/common.ts @@ -1,3 +1,4 @@ +import { AxiosPromise } from 'axios' import createAxios from '/@/utils/axios' /* @@ -5,11 +6,16 @@ import createAxios from '/@/utils/axios' */ /* + * URL * import { getUrl } from '/@/utils/axios' * 不可以直接使用 getUrl() 获取到域名拼接好再返回;会出现`热更新时报 getUrl 未载入`的问题 */ +export const adminUploadUrl = '/index.php/admin/ajax/upload' export const captchaUrl = '/index.php/api/common/captcha' +/* + * 远程下拉框数据获取 + */ export function getSelectData(remoteUrl: string, q: string, params: {}) { return createAxios({ url: remoteUrl, @@ -20,3 +26,14 @@ export function getSelectData(remoteUrl: string, q: string, params: {}) { }), }) } + +/* + * admin上传文件 + */ +export function adminFileUpload(fd: FormData): ApiPromise { + return createAxios({ + url: adminUploadUrl, + method: 'POST', + data: fd, + }) as ApiPromise +} diff --git a/web/src/utils/axios.ts b/web/src/utils/axios.ts index 4c3399f2..da03f11a 100644 --- a/web/src/utils/axios.ts +++ b/web/src/utils/axios.ts @@ -1,4 +1,4 @@ -import axios, { Method } from 'axios' +import axios, { AxiosPromise, Method } from 'axios' import type { AxiosRequestConfig } from 'axios' import { computed } from 'vue' import { ElMessage, ElLoading, LoadingOptions, ElNotification } from 'element-plus' @@ -17,7 +17,12 @@ export const getUrl = (): string => { return value == 'getCurrentDomain' ? window.location.protocol + '//' + window.location.host : value } -function createAxios(axiosConfig: AxiosRequestConfig, options: Options = {}, loading: LoadingOptions = {}) { +/* + * 创建Axios + * 默认开启`reductDataFormat(简洁响应)`,返回类型为`ApiPromise` + * 关闭`reductDataFormat`,返回类型则为`AxiosPromise` + */ +function createAxios(axiosConfig: AxiosRequestConfig, options: Options = {}, loading: LoadingOptions = {}): ApiPromise | AxiosPromise { const config = useConfig() const lang = computed(() => config.lang.defaultLang) diff --git a/web/src/views/backend/routine/adminInfo.vue b/web/src/views/backend/routine/adminInfo.vue index f2ee551a..a8b3d10a 100644 --- a/web/src/views/backend/routine/adminInfo.vue +++ b/web/src/views/backend/routine/adminInfo.vue @@ -3,7 +3,14 @@
- +
@@ -57,10 +64,11 @@ import { ref, reactive } from 'vue' import { useI18n } from 'vue-i18n' import { index, postData } from '/@/api/backend/routine/AdminInfo' -import type { ElForm } from 'element-plus' +import { ElForm, ElNotification } from 'element-plus' import { onResetForm } from '/@/utils/common' import { uuid } from '/@/utils/uuid' import { validatorMobile, validatorPassword } from '/@/utils/validate' +import { adminFileUpload } from '/@/api/common' const { t } = useI18n() const formRef = ref>() @@ -115,6 +123,21 @@ const rules: any = reactive({ ], }) +const onAvatarBeforeUpload = (file: any) => { + let fd = new FormData() + fd.append('file', file.raw) + adminFileUpload(fd).then((res) => { + if (res.code == 1) { + postData({ + id: state.adminInfo.id, + avatar: res.data.file.url, + }).then(() => { + state.adminInfo.avatar = res.data.file.fullurl + }) + } + }) +} + const onSubmit = (formEl: InstanceType | undefined) => { if (!formEl) return formEl.validate((valid) => { diff --git a/web/types/global.d.ts b/web/types/global.d.ts index 80391ac1..aa975bf2 100644 --- a/web/types/global.d.ts +++ b/web/types/global.d.ts @@ -17,3 +17,12 @@ interface FormItemProps { interface anyObj { [key: string]: any } + +interface ApiResponse { + code: number + data: T + msg: string + time: number +} + +interface ApiPromise extends Promise> {}