feat: 文件分享查看链接

This commit is contained in:
kuaifan 2021-12-29 14:13:34 +08:00
parent fd6e7f3096
commit 53879fcefb
9 changed files with 507 additions and 40 deletions

View File

@ -6,6 +6,7 @@ namespace App\Http\Controllers\Api;
use App\Models\AbstractModel;
use App\Models\File;
use App\Models\FileContent;
use App\Models\FileLink;
use App\Models\FileUser;
use App\Models\User;
use App\Module\Base;
@ -79,15 +80,25 @@ class FileController extends AbstractController
/**
* 获取单条数据
*
* @apiParam {String} [code] 链接码(用于预览)
* @apiParam {Number} [id] 文件ID需要权限用于管理
*
* @return array
*/
public function one()
{
User::auth();
//
$id = intval(Request::input('id'));
//
$file = File::allowFind($id);
if (Request::exists("code")) {
$fileLink = FileLink::whereCode(Request::input('code'))->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
}
} else {
User::auth();
$id = intval(Request::input('id'));
$file = File::allowFind($id);
}
return Base::retSuccess('success', $file);
}
@ -292,13 +303,21 @@ class FileController extends AbstractController
/**
* 获取文件内容
*
* @apiParam {Number} id 文件ID
* @apiParam {String} [code] 链接码(用于预览)
* @apiParam {Number} [id] 文件ID需要权限用于管理
*/
public function content()
{
$id = intval(Request::input('id'));
//
$file = File::allowFind($id);
if (Request::exists("code")) {
$fileLink = FileLink::whereCode(Request::input('code'))->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
}
} else {
$id = intval(Request::input('id'));
$file = File::allowFind($id);
}
//
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
return FileContent::formatContent($file->type, $content ? $content->content : []);
@ -621,4 +640,48 @@ class FileController extends AbstractController
$file->setShare();
return Base::retSuccess("退出成功");
}
/**
* 获取链接
*
* @apiParam {Number} id 文件ID
* @apiParam {String} refresh 刷新链接
* - no: 只获取(默认)
* - yes: 刷新链接,之前的将失效
*/
public function link()
{
$user = User::auth();
//
$id = intval(Request::input('id'));
$refresh = Request::input('refresh', 'no');
//
$file = File::allowFind($id);
//
if ($file->userid != $user->userid) {
return Base::retError('仅限所有者操作');
}
if ($file->type == 'folder') {
return Base::retError('文件夹暂不支持此功能');
}
//
$fileLink = FileLink::whereFileId($file->id)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $file->id,
'code' => Base::generatePassword(64),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = Base::generatePassword(64);
$fileLink->save();
}
}
return Base::retSuccess('success', [
'id' => $file->id,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'num' => $fileLink->num
]);
}
}

View File

@ -165,6 +165,8 @@ class File extends AbstractModel
AbstractModel::transaction(function () {
$this->delete();
$this->pushMsg('delete');
FileLink::whereFileId($this->id)->delete();
FileUser::whereFileId($this->id)->delete();
FileContent::whereFid($this->id)->delete();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {

35
app/Models/FileLink.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace App\Models;
/**
* App\Models\FileLink
*
* @property int $id
* @property int|null $file_id 项目ID
* @property int|null $num 累计访问
* @property string|null $code 链接码
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\File|null $file
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUpdatedAt($value)
* @mixin \Eloquent
*/
class FileLink extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function file(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(File::class, 'id', 'file_id');
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFileLinksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('file_links', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('file_id')->nullable()->default(0)->comment('项目ID');
$table->integer('num')->nullable()->default(0)->comment('累计访问');
$table->string('code')->nullable()->default('')->comment('链接码');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('file_links');
}
}

View File

@ -0,0 +1,55 @@
<?php
use App\Models\File;
use App\Models\FileUser;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FileUsersAddPermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('file_users', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('file_users', 'permission')) {
$isAdd = true;
$table->tinyInteger('permission')->nullable()->default(0)->after('userid')->comment('权限0只读1读写');
}
});
if ($isAdd) {
// 更新数据
File::whereShare(1)->chunkById(100, function ($lists) {
foreach ($lists as $file) {
FileUser::updateInsert([
'file_id' => $file->id,
'userid' => 0,
]);
}
});
File::whereShare(2)->update([
'share' => 1,
]);
FileUser::wherePermission(0)->update([
'permission' => 1,
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_users', function (Blueprint $table) {
//
});
}
}

View File

