feat:文件上传接口实现、管理员个人资料页面头像上传

This commit is contained in:
妙码生花 2022-03-12 20:57:36 +08:00
parent 206a5d691e
commit 54ef87c46a
15 changed files with 450 additions and 42 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ dist-ssr
/public/*.lock
/public/index.html
/public/assets
/public/uploads/*
/public/storage/*
.DS_Store
composer.lock
/.env

View File

@ -0,0 +1,28 @@
<?php
namespace app\admin\controller;
use think\Exception;
use think\exception\FileException;
use app\common\library\Upload;
use app\common\controller\Backend;
class Ajax extends Backend
{
public function upload()
{
$file = $this->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
]);
}
}

View File

@ -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));

View File

@ -18,4 +18,10 @@ return [
'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',
];

View File

@ -18,4 +18,10 @@ return [
'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' => '没有文件被上传或文件大小超出服务器上传限制!',
];

2
app/api/lang/en.php Normal file
View File

@ -0,0 +1,2 @@
<?php
return [];

2
app/api/lang/zh-cn.php Normal file
View File

@ -0,0 +1,2 @@
<?php
return [];

View File

@ -1,5 +1,6 @@
<?php
// 这是系统自动生成的middleware定义文件
return [
\app\common\library\AllowCrossDomain::class,
\think\middleware\LoadLangPack::class,
];

View File

@ -0,0 +1,248 @@
<?php
namespace app\common\library;
use ba\Random;
use think\Exception;
use think\facade\Config;
use think\file\UploadedFile;
use app\common\model\Attachment;
/**
*
*/
class Upload
{
/**
* 配置信息
* @var array
*/
protected $config = [];
/**
* @var UploadedFile
*/
protected $file = null;
/**
* 是否是图片
* @var bool
*/
protected $isImage = false;
/**
* 文件信息
* @var null
*/
protected $fileInfo = null;
/**
* 细目
* @var string
*/
protected $topic = 'default';
/**
* 构造方法
* @param UploadedFile $file
* @throws Exception
*/
public function __construct($file = null, $config = [])
{
$this->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();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace app\common\model;
use think\Model;
class Attachment extends Model
{
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createtime';
protected $updateTime = null;
protected static function onBeforeInsert($model)
{
$repeat = $model->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();
}
}
}

17
config/upload.php Normal file
View File

@ -0,0 +1,17 @@
<?php
// +----------------------------------------------------------------------
// | BuildAdmin设置
// +----------------------------------------------------------------------
return [
// 上传Url
'url' => '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',
];

View File

@ -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
}

View File

@ -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)

View File

@ -3,7 +3,14 @@
<el-row :gutter="20">
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="10">
<div class="admin-info">
<el-upload class="avatar-uploader" action="" :show-file-list="false">
<el-upload
class="avatar-uploader"
action=""
:show-file-list="false"
@change="onAvatarBeforeUpload"
:auto-upload="false"
accept="image/gif, image/jpg, image/jpeg, image/bmp, image/png, image/webp"
>
<img :src="state.adminInfo.avatar" class="avatar" />
</el-upload>
<div class="admin-info-base">
@ -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<InstanceType<typeof ElForm>>()
@ -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<typeof ElForm> | undefined) => {
if (!formEl) return
formEl.validate((valid) => {

View File

@ -17,3 +17,12 @@ interface FormItemProps {
interface anyObj {
[key: string]: any
}
interface ApiResponse<T = any> {
code: number
data: T
msg: string
time: number
}
interface ApiPromise<T = any> extends Promise<ApiResponse<T>> {}