新增文件预览功能

This commit is contained in:
kuaifan 2021-12-10 17:05:16 +08:00
parent a303a60c4e
commit aa9a3238a2
11 changed files with 265 additions and 136 deletions

View File

@ -407,7 +407,8 @@ class FileController extends AbstractController
$path = 'uploads/office/' . date("Ym") . '/u' . $user->userid . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'office',
"type" => 'more',
"autoThumb" => false,
"path" => $path,
]);
if (Base::isError($data)) {
@ -415,36 +416,36 @@ class FileController extends AbstractController
}
$data = $data['data'];
//
$type = "";
switch ($data['ext']) {
case 'doc':
case 'docx':
$type = "word";
break;
case 'xls':
case 'xlsx':
$type = "excel";
break;
case 'ppt':
case 'pptx':
$type = "ppt";
break;
}
$type = match ($data['ext']) {
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'txt', 'html', 'htm', 'asp', 'jsp', 'xml', 'json', 'properties', 'md', 'gitignore', 'log', 'java', 'py', 'c', 'cpp', 'sql', 'sh', 'bat', 'm', 'bas', 'prg', 'cmd' => "text",
'jpg', 'jpeg', 'png', 'gif' => 'image',
'zip', 'rar', 'jar', 'tar', 'gzip' => 'compress',
'mp3', 'wav', 'mp4', 'flv' => 'media',
'pdf' => 'pdf',
'dwg' => 'cad',
default => "",
};
$file = File::createInstance([
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
'type' => $type,
'ext' => $data['ext'],
'userid' => $userid,
'created_id' => $user->userid,
]);
// 开始创建
return AbstractModel::transaction(function () use ($user, $data, $file) {
return AbstractModel::transaction(function () use ($type, $user, $data, $file) {
$file->save();
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path']
],
'text' => '',

View File

@ -10,18 +10,18 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class File
* App\Models\File
*
* @package App\Models
* @property int $id
* @property int|null $pid 上级ID
* @property int|null $cid 复制ID
* @property string|null $name 名称
* @property string|null $type 类型
* @property string|null $ext 后缀名
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享(1:共享所有人,2:指定成员)
* @property int|null $created_id 创建者ID
* @property int|null $share 是否共享
* @property int|null $created_id 创建者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@ -33,6 +33,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)

View File

@ -57,26 +57,28 @@ class FileContent extends AbstractModel
return Response::download(public_path($content['url']));
}
if (empty($content)) {
switch ($type) {
case 'document':
$content = [
"type" => "md",
"content" => "",
];
break;
case 'sheet':
$content = [
[
"name" => "Sheet1",
"config" => json_decode('{}'),
]
];
break;
default:
$content = json_decode('{}');
break;
$content = match ($type) {
'document' => [
"type" => "md",
"content" => "",
],
'sheet' => [
[
"name" => "Sheet1",
"config" => json_decode('{}'),
]
],
default => json_decode('{}'),
};
} else {
$content['preview'] = false;
if ($content['ext'] && !in_array($content['ext'], ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'])) {
$url = 'http://' . env('APP_IPPR') . '.3/' . $content['url'];
if ($type == 'image') {
$url = Base::fillUrl($content['url']);
}
$content['url'] = base64_encode($url);
$content['preview'] = true;
}
}
return Base::retSuccess('success', [ 'content' => $content ]);

View File

@ -2218,17 +2218,22 @@ class Base
case 'file':
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz'];
break;
case 'office':
$type = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
break;
case 'firmware':
$type = ['img', 'tar', 'bin'];
break;
case 'md':
$type = ['md'];
break;
case 'node_template':
$type = ['csv'];
case 'more':
$type = [
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'txt', 'html', 'htm', 'asp', 'jsp', 'xml', 'json', 'properties', 'md', 'gitignore', 'log', 'java', 'py', 'c', 'cpp', 'sql', 'sh', 'bat', 'm', 'bas', 'prg', 'cmd',
'jpg', 'jpeg', 'png', 'gif',
'zip', 'rar', 'jar', 'tar', 'gzip',
'mp3', 'wav', 'mp4', 'flv',
'pdf',
'dwg'
];
break;
default:
return Base::retError('错误的类型参数');

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FilesAddExt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('files', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('files', 'ext')) {
$isAdd = true;
$table->string('ext', 20)->nullable()->default('')->after('type')->comment('后缀名');
}
});
if ($isAdd) {
// 更新数据
\App\Models\File::chunkById(100, function ($lists) {
foreach ($lists as $item) {
if (in_array($item->type, ['word', 'excel', 'ppt'])) {
$item->ext = str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $item->type);
$item->save();
}
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('files', function (Blueprint $table) {
$table->dropColumn("ext");
});
}
}

View File

@ -41,11 +41,10 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.3"
depends_on:
- php
links:
- php
- office
- fileview
restart: unless-stopped
redis:
@ -95,6 +94,17 @@ services:
ipv4_address: "${APP_IPPR}.6"
restart: unless-stopped
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.1.0"
environment:
TZ: "Asia/Shanghai"
KK_CONTEXT_PATH: "/fileview"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.7"
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

@ -10,6 +10,10 @@ upstream office {
server office weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
upstream fileview {
server fileview:8012 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
@ -32,26 +36,12 @@ server {
allow all;
}
location ~* ^/(6.3.1-32|cache/files|web-apps/apps)/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://office;
}
location =/ws {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
@ -69,6 +59,8 @@ server {
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
@ -78,6 +70,41 @@ server {
proxy_set_header Server-Port $server_port;
proxy_pass http://service;
}
location /office/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $http_host/office;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://office/;
}
location /fileview {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://fileview;
}
}
include /etc/nginx/conf.d/conf.d/*.conf;

View File

@ -85,7 +85,7 @@ export default {
if (!url) {
return;
}
$A.loadScript(this.$store.state.method.apiUrl("../web-apps/apps/api/documents/api.js"), () => {
$A.loadScript(this.$store.state.method.apiUrl("../office/web-apps/apps/api/documents/api.js"), () => {
this.loadFile()
})
},

View File

@ -1,64 +1,67 @@
<template>
<div class="file-content">
<div v-show="!['word', 'excel', 'ppt'].includes(file.type)" class="edit-header">
<div class="header-title">
<EPopover v-if="!equalContent" v-model="unsaveTip" class="file-unsave-tip">
<div class="task-detail-delete-file-popover">
<p>{{$L('未保存当前修改内容?')}}</p>
<div class="buttons">
<Button size="small" type="text" @click="unsaveGive">{{$L('放弃')}}</Button>
<Button size="small" type="primary" @click="unsaveSave">{{$L('保存')}}</Button>
<iframe v-if="isPreview" ref="myPreview" class="preview-iframe" :src="previewUrl"></iframe>
<template v-else>
<div v-show="!['word', 'excel', 'ppt'].includes(file.type)" class="edit-header">
<div class="header-title">
<EPopover v-if="!equalContent" v-model="unsaveTip" class="file-unsave-tip">
<div class="task-detail-delete-file-popover">
<p>{{$L('未保存当前修改内容?')}}</p>
<div class="buttons">
<Button size="small" type="text" @click="unsaveGive">{{$L('放弃')}}</Button>
<Button size="small" type="primary" @click="unsaveSave">{{$L('保存')}}</Button>
</div>
</div>
</div>
<span slot="reference">[{{$L('未保存')}}*]</span>
</EPopover>
{{formatName(file.name, file.type)}}
<span slot="reference">[{{$L('未保存')}}*]</span>
</EPopover>
{{formatName(file.name, file.type)}}
</div>
<div class="header-user">
<ul>
<li v-for="(userid, index) in editUser" :key="index" v-if="index <= 10">
<UserAvatar :userid="userid" :size="28" :border-witdh="2"/>
</li>
<li v-if="editUser.length > 10" class="more">{{editUser.length > 99 ? '99+' : editUser.length}}</li>
</ul>
</div>
<div v-if="file.type=='document' && contentDetail" class="header-hint">
<ButtonGroup size="small" shape="circle">
<Button :type="`${contentDetail.type=='md'?'primary':'default'}`" @click="$set(contentDetail, 'type', 'md')">{{$L('MD编辑器')}}</Button>
<Button :type="`${contentDetail.type!='md'?'primary':'default'}`" @click="$set(contentDetail, 'type', 'text')">{{$L('文本编辑器')}}</Button>
</ButtonGroup>
</div>
<div v-if="file.type=='mind'" class="header-hint">
{{$L('选中节点按enter键添加同级节点tab键添加子节点')}}
</div>
<Dropdown v-if="file.type=='mind' || file.type=='flow' || file.type=='sheet'"
trigger="click"
class="header-hint"
@on-click="exportMenu">
<a href="javascript:void(0)">{{$L('导出')}}<Icon type="ios-arrow-down"></Icon></a>
<DropdownMenu v-if="file.type=='sheet'" slot="list">
<DropdownItem name="xlsx">{{$L('导出XLSX')}}</DropdownItem>
<DropdownItem name="xlml">{{$L('导出XLS')}}</DropdownItem>
<DropdownItem name="csv">{{$L('导出CSV')}}</DropdownItem>
<DropdownItem name="txt">{{$L('导出TXT')}}</DropdownItem>
</DropdownMenu>
<DropdownMenu v-else slot="list">
<DropdownItem name="png">{{$L('导出PNG图片')}}</DropdownItem>
<DropdownItem name="pdf">{{$L('导出PDF文件')}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<Button v-if="!file.only_view" :disabled="equalContent" :loading="loadIng > 0" class="header-button" size="small" type="primary" @click="handleClick('save')">{{$L('保存')}}</Button>
</div>
<div class="header-user">
<ul>
<li v-for="(userid, index) in editUser" :key="index" v-if="index <= 10">
<UserAvatar :userid="userid" :size="28" :border-witdh="2"/>
</li>
<li v-if="editUser.length > 10" class="more">{{editUser.length > 99 ? '99+' : editUser.length}}</li>
</ul>
<div v-if="contentDetail" class="content-body">
<template v-if="file.type=='document'">
<MDEditor v-if="contentDetail.type=='md'" v-model="contentDetail.content" height="100%"/>
<TEditor v-else v-model="contentDetail.content" height="100%" @editorSave="handleClick('saveBefore')"/>
</template>
<Flow v-else-if="file.type=='flow'" ref="myFlow" v-model="contentDetail" @saveData="handleClick('saveBefore')"/>
<Minder v-else-if="file.type=='mind'" ref="myMind" v-model="contentDetail" @saveData="handleClick('saveBefore')"/>
<LuckySheet v-else-if="file.type=='sheet'" ref="mySheet" v-model="contentDetail"/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" v-model="contentDetail"/>
</div>
<div v-if="file.type=='document' && contentDetail" class="header-hint">
<ButtonGroup size="small" shape="circle">
<Button :type="`${contentDetail.type=='md'?'primary':'default'}`" @click="$set(contentDetail, 'type', 'md')">{{$L('MD编辑器')}}</Button>
<Button :type="`${contentDetail.type!='md'?'primary':'default'}`" @click="$set(contentDetail, 'type', 'text')">{{$L('文本编辑器')}}</Button>
</ButtonGroup>
</div>
<div v-if="file.type=='mind'" class="header-hint">
{{$L('选中节点按enter键添加同级节点tab键添加子节点')}}
</div>
<Dropdown v-if="file.type=='mind' || file.type=='flow' || file.type=='sheet'"
trigger="click"
class="header-hint"
@on-click="exportMenu">
<a href="javascript:void(0)">{{$L('导出')}}<Icon type="ios-arrow-down"></Icon></a>
<DropdownMenu v-if="file.type=='sheet'" slot="list">
<DropdownItem name="xlsx">{{$L('导出XLSX')}}</DropdownItem>
<DropdownItem name="xlml">{{$L('导出XLS')}}</DropdownItem>
<DropdownItem name="csv">{{$L('导出CSV')}}</DropdownItem>
<DropdownItem name="txt">{{$L('导出TXT')}}</DropdownItem>
</DropdownMenu>
<DropdownMenu v-else slot="list">
<DropdownItem name="png">{{$L('导出PNG图片')}}</DropdownItem>
<DropdownItem name="pdf">{{$L('导出PDF文件')}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<Button v-if="!file.only_view" :disabled="equalContent" :loading="loadIng > 0" class="header-button" size="small" type="primary" @click="handleClick('save')">{{$L('保存')}}</Button>
</div>
<div v-if="contentDetail" class="content-body">
<template v-if="file.type=='document'">
<MDEditor v-if="contentDetail.type=='md'" v-model="contentDetail.content" height="100%"/>
<TEditor v-else v-model="contentDetail.content" height="100%" @editorSave="handleClick('saveBefore')"/>
</template>
<Flow v-else-if="file.type=='flow'" ref="myFlow" v-model="contentDetail" @saveData="handleClick('saveBefore')"/>
<Minder v-else-if="file.type=='mind'" ref="myMind" v-model="contentDetail" @saveData="handleClick('saveBefore')"/>
<LuckySheet v-else-if="file.type=='sheet'" ref="mySheet" v-model="contentDetail"/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" v-model="contentDetail"/>
</div>
</template>
<div v-if="loadContent > 0" class="content-load"><Loading/></div>
</div>
</template>
@ -166,6 +169,18 @@ export default {
equalContent() {
return this.contentBak == $A.jsonStringify(this.contentDetail);
},
isPreview() {
return this.contentDetail && this.contentDetail.preview === true;
},
previewUrl() {
if (this.isPreview) {
return this.$store.state.method.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.contentDetail.url))
} else {
return '';
}
},
},
methods: {

View File

@ -85,7 +85,7 @@
@on-enter="onEnter(item)"/>
<div v-if="item._load" class="file-load"><Loading/></div>
</div>
<div v-else class="file-name" :title="item.name">{{formatName(item.name, item.type)}}</div>
<div v-else class="file-name" :title="item.name">{{formatName(item)}}</div>
</li>
</ul>
</div>
@ -199,7 +199,7 @@
v-model="editShow"
class="page-file-drawer"
:mask-closable="false">
<FileContent v-if="editShowNum > 0" :parent-show="editShow" :file="editInfo"/>
<FileContent v-if="editNum > 0" :parent-show="editShow" :file="editInfo"/>
</DrawerOverlay>
</div>
@ -290,13 +290,21 @@ export default {
shareLoad: 0,
editShow: false,
editShowNum: 0,
editNum: 0,
editInfo: {},
uploadDir: false,
uploadIng: 0,
uploadFormat: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
uploadAccept: ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'].join(","),
uploadFormat: [
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'txt', 'html', 'htm', 'asp', 'jsp', 'xml', 'json', 'properties', 'md', 'gitignore', 'log', 'java', 'py', 'c', 'cpp', 'sql', 'sh', 'bat', 'm', 'bas', 'prg', 'cmd',
'jpg', 'jpeg', 'png', 'gif',
'zip', 'rar', 'jar', 'tar', 'gzip',
'mp3', 'wav', 'mp4', 'flv',
'pdf',
'dwg'
],
uploadAccept: '',
maxSize: 204800,
contextMenuItem: {},
@ -310,6 +318,9 @@ export default {
mounted() {
this.tableHeight = window.innerHeight - 160;
this.uploadAccept = this.uploadFormat.map(item => {
return '.' + item
}).join(",");
},
activated() {
@ -381,7 +392,7 @@ export default {
editShow(val) {
if (val) {
this.editShowNum++;
this.editNum++;
this.$store.dispatch("websocketPath", "file/content/" + this.editInfo.id);
} else {
this.$store.dispatch("websocketPath", "file");
@ -397,7 +408,6 @@ export default {
title: this.$L('文件名'),
key: 'name',
minWidth: 200,
resizable: true,
sortable: true,
render: (h, {row}) => {
let array = [];
@ -467,7 +477,7 @@ export default {
}
}
}, [
h('AutoTip', this.formatName(row.name, row.type))
h('AutoTip', this.formatName(row))
]));
//
const iconArray = [];
@ -556,13 +566,9 @@ export default {
]
},
formatName(name, type) {
if (type == 'word') {
name += ".docx";
} else if (type == 'excel') {
name += ".xlsx";
} else if (type == 'ppt') {
name += ".pptx";
formatName({name, ext}) {
if (ext != '') {
name += "." + ext;
}
return name;
},

View File

@ -9,6 +9,21 @@
border-radius: 18px 18px 0 0;
overflow: hidden;
.preview-iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: 0 0;
border: 0;
float: none;
margin: -1px 0 0;
max-width: none;
outline: 0;
padding: 0;
}
.edit-header {
display: flex;
flex-direction: row;