@ -52,16 +52,16 @@ export default {
return {}
}
},
code: {
type: String,
default: ''
},
},
data() {
return {
loadIng: 0,
fileName: null,
fileType: null,
fileUrl: null,
docEditor: null,
}
},
@ -79,19 +79,29 @@ export default {
computed: {
...mapState(['userToken', 'userInfo']),
isPreview() {
return !!this.code
},
fileUrl() {
if (this.isPreview) {
return 'http://nginx/api/file/content/?code=' + this.code;
} else {
return 'http://nginx/api/file/content/?id=' + this.value.id + '&token=' + this.userToken;
}
},
fileType() {
return this.getType(this.value.type);
},
fileName() {
return this.value.name;
}
},
watch: {
value: {
handler(val) {
this.fileUrl = 'http://nginx/api/file/content/?id=' + val.id + '&token=' + this.userToken;
this.fileType = this.getType(val.type);
this.fileName = val.name;
},
immediate: true,
deep: true,
},
fileUrl: {
handler(url) {
if (!url) {
@ -164,6 +174,19 @@ export default {
"callbackUrl": 'http://nginx/api/file/content/office?id=' + this.value.id + '&token=' + this.userToken,
}
};
if (this.isPreview) {
config.editorConfig.mode = "view";
config.editorConfig.callbackUrl = null;
if (!config.editorConfig.user.id) {
let viewer = this.$store.state.method.getStorageInt("viewer")
if (!viewer) {
viewer = $A.randNum(1000, 99999);
this.$store.state.method.setStorage("viewer", viewer)
}
config.editorConfig.user.id = "viewer_" + viewer;
config.editorConfig.user.name = "Viewer_" + viewer
}
}
this.$nextTick(() => {
this.docEditor = new DocsAPI.DocEditor(this.id, config);
})

View File

@ -0,0 +1,172 @@
<template>
<div class="file-content">
<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">
{{formatName(file)}}
</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>
</div>
<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/>
</template>
<Flow v-else-if="file.type=='flow'" ref="myFlow" v-model="contentDetail" readOnly/>
<Minder v-else-if="file.type=='mind'" ref="myMind" v-model="contentDetail" readOnly/>
<LuckySheet v-else-if="file.type=='sheet'" ref="mySheet" v-model="contentDetail" readOnly/>
<OnlyOffice v-else-if="['word', 'excel', 'ppt'].includes(file.type)" v-model="contentDetail" :code="code"/>
</div>
</template>
<div v-if="loadContent > 0 || previewLoad" class="content-load"><Loading/></div>
</div>
</template>
<script>
import Vue from 'vue'
import Minder from '../../../components/minder'
Vue.use(Minder)
const MDPreview = () => import('../../../components/MDEditor/preview');
const TEditor = () => import('../../../components/TEditor');
const LuckySheet = () => import('../../../components/LuckySheet');
const Flow = () => import('../../../components/flow');
const OnlyOffice = () => import('../../../components/OnlyOffice');
export default {
name: "FilePreview",
components: {TEditor, MDPreview, LuckySheet, Flow, OnlyOffice},
props: {
code: {
type: String,
default: ''
},
file: {
type: Object,
default: () => {
return {};
}
},
},
data() {
return {
loadContent: 0,
loadIng: 0,
contentDetail: null,
loadPreview: true,
}
},
mounted() {
window.addEventListener('message', this.handleMessage)
},
beforeDestroy() {
window.removeEventListener('message', this.handleMessage)
},
watch: {
code: {
handler(code) {
if (code) {
this.contentDetail = null;
this.getContent();
}
},
immediate: true,
deep: true,
},
},
computed: {
isPreview() {
return this.contentDetail && this.contentDetail.preview === true;
},
previewLoad() {
return this.isPreview && this.loadPreview === true;
},
previewUrl() {
if (this.isPreview) {
return this.$store.state.method.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.contentDetail.url))
} else {
return '';
}
},
},
methods: {
handleMessage (event) {
const data = event.data;
switch (data.act) {
case 'ready':
this.loadPreview = false;
break
}
},
getContent() {
if (['word', 'excel', 'ppt'].includes(this.file.type)) {
this.contentDetail = $A.cloneJSON(this.file);
return;
}
this.loadIng++;
this.loadContent++;
this.$store.dispatch("call", {
url: 'file/content',
data: {
code: this.code,
},
}).then(({data}) => {
this.loadIng--;
this.loadContent--;
this.contentDetail = data.content;
}).catch(({msg}) => {
$A.modalError(msg);
this.loadIng--;
this.loadContent--;
})
},
exportMenu(act) {
switch (this.file.type) {
case 'mind':
this.$refs.myMind.exportHandle(act == 'pdf' ? 1 : 0, this.file.name);
break;
case 'flow':
this.$refs.myFlow[act == 'pdf' ? 'exportPDF' : 'exportPNG'](this.file.name, 3);
break;
case 'sheet':
this.$refs.mySheet.exportExcel(this.file.name, act);
break;
}
},
formatName({name, ext}) {
if (ext != '') {
name += "." + ext;
}
return name;
},
}
}
</script>

View File

@ -117,11 +117,11 @@
</DropdownMenu>
</Dropdown>
<DropdownItem @click.native="handleContextClick('rename')" divided>{{$L('重命名')}}</DropdownItem>
<DropdownItem @click.native="handleContextClick('copy')" :disabled="contextMenuItem.type=='folder'">{{$L('复制')}}</DropdownItem>
<DropdownItem @click.native="handleContextClick('copy')" :disabled="contextMenuItem.type == 'folder'">{{$L('复制')}}</DropdownItem>
<DropdownItem @click.native="handleContextClick('shear')" :disabled="contextMenuItem.userid != userId">{{$L('剪切')}}</DropdownItem>
<template v-if="contextMenuItem.userid == userId">
<DropdownItem @click.native="handleContextClick('share')" divided>{{$L('共享')}}</DropdownItem>
<DropdownItem @click.native="handleContextClick('share')">{{$L('链接')}}</DropdownItem>
<DropdownItem @click.native="handleContextClick('link')" :disabled="contextMenuItem.type == 'folder'">{{$L('链接')}}</DropdownItem>
</template>
<template v-else-if="contextMenuItem.share">
<DropdownItem @click.native="handleContextClick('outshare')" divided>{{$L('退出共享')}}</DropdownItem>
@ -252,6 +252,31 @@
</div>
</Modal>
<!--文件链接-->
<Modal
v-model="linkShow"
:title="$L('文件链接')"
:mask-closable="false">
<div>
<Input ref="linkInput" v-model="linkData.url" type="textarea" :rows="3" @on-focus="linkFocus" readonly/>
<div class="form-tip" style="padding-top:6px">{{$L('可通过此链接浏览文件。')}}</div>
</div>
<div slot="footer" class="adaption">
<Button type="default" @click="linkShow=false">{{$L('取消')}}</Button>
<Poptip
confirm
placement="bottom"
style="margin-left:8px"
@on-ok="linkGet(true)"
transfer>
<div slot="title">
<p><strong>{{$L('注意:刷新将导致原来的链接失效!')}}</strong></p>
</div>
<Button type="primary" :loading="linkLoad > 0">{{$L('刷新')}}</Button>
</Poptip>
</div>
</Modal>
<!--查看/修改文件-->
<DrawerOverlay
v-model="editShow"
@ -264,6 +289,10 @@
</template>
<script>
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
import {mapState} from "vuex";
import {sortBy} from "lodash";
import UserInput from "../../components/UserInput";
@ -348,6 +377,10 @@ export default {
shareList: [],
shareLoad: 0,
linkShow: false,
linkData: {},
linkLoad: 0,
editShow: false,
editInfo: {},
@ -825,6 +858,14 @@ export default {
});
break;
case 'link':
this.linkData = {
id: item.id
};
this.linkShow = true;
this.linkGet()
break;
case 'delete':
let typeName = item.type == 'folder' ? '文件夹' : '文件';
$A.modalConfirm({
@ -851,6 +892,42 @@ export default {
}
},
linkGet(refresh) {
this.linkLoad++;
this.$store.dispatch("call", {
url: 'file/link',
data: {
id: this.linkData.id,
refresh: refresh === true ? 'yes' : 'no'
},
}).then(({data}) => {
this.linkLoad--;
this.linkData = Object.assign(data, {
id: this.linkData.id
});
this.linkCopy();
}).catch(({msg}) => {
this.linkLoad--;
this.linkShow = false
$A.modalError(msg);
});
},
linkCopy() {
if (!this.linkData.url) {
return;
}
this.$copyText(this.linkData.url).then(() => {
$A.messageSuccess(this.$L('复制成功!'));
}, () => {
$A.messageError(this.$L('复制失败!'));
});
},
linkFocus() {
this.$refs.linkInput.focus({cursor:'all'});
},
shearTo() {
if (!this.shearFile) {
return;

View File

@ -1,8 +1,11 @@
<template>
<div class="electron-file">
<PageTitle :title="editInfo.name"/>
<PageTitle :title="fileInfo.name"/>
<Loading v-if="loadIng > 0"/>
<FileContent v-else v-model="editShow" :file="editInfo"/>
<template v-else>
<FilePreview v-if="fileCode" :code="fileCode" :file="fileInfo"/>
<FileContent v-else v-model="fileShow" :file="fileInfo"/>
</template>
</div>
</template>
@ -17,15 +20,17 @@
</style>
<script>
import FileContent from "../manage/components/FileContent";
import FilePreview from "../manage/components/FilePreview";
export default {
components: {FileContent},
components: {FilePreview, FileContent},
data() {
return {
loadIng: 0,
editShow: true,
editInfo: {},
fileShow: true,
fileInfo: {},
fileCode: null,
}
},
mounted() {
@ -41,27 +46,28 @@ export default {
},
methods: {
getInfo() {
let id = $A.runNum(this.$route.params.id);
if (id <= 0) {
return;
let id = this.$route.params.id;
let data = {};
if (id > 0) {
data.id = id;
this.fileCode = null;
} else if (id != '') {
data.code = id;
this.fileCode = id;
}
this.loadIng++;
this.$store.dispatch("call", {
url: 'file/one',
data: {
id,
},
data,
}).then(({data}) => {
this.loadIng--;
this.editInfo = data;
this.fileInfo = data;
}).catch(({msg}) => {
this.loadIng--;
$A.modalError({
content: msg,
onOk: () => {
if (this.$Electron) {
window.close();
}
window.close();
}
});
});