perf: 文件、聊天文件、任务文件预览优化(支持预览drawio、mind等)

This commit is contained in:
kuaifan 2022-02-25 09:07:49 +08:00
parent 14006068c8
commit 8d2ee364ba
12 changed files with 243 additions and 176 deletions

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Models\File;
use App\Models\ProjectTask;
use App\Models\ProjectTaskFile;
use App\Models\User;
@ -393,31 +394,13 @@ class DialogController extends AbstractController
$data = $dialogMsg->toArray();
//
if ($data['type'] == 'file') {
$codeExt = ['txt'];
$officeExt = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
$localExt = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw', 'tif', 'tiff', 'mp3', 'wav', 'mp4', 'flv', 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm'];
$msg = Base::json2array($dialogMsg->getRawOriginal('msg'));
$filePath = public_path($msg['path']);
if (in_array($msg['ext'], $codeExt) && $msg['size'] < 2 * 1024 * 1024) {
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($filePath);
$data['file_mode'] = 1;
} elseif (in_array($msg['ext'], $officeExt)) {
// office预览
$data['file_mode'] = 2;
} else {
// 其他预览
if (in_array($msg['ext'], $localExt)) {
$url = Base::fillUrl($msg['path']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $msg['path'];
}
$data['url'] = base64_encode($url);
$data['file_mode'] = 3;
}
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
}
//
return Base::retSuccess("success", $data);
return Base::retSuccess('success', $data);
}
/**

View File

@ -516,40 +516,41 @@ class FileController extends AbstractController
}
}
//
$contentArray = Base::json2array($content);
switch ($file->type) {
case 'document':
$file->ext = $contentArray['type'] ?: 'md';
$contentArray = Base::json2array($content);
$contentString = $contentArray['content'];
$file->ext = $contentArray['type'] == 'md' ? 'md' : 'text';
break;
case 'drawio':
$file->ext = 'drawio';
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
break;
case 'code':
case 'txt':
$contentString = $content;
break;
default:
return Base::retError('参数错误');
}
if (isset($contentString)) {
$path = "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $id . "/" . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
$content = [
'type' => $file->ext,
'url' => $path
];
$size = filesize($save);
} else {
$size = strlen($content);
}
$path = "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $id . "/" . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'content' => [
'type' => $file->ext,
'url' => $path
],
'text' => $text,
'size' => $size,
'size' => filesize($save),
'userid' => $user->userid,
]);
$content->save();

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\File;
use App\Models\Project;
use App\Models\ProjectColumn;
use App\Models\ProjectFlow;
@ -1132,29 +1133,7 @@ class ProjectController extends AbstractController
//
ProjectTask::userTask($file->task_id, null);
//
$codeExt = ['txt'];
$officeExt = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
$localExt = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw', 'tif', 'tiff', 'mp3', 'wav', 'mp4', 'flv', 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm'];
$filePath = public_path($data['path']);
if (in_array($data['ext'], $codeExt) && $data['size'] < 2 * 1024 * 1024) {
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($filePath);
$data['file_mode'] = 1;
} elseif (in_array($data['ext'], $officeExt)) {
// office预览
$data['file_mode'] = 2;
} else {
// 其他预览
if (in_array($data['ext'], $localExt)) {
$url = Base::fillUrl($data['path']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $data['path'];
}
$data['url'] = base64_encode($url);
$data['file_mode'] = 3;
}
//
return Base::retSuccess('success', $data);
return Base::retSuccess('success', File::formatFileData($data));
}
/**

View File

@ -50,6 +50,39 @@ class File extends AbstractModel
{
use SoftDeletes;
/**
* 文件文件
*/
const codeExt = [
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx'
];
/**
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
];
/**
* 是否有访问权限
* @param $userid
@ -239,4 +272,76 @@ class File extends AbstractModel
}
return $file;
}
/**
* 格式化内容数据
* @param array $data [path, ext, size, name]
* @return array
*/
public static function formatFileData(array $data)
{
$filePath = $data['path'];
$fileExt = $data['ext'];
$fileSize = $data['size'];
$fileName = $data['name'];
$publicPath = public_path($filePath);
//
switch ($fileExt) {
case 'md':
case 'text':
// 文本
$data['content'] = [
'type' => $fileExt,
'content' => file_get_contents($publicPath),
];
$data['file_mode'] = $fileExt;
break;
case 'drawio':
// 图表
$data['content'] = [
'xml' => file_get_contents($publicPath)
];
$data['file_mode'] = $fileExt;
break;
case 'mind':
// 思维导图
$data['content'] = Base::json2array(file_get_contents($publicPath));
$data['file_mode'] = $fileExt;
break;
default:
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
{
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($publicPath);
$data['file_mode'] = 'code';
}
elseif (in_array($fileExt, File::officeExt))
{
// office预览
$data['content'] = '';
$data['file_mode'] = 'office';
}
else
{
// 其他预览
if (in_array($fileExt, File::localExt)) {
$url = Base::fillUrl($filePath);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $filePath;
}
$data['content'] = [
'preview' => true,
'url' => base64_encode(Base::urlAddparameter($url, [
'fullfilename' => $fileName
])),
];
$data['file_mode'] = 'preview';
}
break;
}
return $data;
}
}

