mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-13 12:02:51 +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\Module\Base;
|
||||
use App\Module\Ihttp;
|
||||
use Swoole\Coroutine;
|
||||
use Carbon\Carbon;
|
||||
use Redirect;
|
||||
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身份
|
||||
* @apiVersion 1.0.0
|
||||
* @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 {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function download__zip()
|
||||
public function download__pack()
|
||||
{
|
||||
$user = User::auth();
|
||||
$ids = Request::input('ids');
|
||||
$downName = Request::input('name');
|
||||
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
abort(403, "Please select the file or folder to download.");
|
||||
@ -1053,9 +1056,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
// 下载文件名
|
||||
$downName = count($ids) > 1 ? 'file_'. date("YmdHis") : $files[0]->name;
|
||||
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName . '.zip';
|
||||
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
||||
$zipPath = storage_path('app/'.$zipName);
|
||||
Base::makeDir(dirname($zipPath));
|
||||
|
||||
@ -1063,12 +1064,56 @@ class FileController extends AbstractController
|
||||
abort(403, "Failed to create compressed file.");
|
||||
}
|
||||
|
||||
array_walk($files, function($file) use ($zip) {
|
||||
File::addFileTreeToZip($zip, $file);
|
||||
go(function() use ($zip, $files, $downName) {
|
||||
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
|
||||
* @return void
|
||||
*/
|
||||
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 = Base::json2array($content?->content ?: []);
|
||||
$typeExtensions = [
|
||||
@ -892,7 +892,7 @@ class File extends AbstractModel
|
||||
if (array_key_exists($file->type, $typeExtensions)) {
|
||||
$filePath = empty($content) ? public_path('assets/office/empty.' . $typeExtensions[$file->type]) : public_path($content['url']);
|
||||
}
|
||||
|
||||
//
|
||||
$relativePath = $file->path . '.' . $file->ext;
|
||||
if (file_exists($filePath)) {
|
||||
$zip->addFile($filePath, $relativePath);
|
||||
@ -917,4 +917,31 @@ class File extends AbstractModel
|
||||
$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>
|
||||
<div v-if="loadIng == 0" class="file-refresh" @click="getFileList"><i class="taskfont"></i></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')"/>
|
||||
<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 :class="['file-search', searchKey ? 'has-value' : '']" @click="onSearchFocus" @mouseenter="onSearchFocus">
|
||||
<Input v-model="searchKey" ref="searchInput" suffix="ios-search" @on-change="onSearchChange" :placeholder="$L('搜索名称')"/>
|
||||
@ -240,6 +244,24 @@
|
||||
</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
|
||||
name="files"
|
||||
@ -407,6 +429,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import {mapState} from "vuex";
|
||||
import {sortBy} from "lodash";
|
||||
import DrawerOverlay from "../../components/DrawerOverlay";
|
||||
@ -423,6 +446,9 @@ export default {
|
||||
directives: {longpress},
|
||||
data() {
|
||||
return {
|
||||
packList: [],
|
||||
packShow: false,
|
||||
|
||||
loadIng: 0,
|
||||
searchKey: '',
|
||||
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){
|
||||
if (ids.length === 0) {
|
||||
return
|
||||
@ -1427,26 +1512,26 @@ export default {
|
||||
const allFolder = !ids.some(id => this.fileLists.some(({ type, id: itemId }) => type !== 'folder' && itemId === id));
|
||||
const typeName = allFolder ? "文件夹" : "文件";
|
||||
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({
|
||||
title: '打包下载',
|
||||
content: `你确定要打包下载${fileName}吗?`,
|
||||
okText: '确定',
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$store.dispatch("call", {
|
||||
onOk: async () => {
|
||||
try {
|
||||
const { msg } = await this.$store.dispatch("call", {
|
||||
url: 'file/download/check',
|
||||
data: {
|
||||
ids,
|
||||
},
|
||||
}).then(({msg}) => {
|
||||
const idsParam = ids.join('&ids[]=');
|
||||
this.$store.dispatch('downUrl', $A.apiUrl(`file/download/zip?ids[]=${idsParam}`));
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg)
|
||||
data: { ids },
|
||||
});
|
||||
})
|
||||
|
||||
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() {
|
||||
//上传前判断
|
||||
this.uploadShow = true;
|
||||
this.packShow = false;
|
||||
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
|
||||
@ -2781,7 +2799,7 @@ export default {
|
||||
state.dialogMsgs = state.dialogMsgs.filter(({dialog_id}) => dialog_id !== data.dialog_id)
|
||||
$A.IDBSave("dialogMsgs", state.dialogMsgs, 600)
|
||||
}
|
||||
//
|
||||
//
|
||||
data.pagesize = 25;
|
||||
//
|
||||
dispatch("call", {
|
||||
@ -3379,6 +3397,9 @@ export default {
|
||||
case 'delete':
|
||||
dispatch("forgetFile", data.id);
|
||||
break;
|
||||
case 'compress':
|
||||
dispatch("packProgress", data);
|
||||
break;
|
||||
}
|
||||
})(msgDetail);
|
||||
break;
|
||||
|
||||
1
resources/assets/js/store/state.js
vendored
1
resources/assets/js/store/state.js
vendored
@ -118,6 +118,7 @@ export default {
|
||||
// 文件
|
||||
fileLists: [],
|
||||
fileLinks: [],
|
||||
packLists: [],
|
||||
|
||||
// 项目任务
|
||||
projectId: 0,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user