mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-15 21:32:49 +00:00
perf: 优化实现文件夹下载以及多文件压缩下载功能
This commit is contained in:
parent
08fccf4adc
commit
ed4afa63f0
@ -11,6 +11,7 @@ use App\Models\FileUser;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Ihttp;
|
use App\Module\Ihttp;
|
||||||
|
use Swoole\Coroutine;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
use Request;
|
use Request;
|
||||||
@ -1014,23 +1015,25 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/download/zip 20. 打包下载
|
* @api {get} api/file/download/pack 20. 打包文件
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup file
|
* @apiGroup file
|
||||||
* @apiName download__zip
|
* @apiName download__pack
|
||||||
*
|
*
|
||||||
* @apiParam {Array} [ids] 文件ID,格式: [id, id2, id3]
|
* @apiParam {Array} [ids] 文件ID,格式: [id, id2, id3]
|
||||||
|
* @apiParam {String} [name] 下载文件名
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
* @apiSuccess {Object} data 返回数据
|
* @apiSuccess {Object} data 返回数据
|
||||||
*/
|
*/
|
||||||
public function download__zip()
|
public function download__pack()
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$ids = Request::input('ids');
|
$ids = Request::input('ids');
|
||||||
|
$downName = Request::input('name');
|
||||||
|
|
||||||
if (!is_array($ids) || empty($ids)) {
|
if (!is_array($ids) || empty($ids)) {
|
||||||
abort(403, "Please select the file or folder to download.");
|
abort(403, "Please select the file or folder to download.");
|
||||||
@ -1053,9 +1056,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$zip = new \ZipArchive();
|
$zip = new \ZipArchive();
|
||||||
// 下载文件名
|
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
||||||
$downName = count($ids) > 1 ? 'file_'. date("YmdHis") : $files[0]->name;
|
|
||||||
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName . '.zip';
|
|
||||||
$zipPath = storage_path('app/'.$zipName);
|
$zipPath = storage_path('app/'.$zipName);
|
||||||
Base::makeDir(dirname($zipPath));
|
Base::makeDir(dirname($zipPath));
|
||||||
|
|
||||||
@ -1063,12 +1064,56 @@ class FileController extends AbstractController
|
|||||||
abort(403, "Failed to create compressed file.");
|
abort(403, "Failed to create compressed file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
array_walk($files, function($file) use ($zip) {
|
go(function() use ($zip, $files, $downName) {
|
||||||
File::addFileTreeToZip($zip, $file);
|
Coroutine::sleep(0.1);
|
||||||
|
// 压缩进度
|
||||||
|
$progress = 0;
|
||||||
|
$zip->registerProgressCallback(0.05, function($ratio) use ($downName, $progress) {
|
||||||
|
$progress = round($ratio * 100);
|
||||||
|
File::filePushMsg('compress', [
|
||||||
|
'name'=> $downName,
|
||||||
|
'progress' => $progress
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
File::addFileTreeToZip($zip, $file);
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
if ($progress < 100) {
|
||||||
|
File::filePushMsg('compress', [
|
||||||
|
'name'=> $downName,
|
||||||
|
'progress' => 100
|
||||||
|
]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$zip->close();
|
return Base::retSuccess('success');
|
||||||
|
}
|
||||||
|
|
||||||
return response()->download($zipPath, $downName .'.zip')->deleteFileAfterSend(true);
|
/**
|
||||||
|
* @api {get} api/file/download/confirm 21. 确认下载
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup file
|
||||||
|
* @apiName download__confirm
|
||||||
|
*
|
||||||
|
* @apiParam {String} [name] 下载文件名
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function download__confirm()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$downName = Request::input('name');
|
||||||
|
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
||||||
|
$zipPath = storage_path('app/'.$zipName);
|
||||||
|
if (!file_exists($zipPath)) {
|
||||||
|
abort(403, "The file does not exist.");
|
||||||
|
}
|
||||||
|
return response()->download($zipPath)->deleteFileAfterSend(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -875,13 +875,13 @@ class File extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* 文件夹文件添加到压缩文件
|
* 文件夹文件添加到压缩文件
|
||||||
*
|
*
|
||||||
* @param [type] $zip
|
* @param \ZipArchive $zip
|
||||||
* @param object $file
|
* @param object $file
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function addFileTreeToZip($zip, $file)
|
public static function addFileTreeToZip($zip, $file)
|
||||||
{
|
{
|
||||||
if ($file->type != 'folder') {
|
if ($file->type != 'folder' && $file->name != '') {
|
||||||
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
|
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
|
||||||
$content = Base::json2array($content?->content ?: []);
|
$content = Base::json2array($content?->content ?: []);
|
||||||
$typeExtensions = [
|
$typeExtensions = [
|
||||||
@ -892,7 +892,7 @@ class File extends AbstractModel
|
|||||||
if (array_key_exists($file->type, $typeExtensions)) {
|
if (array_key_exists($file->type, $typeExtensions)) {
|
||||||
$filePath = empty($content) ? public_path('assets/office/empty.' . $typeExtensions[$file->type]) : public_path($content['url']);
|
$filePath = empty($content) ? public_path('assets/office/empty.' . $typeExtensions[$file->type]) : public_path($content['url']);
|
||||||
}
|
}
|
||||||
|
//
|
||||||
$relativePath = $file->path . '.' . $file->ext;
|
$relativePath = $file->path . '.' . $file->ext;
|
||||||
if (file_exists($filePath)) {
|
if (file_exists($filePath)) {
|
||||||
$zip->addFile($filePath, $relativePath);
|
$zip->addFile($filePath, $relativePath);
|
||||||
@ -917,4 +917,31 @@ class File extends AbstractModel
|
|||||||
$zip->addEmptyDir($file->path);
|
$zip->addEmptyDir($file->path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件推送消息
|
||||||
|
* @param $action
|
||||||
|
* @param array|null $data 发送内容
|
||||||
|
* @param array $userid 会员ID
|
||||||
|
*/
|
||||||
|
public static function filePushMsg($action, $data = null, $userid = null)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
$userid = User::auth()->userid();
|
||||||
|
if (empty($userid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$msg = [
|
||||||
|
'type' => 'file',
|
||||||
|
'action' => $action,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
$params = [
|
||||||
|
'userid' => $userid,
|
||||||
|
'msg' => $msg
|
||||||
|
];
|
||||||
|
$task = new PushTask($params, false);
|
||||||
|
Task::deliver($task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,9 +10,13 @@
|
|||||||
<h1>{{$L('文件')}}</h1>
|
<h1>{{$L('文件')}}</h1>
|
||||||
<div v-if="loadIng == 0" class="file-refresh" @click="getFileList"><i class="taskfont"></i></div>
|
<div v-if="loadIng == 0" class="file-refresh" @click="getFileList"><i class="taskfont"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="uploadList.length > 0" class="file-status" @click="uploadShow=true">
|
<div v-if="uploadList.length > 0" class="file-status" @click="[uploadShow=true, packShow=false]">
|
||||||
<Loading v-if="uploadList.find(({status}) => status !== 'finished')"/>
|
<Loading v-if="uploadList.find(({status}) => status !== 'finished')"/>
|
||||||
<Button v-else shape="circle" icon="md-checkmark"></Button>
|
<Button v-else shape="circle" icon="md-arrow-round-up"></Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="packList.length > 0" class="file-status" @click="[packShow=true, uploadShow=false]">
|
||||||
|
<Loading v-if="packList.find(({status}) => status !== 'finished')"/>
|
||||||
|
<Button v-else shape="circle" icon="md-arrow-round-down"></Button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['file-search', searchKey ? 'has-value' : '']" @click="onSearchFocus" @mouseenter="onSearchFocus">
|
<div :class="['file-search', searchKey ? 'has-value' : '']" @click="onSearchFocus" @mouseenter="onSearchFocus">
|
||||||
<Input v-model="searchKey" ref="searchInput" suffix="ios-search" @on-change="onSearchChange" :placeholder="$L('搜索名称')"/>
|
<Input v-model="searchKey" ref="searchInput" suffix="ios-search" @on-change="onSearchChange" :placeholder="$L('搜索名称')"/>
|
||||||
@ -240,6 +244,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="packShow && packList.length > 0" class="file-upload-list">
|
||||||
|
<div class="upload-wrap">
|
||||||
|
<div class="title">
|
||||||
|
打包列表 ({{packList.length}})
|
||||||
|
<em v-if="packList.find(({status}) => status === 'finished')" @click="packClear">清空已完成</em>
|
||||||
|
</div>
|
||||||
|
<ul class="content">
|
||||||
|
<li v-for="(item, index) in packList" :key="index" v-if="index < 100">
|
||||||
|
<AutoTip class="file-name">{{packName(item)}}</AutoTip>
|
||||||
|
<AutoTip v-if="item.status === 'finished' && item.response && item.response.ret !== 1" class="file-error">{{item.response.msg}}</AutoTip>
|
||||||
|
<Progress v-else :percent="packPercentageParse(item.percentage)" :stroke-width="5" />
|
||||||
|
<Icon class="file-close" type="ios-close-circle-outline" @click="packList.splice(index, 1)"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Icon class="close" type="md-close" @click="packShow=false"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--上传文件-->
|
<!--上传文件-->
|
||||||
<Upload
|
<Upload
|
||||||
name="files"
|
name="files"
|
||||||
@ -407,6 +429,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
import {sortBy} from "lodash";
|
import {sortBy} from "lodash";
|
||||||
import DrawerOverlay from "../../components/DrawerOverlay";
|
import DrawerOverlay from "../../components/DrawerOverlay";
|
||||||
@ -423,6 +446,9 @@ export default {
|
|||||||
directives: {longpress},
|
directives: {longpress},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
packList: [],
|
||||||
|
packShow: false,
|
||||||
|
|
||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
@ -1419,6 +1445,65 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/********************文件打包下载部分************************/
|
||||||
|
|
||||||
|
packName(item) {
|
||||||
|
return item.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
packPercentageParse(val) {
|
||||||
|
return parseInt(val, 10);
|
||||||
|
},
|
||||||
|
|
||||||
|
packClear() {
|
||||||
|
this.packList = this.packList.filter(item => item.status !== 'finished');
|
||||||
|
this.packShow = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async startPack(filePackName) {
|
||||||
|
const file = { name: filePackName, status: 'packing', percentage: 0 };
|
||||||
|
this.packList.push(file);
|
||||||
|
this.uploadShow = false; // 隐藏上传列表
|
||||||
|
this.packShow = true; // 显示打包列表
|
||||||
|
|
||||||
|
const downloadInterval = setInterval(async () => {
|
||||||
|
const pack = this.$store.state.packLists.find(({name}) => name == filePackName)
|
||||||
|
file.percentage = Math.max(1, pack.progress);
|
||||||
|
if (file.percentage >= 100) {
|
||||||
|
file.status = 'finished';
|
||||||
|
clearInterval(downloadInterval);
|
||||||
|
// 下载文件
|
||||||
|
await this.downloadPackFile(filePackName);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadPackFile(filePackName) {
|
||||||
|
const downloadUrl = $A.apiUrl(`file/download/confirm?name=${filePackName}&token=${this.userToken}`);
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
url: downloadUrl,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
console.error('No data received from server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], { type: response.data.type });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `${filePackName}`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
downloadZipFile(ids){
|
downloadZipFile(ids){
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return
|
return
|
||||||
@ -1427,26 +1512,26 @@ export default {
|
|||||||
const allFolder = !ids.some(id => this.fileLists.some(({ type, id: itemId }) => type !== 'folder' && itemId === id));
|
const allFolder = !ids.some(id => this.fileLists.some(({ type, id: itemId }) => type !== 'folder' && itemId === id));
|
||||||
const typeName = allFolder ? "文件夹" : "文件";
|
const typeName = allFolder ? "文件夹" : "文件";
|
||||||
const fileName = ids.length === 1 ? `【${firstFile.name}】${typeName}` : `【${firstFile.name}】等${ids.length}个${typeName}`;
|
const fileName = ids.length === 1 ? `【${firstFile.name}】${typeName}` : `【${firstFile.name}】等${ids.length}个${typeName}`;
|
||||||
|
const filePackName = ids.length === 1 ? `${firstFile.name}_${$A.formatDate("YmdHis")}.zip` : `file_${$A.formatDate("YmdHis")}.zip`;
|
||||||
|
|
||||||
$A.modalConfirm({
|
$A.modalConfirm({
|
||||||
title: '打包下载',
|
title: '打包下载',
|
||||||
content: `你确定要打包下载${fileName}吗?`,
|
content: `你确定要打包下载${fileName}吗?`,
|
||||||
okText: '确定',
|
okText: '确定',
|
||||||
onOk: () => {
|
onOk: async () => {
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
this.$store.dispatch("call", {
|
const { msg } = await this.$store.dispatch("call", {
|
||||||
url: 'file/download/check',
|
url: 'file/download/check',
|
||||||
data: {
|
data: { ids },
|
||||||
ids,
|
|
||||||
},
|
|
||||||
}).then(({msg}) => {
|
|
||||||
const idsParam = ids.join('&ids[]=');
|
|
||||||
this.$store.dispatch('downUrl', $A.apiUrl(`file/download/zip?ids[]=${idsParam}`));
|
|
||||||
}).catch(({msg}) => {
|
|
||||||
$A.modalError(msg)
|
|
||||||
});
|
});
|
||||||
})
|
this.startPack(filePackName);
|
||||||
|
await this.$store.dispatch("call", {
|
||||||
|
url: 'file/download/pack',
|
||||||
|
data: { ids, name: filePackName },
|
||||||
|
});
|
||||||
|
} catch ({ msg }) {
|
||||||
|
$A.modalError(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -1884,6 +1969,7 @@ export default {
|
|||||||
handleBeforeUpload() {
|
handleBeforeUpload() {
|
||||||
//上传前判断
|
//上传前判断
|
||||||
this.uploadShow = true;
|
this.uploadShow = true;
|
||||||
|
this.packShow = false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
23
resources/assets/js/store/actions.js
vendored
23
resources/assets/js/store/actions.js
vendored
@ -920,6 +920,24 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取压缩进度
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
packProgress({state, dispatch}, data) {
|
||||||
|
$A.execMainDispatch("packProgress", data)
|
||||||
|
//
|
||||||
|
const index = state.packLists.findIndex(({name}) => name == data.name);
|
||||||
|
if (index > -1) {
|
||||||
|
state.packLists[index].progress = data.progress;
|
||||||
|
} else {
|
||||||
|
state.packLists.push(data);
|
||||||
|
$A.IDBSave("packLists", state.packLists, 600)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件
|
* 获取文件
|
||||||
* @param state
|
* @param state
|
||||||
@ -2781,7 +2799,7 @@ export default {
|
|||||||
state.dialogMsgs = state.dialogMsgs.filter(({dialog_id}) => dialog_id !== data.dialog_id)
|
state.dialogMsgs = state.dialogMsgs.filter(({dialog_id}) => dialog_id !== data.dialog_id)
|
||||||
$A.IDBSave("dialogMsgs", state.dialogMsgs, 600)
|
$A.IDBSave("dialogMsgs", state.dialogMsgs, 600)
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
data.pagesize = 25;
|
data.pagesize = 25;
|
||||||
//
|
//
|
||||||
dispatch("call", {
|
dispatch("call", {
|
||||||
@ -3379,6 +3397,9 @@ export default {
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch("forgetFile", data.id);
|
dispatch("forgetFile", data.id);
|
||||||
break;
|
break;
|
||||||
|
case 'compress':
|
||||||
|
dispatch("packProgress", data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})(msgDetail);
|
})(msgDetail);
|
||||||
break;
|
break;
|
||||||
|
|||||||
1
resources/assets/js/store/state.js
vendored
1
resources/assets/js/store/state.js
vendored
@ -118,6 +118,7 @@ export default {
|
|||||||
// 文件
|
// 文件
|
||||||
fileLists: [],
|
fileLists: [],
|
||||||
fileLinks: [],
|
fileLinks: [],
|
||||||
|
packLists: [],
|
||||||
|
|
||||||
// 项目任务
|
// 项目任务
|
||||||
projectId: 0,
|
projectId: 0,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user