View File

@ -69,48 +69,20 @@ class FileContent extends AbstractModel
abort(403, "This file is empty.");
}
} else {
$content['preview'] = false;
$path = $content['url'];
if ($file->ext) {
$filePath = public_path($content['url']);
$fileType = $file->type;
if ($fileType == 'document')
{
// 文本
$content = [
'type' => $file->ext,
'content' => file_get_contents($filePath)
];
}
elseif ($fileType == 'drawio')
{
// 图表
$content = [
'xml' => file_get_contents($filePath)
];
}
elseif ($fileType == 'mind')
{
// 思维导图
$content = Base::json2array(file_get_contents($filePath));
}
elseif (in_array($fileType, ['txt', 'code']) && $file->size < 2 * 1024 * 1024)
{
// 其他文本和代码限制2M内的文件支持编辑
$content['content'] = file_get_contents($filePath);
}
else
{
// 支持预览
if (in_array($fileType, ['picture', 'image', 'tif', 'media'])) {
$url = Base::fillUrl($content['url']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $content['url'];
}
$content['url'] = base64_encode($url);
$content['preview'] = true;
}
$res = File::formatFileData([
'path' => $path,
'ext' => $file->ext,
'size' => $file->size,
'name' => $file->name,
]);
$content = $res['content'];
} else {
$content['preview'] = false;
}
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Response::download($filePath, $name);
} else {

View File

@ -812,6 +812,31 @@ class Base
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
}
/**
* 地址后拼接参数
* @param $url
* @param $parames
* @return mixed|string
*/
public static function urlAddparameter($url, $parames)
{
if ($parames && is_array($parames)) {
$array = [];
foreach ($parames as $key => $val) {
$array[] = $key . "=" . $val;
}
if ($array) {
$query = implode("&", $array);
if (str_contains($url, "?")) {
$url .= "&" . $query;
} else {
$url .= "?" . $query;
}
}
}
return $url;
}
/**
* 格式化内容图片地址
* @param $content

View File

@ -216,6 +216,7 @@ export default {
this.$Electron.sendMessage('windowRouter', {
name: 'file-msg-' + this.msgData.id,
path: "/single/file/msg/" + this.msgData.id,
userAgent: "/hideenOfficeTitle/",
force: false,
config: {
title: `${this.msgData.msg.name} (${$A.bytesToSize(this.msgData.msg.size)})`,

View File

@ -52,8 +52,8 @@
</template>
<Drawio v-else-if="file.type=='drawio'" ref="myFlow" v-model="contentDetail" :title="file.name" @saveData="handleClick('saveBefore')"/>
<Minder v-else-if="file.type=='mind'" ref="myMind" v-model="contentDetail" @saveData="handleClick('saveBefore')"/>
<AceEditor v-else-if="['code', 'txt'].includes(file.type)" v-model="contentDetail" :ext="file.ext" @saveData="handleClick('saveBefore')"/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" v-model="contentDetail" :documentKey="documentKey"/>
<AceEditor v-else-if="['code', 'txt'].includes(file.type)" v-model="contentDetail.content" :ext="file.ext" @saveData="handleClick('saveBefore')"/>
</div>
</template>
<div v-if="contentLoad" class="content-load"><Loading/></div>

View File

@ -25,12 +25,12 @@
<div v-if="contentDetail" class="content-body">
<template v-if="file.type=='document'">
<MDPreview v-if="contentDetail.type=='md'" :initialValue="contentDetail.content"/>
<TEditor v-else v-model="contentDetail.content" height="100%" readOnly/>
<TEditor v-else :value="contentDetail.content" height="100%" readOnly/>
</template>
<Drawio v-else-if="file.type=='drawio'" ref="myFlow" v-model="contentDetail" :title="file.name" readOnly/>
<Minder v-else-if="file.type=='mind'" ref="myMind" v-model="contentDetail" readOnly/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" v-model="contentDetail" :code="code" :documentKey="documentKey" readOnly/>
<AceEditor v-else-if="['code', 'txt'].includes(file.type)" v-model="contentDetail.content" :ext="file.ext" readOnly/>
<Drawio v-else-if="file.type=='drawio'" ref="myFlow" :value="contentDetail" :title="file.name" readOnly/>
<Minder v-else-if="file.type=='mind'" ref="myMind" :value="contentDetail" readOnly/>
<AceEditor v-else-if="['code', 'txt'].includes(file.type)" :value="contentDetail" :ext="file.ext" readOnly/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" :value="contentDetail" :code="code" :documentKey="documentKey" readOnly/>
</div>
</template>
<div v-if="contentLoad" class="content-load"><Loading/></div>

View File

@ -1164,6 +1164,7 @@ export default {
this.$Electron.sendMessage('windowRouter', {
name: 'file-task-' + file.id,
path: "/single/file/task/" + file.id,
userAgent: "/hideenOfficeTitle/",
force: false,
config: {
title: `${file.name} (${$A.bytesToSize(file.size)})`,

View File

@ -3,9 +3,13 @@
<PageTitle :title="title"/>
<Loading v-if="loadIng > 0"/>
<template v-else>
<AceEditor v-if="isCode" v-model="codeContent" :ext="codeExt" class="view-editor" readOnly/>
<OnlyOffice v-else-if="isOffice" v-model="officeContent" :code="officeCode" :documentKey="documentKey" readOnly/>
<iframe v-else-if="isPreview" class="preview-iframe" :src="previewUrl"/>
<MDPreview v-if="isType('md')" :initialValue="msgDetail.content.content"/>
<TEditor v-else-if="isType('text')" :value="msgDetail.content.content" height="100%" readOnly/>
<Drawio v-else-if="isType('drawio')" v-model="msgDetail.content" :title="msgDetail.msg.name" readOnly/>
<Minder v-else-if="isType('mind')" :value="msgDetail.content" readOnly/>
<AceEditor v-else-if="isType('code')" v-model="msgDetail.content" :ext="msgDetail.msg.ext" class="view-editor" readOnly/>
<OnlyOffice v-else-if="isType('office')" v-model="officeContent" :code="officeCode" :documentKey="documentKey" readOnly/>
<iframe v-else-if="isType('preview')" class="preview-iframe" :src="previewUrl"/>
<div v-else class="no-support">{{$L('不支持单独查看此消息')}}</div>
</template>
</div>
@ -17,6 +21,8 @@
align-items: center;
.preview-iframe,
.ace_editor,
.markdown-preview-warp,
.teditor-wrapper,
.no-support {
position: absolute;
top: 0;
@ -42,13 +48,27 @@
}
</style>
<style lang="scss">
.single-file-msg {
.teditor-wrapper {
.teditor-box {
height: 100%;
}
}
}
</style>
<script>
import AceEditor from "../../components/AceEditor";
import OnlyOffice from "../../components/OnlyOffice";
import Vue from 'vue'
import Minder from '../../components/minder'
Vue.use(Minder)
const MDPreview = () => import('../../components/MDEditor/preview');
const TEditor = () => import('../../components/TEditor');
const AceEditor = () => import('../../components/AceEditor');
const OnlyOffice = () => import('../../components/OnlyOffice');
const Drawio = () => import('../../components/Drawio');
export default {
components: {OnlyOffice, AceEditor},
components: {AceEditor, TEditor, MDPreview, OnlyOffice, Drawio},
data() {
return {
loadIng: 0,
@ -80,47 +100,27 @@ export default {
return "Loading..."
},
isCode() {
return this.msgDetail.type == 'file' && this.msgDetail.file_mode == 1;
},
codeContent() {
if (this.isCode) {
return this.msgDetail.content;
isType() {
const {msgDetail} = this;
return function (type) {
return msgDetail.type == 'file' && msgDetail.file_mode == type;
}
return '';
},
codeExt() {
if (this.isCode) {
return this.msgDetail.msg.ext;
}
return 'txt'
},
isOffice() {
return this.msgDetail.type == 'file' && this.msgDetail.file_mode == 2;
},
officeContent() {
return {
id: this.isOffice ? this.msgDetail.id : 0,
id: this.msgDetail.id || 0,
type: this.msgDetail.msg.ext,
name: this.title,
}
},
officeCode() {
if (this.isOffice) {
return "msgFile_" + this.msgDetail.id;
}
return ''
return "msgFile_" + this.msgDetail.id;
},
isPreview() {
return this.msgDetail.type == 'file' && this.msgDetail.file_mode == 3;
},
previewUrl() {
if (this.isPreview) {
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.msgDetail.url))
}
return ''
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.msgDetail.url))
}
},
methods: {

View File

@ -3,9 +3,13 @@
<PageTitle :title="title"/>
<Loading v-if="loadIng > 0"/>
<template v-else>
<AceEditor v-if="isCode" v-model="codeContent" :ext="codeExt" class="view-editor" readOnly/>
<OnlyOffice v-else-if="isOffice" v-model="officeContent" :code="officeCode" :documentKey="documentKey" readOnly/>
<iframe v-else-if="isPreview" class="preview-iframe" :src="previewUrl"/>
<MDPreview v-if="isType('md')" :initialValue="fileDetail.content.content"/>
<TEditor v-else-if="isType('text')" :value="fileDetail.content.content" height="100%" readOnly/>
<Drawio v-else-if="isType('drawio')" v-model="fileDetail.content" :title="fileDetail.name" readOnly/>
<Minder v-else-if="isType('mind')" :value="fileDetail.content" readOnly/>
<AceEditor v-else-if="isType('code')" v-model="fileDetail.content" :ext="fileDetail.ext" class="view-editor" readOnly/>
<OnlyOffice v-else-if="isType('office')" v-model="officeContent" :code="officeCode" :documentKey="documentKey" readOnly/>
<iframe v-else-if="isType('preview')" class="preview-iframe" :src="previewUrl"/>
<div v-else class="no-support">{{$L('不支持单独查看此消息')}}</div>
</template>
</div>
@ -17,6 +21,8 @@
align-items: center;
.preview-iframe,
.ace_editor,
.markdown-preview-warp,
.teditor-wrapper,
.no-support {
position: absolute;
top: 0;
@ -42,13 +48,27 @@
}
</style>
<style lang="scss">
.single-file-task {
.teditor-wrapper {
.teditor-box {
height: 100%;
}
}
}
</style>
<script>
import AceEditor from "../../components/AceEditor";
import OnlyOffice from "../../components/OnlyOffice";
import Vue from 'vue'
import Minder from '../../components/minder'
Vue.use(Minder)
const MDPreview = () => import('../../components/MDEditor/preview');
const TEditor = () => import('../../components/TEditor');
const AceEditor = () => import('../../components/AceEditor');
const OnlyOffice = () => import('../../components/OnlyOffice');
const Drawio = () => import('../../components/Drawio');
export default {
components: {OnlyOffice, AceEditor},
components: {AceEditor, TEditor, MDPreview, OnlyOffice, Drawio},
data() {
return {
loadIng: 0,
@ -80,47 +100,27 @@ export default {
return "Loading..."
},
isCode() {
return this.fileDetail.file_mode == 1;
},
codeContent() {
if (this.isCode) {
return this.fileDetail.content;
isType() {
const {fileDetail} = this;
return function (type) {
return fileDetail.file_mode == type;
}
return '';
},
codeExt() {
if (this.isCode) {
return this.fileDetail.ext;
}
return 'txt'
},
isOffice() {
return this.fileDetail.file_mode == 2;
},
officeContent() {
return {
id: this.isOffice ? this.fileDetail.id : 0,
id: this.fileDetail.id || 0,
type: this.fileDetail.ext,
name: this.title,
}
},
officeCode() {
if (this.isOffice) {
return "taskFile_" + this.fileDetail.id;
}
return ''
return "taskFile_" + this.fileDetail.id;
},
isPreview() {
return this.fileDetail.file_mode == 3;
},
previewUrl() {
if (this.isPreview) {
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.fileDetail.url))
}
return ''
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.fileDetail.url))
}
},
methods: {