【升级】新增在线文档示例功能

pull/24/head
小诺 2021-07-12 20:29:11 +08:00 committed by 小诺
parent 178fb8e23b
commit 47121ebcd0
20 changed files with 1465 additions and 6 deletions

View File

@ -579,6 +579,7 @@ INSERT INTO `sys_menu` VALUES (1264622039642256571, 1264622039642256531, '[0],[1
INSERT INTO `sys_menu` VALUES (1264622039642256581, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '文件上传', 'sys_file_mgr_sys_file_upload', 2, NULL, NULL, NULL, 'sysFileInfo:upload', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:34:29', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256581, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '文件上传', 'sys_file_mgr_sys_file_upload', 2, NULL, NULL, NULL, 'sysFileInfo:upload', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:34:29', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256591, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '文件下载', 'sys_file_mgr_sys_file_download', 2, NULL, NULL, NULL, 'sysFileInfo:download', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:34:55', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256591, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '文件下载', 'sys_file_mgr_sys_file_download', 2, NULL, NULL, NULL, 'sysFileInfo:download', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:34:55', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256601, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '图片预览', 'sys_file_mgr_sys_file_preview', 2, NULL, NULL, NULL, 'sysFileInfo:preview', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:35:19', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256601, 1264622039642256531, '[0],[1264622039642256521],[1264622039642256531],', '图片预览', 'sys_file_mgr_sys_file_preview', 2, NULL, NULL, NULL, 'sysFileInfo:preview', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-06-24 17:35:19', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256602, 1264622039642256521, '[0],[1264622039642256521],', '在线文档', 'sys_file_mgr_sys_online_file', 1, NULL, '/fileOnline', '/system/file/onlineIndex', NULL, 'system', 1, 'Y', NULL, NULL, 1, 21, NULL, 0, '2020-06-24 17:32:57', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256611, 0, '[0],', '定时任务', 'sys_timers', 0, 'dashboard', '/timers', 'PageView', NULL, 'system', 1, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-07-01 17:17:20', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256611, 0, '[0],', '定时任务', 'sys_timers', 0, 'dashboard', '/timers', 'PageView', NULL, 'system', 1, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-07-01 17:17:20', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256621, 1264622039642256611, '[0],[1264622039642256611],', '任务管理', 'sys_timers_mgr', 1, NULL, '/timers', 'system/timers/index', NULL, 'system', 1, 'Y', NULL, NULL, 1, 22, NULL, 0, '2020-07-01 17:18:53', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256621, 1264622039642256611, '[0],[1264622039642256611],', '任务管理', 'sys_timers_mgr', 1, NULL, '/timers', 'system/timers/index', NULL, 'system', 1, 'Y', NULL, NULL, 1, 22, NULL, 0, '2020-07-01 17:18:53', 1265476890672672808, NULL, NULL);
INSERT INTO `sys_menu` VALUES (1264622039642256631, 1264622039642256621, '[0],[1264622039642256611],[1264622039642256621],', '定时任务查询', 'sys_timers_mgr_page', 2, NULL, NULL, NULL, 'sysTimers:page', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-07-01 17:19:43', 1265476890672672808, NULL, NULL); INSERT INTO `sys_menu` VALUES (1264622039642256631, 1264622039642256621, '[0],[1264622039642256611],[1264622039642256621],', '定时任务查询', 'sys_timers_mgr_page', 2, NULL, NULL, NULL, 'sysTimers:page', 'system', 0, 'Y', NULL, NULL, 1, 100, NULL, 0, '2020-07-01 17:19:43', 1265476890672672808, NULL, NULL);

View File

@ -6,11 +6,14 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>logo.png"> <link rel="icon" href="<%= BASE_URL %>logo.png">
<title>Snowy</title> <title>Snowy</title>
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style> <style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
<script type="text/javascript" src="https://onlyoffice.xiaonuo.vip/web-apps/apps/api/documents/api.js"></script>
<!-- require cdn assets css --> <!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" /> <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %> <% } %>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@ -99,3 +99,17 @@ export function sysFileInfoDelete (parameter) {
data: parameter data: parameter
}) })
} }
/**
* 线
*
* @author yubaoshan
* @date 2020/6/30 00:20
*/
export function sysFileInfoGetOnlineConfig (parameter) {
return axios({
url: '/sysFileInfo/getOnlineFileConfig',
method: 'get',
params: parameter
})
}

View File

@ -0,0 +1,95 @@
/* eslint-disable no-undef */
<!--onlyoffice 编辑器-->
<template>
<div id="editorDiv"></div>
</template>
<script>
import { handleDocType } from '@/utils/onlyofficeUtil'
export default {
name: 'Editor',
props: {
option: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
doctype: ''
}
},
mounted() {
if (this.option.url) {
this.setEditor(this.option)
}
},
methods: {
setEditor(option) {
this.doctype = handleDocType(option.fileType)
const config = {
document: {
fileType: option.fileType,
key: option.key,
title: option.title,
permissions: {
comment: true,
download: true,
modifyContentControl: true,
modifyFilter: true,
print: false,
edit: option.isEdit,
fillForms: true
// review: false
},
url: option.url
},
type: option.type,
documentType: this.doctype,
editorConfig: {
callbackUrl: option.callbackUrl,
lang: 'zh',
customization: {
commentAuthorOnly: false,
comments: true,
compactHeader: false,
compactToolbar: true,
feedback: false,
plugins: true
},
user: {
id: option.user.id,
name: option.user.name
}
// mode: option.mode
},
width: '100%',
height: '100%',
position: 'absolute',
token: option.token
}
// eslint-disable-next-line no-unused-vars
let docEditor = null
// eslint-disable-next-line no-undef
docEditor = new DocsAPI.DocEditor('editorDiv', config)
}
},
watch: {
option: {
handler: function (n, o) {
this.setEditor(n)
this.doctype = handleDocType(n.fileType)
},
deep: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,22 @@
export function handleDocType(fileType) {
let docType = ''
const fileTypesDoc = [
'doc', 'docm', 'docx', 'dot', 'dotm', 'dotx', 'epub', 'fodt', 'htm', 'html', 'mht', 'odt', 'ott', 'pdf', 'rtf', 'txt', 'djvu', 'xps'
]
const fileTypesCsv = [
'csv', 'fods', 'ods', 'ots', 'xls', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx'
]
const fileTypesPPt = [
'fodp', 'odp', 'otp', 'pot', 'potm', 'potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx'
]
if (fileTypesDoc.includes(fileType)) {
docType = 'text'
}
if (fileTypesCsv.includes(fileType)) {
docType = 'spreadsheet'
}
if (fileTypesPPt.includes(fileType)) {
docType = 'presentation'
}
return docType
}

View File

@ -0,0 +1,99 @@
<template>
<a-modal
title="文件信息详情"
:footer="null"
:width="900"
:visible="visible"
:confirmLoading="confirmLoading"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form :form="form">
<a-form-item v-show="false">
<a-input v-decorator="['id']" />
</a-form-item>
<a-form-item
label="文件存储位置"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
{{ fileDetail.fileLocation }}
</a-form-item>
<a-form-item
label="文件仓库"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
{{ fileDetail.fileBucket }}
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="文件名称"
>
{{ fileDetail.fileOriginName }}
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="文件后缀"
>
{{ fileDetail.fileSuffix }}
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="文件大小"
>
{{ fileDetail.fileSizeKb }}
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="唯一标识"
>
{{ fileDetail.fileObjectName }}
</a-form-item>
<a-form-item
label="存储路径"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
{{ fileDetail.filePath }}
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script>
export default {
data () {
return {
labelCol: {
xs: { span: 24 },
sm: { span: 8 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 15 }
},
fileDetail: [],
visible: false,
confirmLoading: false,
form: this.$form.createForm(this)
}
},
methods: {
//
detail (record) {
this.fileDetail = record
this.visible = true
},
handleCancel () {
this.form.resetFields()
this.visible = false
}
}
}
</script>

View File

@ -0,0 +1,251 @@
<template>
<a-spin :spinning="cardLoading">
<x-card v-if="hasPerm('sysFileInfo:page')">
<div slot="content" class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col :md="8" :sm="24">
<a-form-item label="存储位置">
<a-select v-model="queryParam.fileLocation" placeholder="请选择存储位置" >
<a-select-option v-for="(item,index) in fileLocationDictTypeDropDown" :key="index" :value="item.code" >{{ item.name }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="文件仓库">
<a-input v-model="queryParam.fileBucket" placeholder="请输入文件仓库"/>
</a-form-item>
</a-col>
<template v-if="advanced">
<a-col :md="8" :sm="24">
<a-form-item label="文件名称">
<a-input v-model="queryParam.fileOriginName" placeholder="请输入文件名称(上传时候的文件名)"/>
</a-form-item>
</a-col>
</template>
<a-col :md="!advanced && 8 || 24" :sm="24">
<span class="table-page-search-submitButtons" :style="advanced && { float: 'right', overflow: 'hidden' } || {} ">
<a-button type="primary" @click="$refs.table.refresh(true)" >查询</a-button>
<a-button style="margin-left: 8px" @click="() => queryParam = { fileSuffix: 'doc,docx,xls,xlsx,ppt,pptx' }">重置</a-button>
<a @click="toggleAdvanced" style="margin-left: 8px">
{{ advanced ? '收起' : '展开' }}
<a-icon :type="advanced ? 'up' : 'down'"/>
</a>
</span>
</a-col>
</a-row>
</a-form>
</div>
</x-card>
<a-card :bordered="false">
<s-table
ref="table"
:columns="columns"
:data="loadData"
:alert="false"
:rowKey="(record) => record.id"
:rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
>
<span slot="fileOriginName" slot-scope="text">
<ellipsis :length="10" tooltip>{{ text }}</ellipsis>
</span>
<span slot="fileObjectName" slot-scope="text">
<ellipsis :length="10" tooltip>{{ text }}</ellipsis>
</span>
<span slot="fileLocation" slot-scope="text">
{{ 'file_storage_location' | dictType(text) }}
</span>
<span slot="fileSuffix" slot-scope="text">
<a-tag color="blue">{{ text }}</a-tag>
</span>
<span slot="action" slot-scope="text, record">
<a @click="onlineEdit(record)">线</a>
<a-divider type="vertical"/>
<a v-if="hasPerm('sysFileInfo:download')" @click="sysFileInfoDownload(record)"></a>
<a-divider type="vertical" v-if="hasPerm('sysFileInfo:download') & hasPerm('sysFileInfo:preview')"/>
<a v-if="hasPerm('sysFileInfo:preview')" @click="onlinePreview(record, 'desktop')"></a>
<a-divider type="vertical" v-if="hasPerm('sysFileInfo:preview')"/>
<a v-if="hasPerm('sysFileInfo:preview')" @click="onlinePreview(record, 'mobile')"></a>
<a-divider type="vertical" v-if="hasPerm('sysFileInfo:preview') & hasPerm('sysFileInfo:delete')"/>
<a-popconfirm v-if="hasPerm('sysFileInfo:delete')" placement="topRight" title="确认删除?" @confirm="() => sysFileInfoDelete(record)">
<a>删除</a>
</a-popconfirm>
</span>
</s-table>
<preview-form ref="previewForm"/>
<online-edit-form ref="onlineEditForm"/>
</a-card>
</a-spin>
</template>
<script>
import { STable, Ellipsis, XCard } from '@/components'
import { sysFileInfoPage, sysFileInfoDelete, sysFileInfoDownload, sysFileInfoGetOnlineConfig } from '@/api/modular/system/fileManage'
import previewForm from './previewForm'
import onlineEditForm from './onlineEditForm'
export default {
components: {
XCard,
STable,
Ellipsis,
previewForm,
onlineEditForm
},
data () {
return {
// /
advanced: false,
//
queryParam: { fileSuffix: 'doc,docx,xls,xlsx,ppt,pptx' },
//
columns: [
{
title: '存储位置',
dataIndex: 'fileLocation',
scopedSlots: { customRender: 'fileLocation' }
},
{
title: '文件仓库',
dataIndex: 'fileBucket'
},
{
title: '文件名称',
dataIndex: 'fileOriginName',
scopedSlots: { customRender: 'fileOriginName' }
},
{
title: '文件后缀',
dataIndex: 'fileSuffix',
scopedSlots: { customRender: 'fileSuffix' }
},
{
title: '文件大小',
dataIndex: 'fileSizeInfo'
},
{
title: '唯一标识id',
dataIndex: 'fileObjectName',
scopedSlots: { customRender: 'fileObjectName' }
}
],
// Promise
loadData: parameter => {
return sysFileInfoPage(Object.assign(parameter, this.queryParam)).then((res) => {
return res.data
})
},
cardLoading: false,
fileLocationDictTypeDropDown: [],
selectedRowKeys: [],
selectedRows: []
}
},
created () {
this.sysDictTypeDropDown()
if (this.hasPerm('sysPos:edit') || this.hasPerm('sysPos:delete')) {
this.columns.push({
title: '操作',
width: '350px',
dataIndex: 'action',
scopedSlots: { customRender: 'action' }
})
}
},
methods: {
/**
* 在线编辑
*/
onlineEdit (record) {
this.cardLoading = true
sysFileInfoGetOnlineConfig({ id: record.id }).then((res) => {
this.cardLoading = false
this.$refs.onlineEditForm.onlineEdit(res, 'desktop')
})
},
/**
* 在线预览
*/
onlinePreview (record, type) {
this.cardLoading = true
sysFileInfoGetOnlineConfig({ id: record.id }).then((res) => {
this.cardLoading = false
this.$refs.previewForm.preview(res, type)
})
},
/**
* 预览文件微软插件
*/
previewMicrosoft (record) {
window.open('https://view.officeapps.live.com/op/view.aspx?src=' + process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/download?id=' + record.id)
},
/**
* 获取字典数据
*/
sysDictTypeDropDown () {
this.fileLocationDictTypeDropDown = this.$options.filters['dictData']('file_storage_location')
},
/**
* 下载文件所有文件
*/
sysFileInfoDownload (record) {
this.cardLoading = true
sysFileInfoDownload({ id: record.id }).then((res) => {
this.cardLoading = false
this.downloadfile(res)
// eslint-disable-next-line handle-callback-err
}).catch((err) => {
this.cardLoading = false
this.$message.error('下载错误:获取文件流错误')
})
},
downloadfile (res) {
var blob = new Blob([res.data], { type: 'application/octet-stream;charset=UTF-8' })
var contentDisposition = res.headers['content-disposition']
var patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*')
var result = patt.exec(contentDisposition)
var filename = result[1]
var downloadElement = document.createElement('a')
var href = window.URL.createObjectURL(blob) //
var reg = /^["](.*)["]$/g
downloadElement.style.display = 'none'
downloadElement.href = href
downloadElement.download = decodeURI(filename.replace(reg, '$1')) //
document.body.appendChild(downloadElement)
downloadElement.click() //
document.body.removeChild(downloadElement) //
window.URL.revokeObjectURL(href)
},
sysFileInfoDelete (record) {
sysFileInfoDelete(record).then((res) => {
if (res.success) {
this.$message.success('删除成功')
this.$refs.table.refresh()
} else {
this.$message.error('删除失败:' + res.message)
}
}).catch((err) => {
this.$message.error('删除错误:' + err.message)
})
},
toggleAdvanced () {
this.advanced = !this.advanced
},
handleOk () {
this.$refs.table.refresh()
},
onSelectChange (selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
this.selectedRows = selectedRows
}
}
}
</script>
<style lang="less">
.table-operator {
margin-bottom: 18px;
}
button {
margin-right: 8px;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<a-modal
title="在线编辑"
:footer="null"
:width="1500"
:visible="visible"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-spin :spinning="divLoading">
<div class="editorview" style="height: 800px;">
<Editor :option="option"/>
</div>
</a-spin>
</a-modal>
</template>
<script>
import Editor from '../../../components/xnComponents/EditorDiv'
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
export default {
components: {
Editor
},
data () {
return {
visible: false,
divLoading: false,
sysOnlineFileInfoResult: {},
option: {
url: '',
isEdit: true,
fileType: '',
title: '',
token: Vue.ls.get(ACCESS_TOKEN),
user: {
id: '',
name: ''
},
mode: '',
callbackUrl: '',
key: '',
review: false,
type: 'desktop'
}
}
},
methods: {
/**
* 初始化
*/
onlineEdit(record) {
this.visible = true
const data = record.data.sysOnlineFileInfoResult
this.option.user.id = data.editorConfig.user.id
this.option.user.name = data.editorConfig.user.name
this.option.fileType = data.document.fileType
this.option.title = data.document.title
this.option.key = data.document.key
this.option.url = process.env.VUE_APP_API_BASE_URL + data.document.url // res.data.docServiceApiUrl
this.callbackUrl = process.env.VUE_APP_API_BASE_URL + data.editorConfig.callbackUrl
// this.option.type = type
this.option.review = false
},
handleCancel () {
this.visible = false
this.option = {
url: '',
isEdit: false,
fileType: '',
title: '',
token: Vue.ls.get(ACCESS_TOKEN),
user: {
id: '',
name: ''
},
mode: '',
callbackUrl: '',
key: '',
review: false
}
}
}
}
</script>
<style>
.editorview iframe{
position: absolute !important;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<a-modal
title="在线预览"
:footer="null"
:width="1500"
:visible="visible"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-spin :spinning="divLoading">
<div class="editorview" style="height: 800px;">
<Editor :option="option"/>
</div>
</a-spin>
</a-modal>
</template>
<script>
import Editor from '../../../components/xnComponents/EditorDiv'
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
export default {
components: {
Editor
},
data () {
return {
visible: false,
divLoading: false,
sysOnlineFileInfoResult: {},
option: {
url: '',
isEdit: false,
fileType: '',
title: '',
token: Vue.ls.get(ACCESS_TOKEN),
user: {
id: '',
name: ''
},
mode: '',
callbackUrl: '',
key: '',
review: false
}
}
},
created () {
},
methods: {
/**
* 初始化
*/
preview(record, type) {
this.visible = true
const data = record.data.sysOnlineFileInfoResult
this.option.user.id = '1265476890672672808' // data.editorConfig.user.id
this.option.user.name = '超级管理员' // data.editorConfig.user.name
this.option.fileType = data.document.fileType
this.option.title = data.document.title
this.option.key = data.document.key
this.option.url = process.env.VUE_APP_API_BASE_URL + data.document.url // res.data.docServiceApiUrl
this.callbackUrl = process.env.VUE_APP_API_BASE_URL + data.editorConfig.callbackUrl
this.option.type = type
},
handleCancel () {
this.visible = false
this.option = {
url: '',
isEdit: false,
fileType: '',
title: '',
token: Vue.ls.get(ACCESS_TOKEN),
user: {
id: '',
name: ''
},
mode: '',
callbackUrl: '',
key: '',
review: false
}
const oScript = document.createElement('script')
oScript.type = 'text/javascript'
oScript.src = ''
document.body.appendChild(oScript)
}
}
}
</script>
<style>
.editorview iframe{
position: absolute !important;
}
</style>

View File

@ -311,6 +311,16 @@ public class ConstantContextHolder {
return getSysConfig("SNOWY_ALIPAY_RETURN_URL", String.class, true); return getSysConfig("SNOWY_ALIPAY_RETURN_URL", String.class, true);
} }
/**
* OnlyOffice
*
* @author xuyuxiang
* @date 2020/7/29 14:08
**/
public static String getOnlyOfficeUrl() {
return getSysConfig("SNOWY_ONLY_OFFICE_SERVICE_URL", String.class, true);
}
/** /**
* config * config
* *

View File

@ -25,6 +25,7 @@ Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意
package vip.xiaonuo.core.file.modular.local; package vip.xiaonuo.core.file.modular.local;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil; import cn.hutool.system.SystemUtil;
import vip.xiaonuo.core.file.FileOperator; import vip.xiaonuo.core.file.FileOperator;
@ -47,6 +48,8 @@ public class LocalFileOperator implements FileOperator {
private String currentSavePath = ""; private String currentSavePath = "";
private Dict localClient;
public LocalFileOperator(LocalFileProperties localFileProperties) { public LocalFileOperator(LocalFileProperties localFileProperties) {
this.localFileProperties = localFileProperties; this.localFileProperties = localFileProperties;
initClient(); initClient();
@ -67,6 +70,9 @@ public class LocalFileOperator implements FileOperator {
} }
currentSavePath = savePathLinux; currentSavePath = savePathLinux;
} }
localClient = Dict.create();
localClient.put("currentSavePath", currentSavePath);
localClient.put("localFileProperties", localFileProperties);
} }
@Override @Override
@ -77,7 +83,7 @@ public class LocalFileOperator implements FileOperator {
@Override @Override
public Object getClient() { public Object getClient() {
// empty // empty
return null; return localClient;
} }
@Override @Override

View File

@ -24,15 +24,21 @@ Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意
*/ */
package vip.xiaonuo.sys.modular.file.controller; package vip.xiaonuo.sys.modular.file.controller;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import vip.xiaonuo.core.annotion.BusinessLog; import vip.xiaonuo.core.annotion.BusinessLog;
import vip.xiaonuo.core.annotion.Permission; import vip.xiaonuo.core.annotion.Permission;
import vip.xiaonuo.core.context.constant.ConstantContextHolder;
import vip.xiaonuo.core.enums.LogAnnotionOpTypeEnum; import vip.xiaonuo.core.enums.LogAnnotionOpTypeEnum;
import vip.xiaonuo.core.pojo.response.ResponseData; import vip.xiaonuo.core.pojo.response.ResponseData;
import vip.xiaonuo.core.pojo.response.SuccessResponseData; import vip.xiaonuo.core.pojo.response.SuccessResponseData;
import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam; import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam;
import vip.xiaonuo.sys.modular.file.result.SysOnlineFileInfoResult;
import vip.xiaonuo.sys.modular.file.service.SysFileInfoService; import vip.xiaonuo.sys.modular.file.service.SysFileInfoService;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -50,6 +56,27 @@ public class SysFileInfoController {
@Resource @Resource
private SysFileInfoService sysFileInfoService; private SysFileInfoService sysFileInfoService;
/**
* onlyoffice
*/
public static final String ONLY_OFFICE_APP_JS_SUFFIX = "/web-apps/apps/api/documents/api.js";
/**
* 线
*
* @author xuyuxiang
* @date 2020/11/17 16:40
*/
@GetMapping("/sysFileInfo/getOnlineFileConfig")
public ResponseData getOnlineFileConfig(SysFileInfoParam sysFileInfoParam) {
//生成在线文档的model
SysOnlineFileInfoResult sysOnlineFileInfoResult = sysFileInfoService.onlineAddOrUpdate(sysFileInfoParam);
Dict dict = Dict.create();
dict.put("docServiceApiUrl", ConstantContextHolder.getOnlyOfficeUrl() + ONLY_OFFICE_APP_JS_SUFFIX);
dict.put("sysOnlineFileInfoResult", sysOnlineFileInfoResult);
return new SuccessResponseData(dict);
}
/** /**
* *
* *
@ -139,4 +166,15 @@ public class SysFileInfoController {
return new SuccessResponseData(); return new SuccessResponseData();
} }
/**
* 线
*
* @author xuyuxiang
* @date 2021/3/25 16:06
*/
@ResponseBody
@PostMapping("/sysFileInfo/track")
public void track() {
sysFileInfoService.track();
}
} }

View File

@ -71,7 +71,22 @@ public enum SysFileInfoExceptionEnum implements AbstractBaseExceptionEnum {
/** /**
* *
*/ */
PREVIEW_ERROR_LIBREOFFICE(7, "预览文件异常请检查LibreOffice是否启动"); PREVIEW_ERROR_LIBREOFFICE(7, "预览文件异常请检查LibreOffice是否启动"),
/**
*
*/
CLIENT_INIT_ERROR(8, "文件操作客户端初始化异常"),
/**
* 线
*/
ONLINE_EDIT_SUPPORT_LOCAL_ONLY(9, "在线文档暂时只支持本地文件"),
/**
* 线
*/
ONLINE_EDIT_PARAM_ERROR(10, "在线文档参数错误");
private final Integer code; private final Integer code;

View File

@ -33,7 +33,7 @@ import lombok.Getter;
* @date 2020/6/7 22:24 * @date 2020/6/7 22:24
*/ */
@Getter @Getter
public enum FileLocationEnum { public enum SysFileLocationEnum {
/** /**
* *
@ -57,7 +57,7 @@ public enum FileLocationEnum {
private final Integer code; private final Integer code;
FileLocationEnum(int code) { SysFileLocationEnum(int code) {
this.code = code; this.code = code;
} }

View File

@ -28,6 +28,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import vip.xiaonuo.core.pojo.base.param.BaseParam; import vip.xiaonuo.core.pojo.base.param.BaseParam;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
/** /**
@ -76,6 +77,7 @@ public class SysFileInfoParam extends BaseParam {
/** /**
* bucketid * bucketid
*/ */
@NotEmpty(message = "存储到bucket的名称不能为空请检查fileObjectName参数", groups = {trace.class})
private String fileObjectName; private String fileObjectName;
/** /**
@ -83,4 +85,19 @@ public class SysFileInfoParam extends BaseParam {
*/ */
private String filePath; private String filePath;
/**
*
*/
private Boolean sample = false;
/**
* edit view
*/
private String mode;
/**
* desktop mobile
*/
private String type;
} }

View File

@ -0,0 +1,187 @@
/*
Copyright [2020] [https://www.xiaonuo.vip]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SnowyAPACHE LICENSE 2.0使
1.LICENSE
2.Snowy
3.
4. https://gitee.com/xiaonuobase/snowy-layui
5. https://gitee.com/xiaonuobase/snowy-layui
6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.file.result;
import cn.hutool.core.io.FileUtil;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.Data;
import vip.xiaonuo.core.consts.CommonConstant;
import vip.xiaonuo.core.consts.SymbolConstant;
import vip.xiaonuo.sys.modular.file.util.OnlineDocumentUtil;
import java.io.File;
import java.util.HashMap;
/**
* 线
*
* @author yubaoshan
* @date 2020/6/7 22:15
*/
@Data
public class SysOnlineFileInfoResult {
public String fileId;
public String[] history;
public String type = "desktop";
public String mode = "edit";
public String documentType;
public Document document;
public EditorConfig editorConfig;
public String token;
public static class Document {
public String title;
public String url;
public String fileType;
public String key;
public Permissions permissions;
}
public static class Permissions {
public Boolean comment;
public Boolean download;
public Boolean edit;
public Boolean fillForms;
public Boolean modifyFilter;
public Boolean modifyContentControl;
public Boolean review;
public Permissions(String mode, String type, Boolean canEdit) {
comment = !mode.equals("view") && !mode.equals("fillForms") && !mode.equals("embedded") && !mode.equals("blockcontent");
download = true;
edit = canEdit && (mode.equals("edit") || mode.equals("filter") || mode.equals("blockcontent"));
fillForms = !mode.equals("view") && !mode.equals("comment") && !mode.equals("embedded") && !mode.equals("blockcontent");
modifyFilter = !mode.equals("filter");
modifyContentControl = !mode.equals("blockcontent");
review = mode.equals("edit") || mode.equals("review");
}
}
public static class EditorConfig {
public HashMap<String, Object> actionLink = null;
public String mode = "edit";
public String callbackUrl;
public String lang = "en";
public Integer forcesavetype = 1;
public User user;
public Customization customization;
public Embedded embedded;
public EditorConfig(String actionData) {
if (actionData != null) {
Gson gson = new Gson();
actionLink = gson.fromJson(actionData, new TypeToken<HashMap<String, Object>>() { }.getType());
}
user = new User();
customization = new Customization();
}
public void InitDesktop(String url) {
embedded = new Embedded();
embedded.saveUrl = url;
embedded.embedUrl = url;
embedded.shareUrl = url;
embedded.toolbarDocked = "top";
}
public static class User {
public String id = "-1";
public String name = CommonConstant.UNKNOWN;
}
public static class Customization {
public GoBack goback;
public Boolean forcesave = true;
public Customization()
{
goback = new GoBack();
}
public class GoBack {
public String url;
}
}
public static class Embedded {
public String saveUrl;
public String embedUrl;
public String shareUrl;
public String toolbarDocked;
}
}
public SysOnlineFileInfoResult(String fileId, String fileOriginName, String userId, String userName) {
if (fileOriginName == null) fileOriginName = "";
fileOriginName = fileOriginName.trim();
documentType = OnlineDocumentUtil.getFileType(fileOriginName).toLowerCase();
this.fileId = fileId;
document = new Document();
document.title = fileOriginName;
document.url = OnlineDocumentUtil.getFileUri(fileId, fileOriginName);
document.fileType = FileUtil.getSuffix(fileOriginName);
document.key = GenerateRevisionId(fileOriginName + SymbolConstant.PERIOD + new File(OnlineDocumentUtil.getStoragePath(fileId + SymbolConstant.PERIOD + FileUtil.getSuffix(fileOriginName))).lastModified());
editorConfig = new EditorConfig(null);
editorConfig.callbackUrl = OnlineDocumentUtil.getCallback(fileId, document.fileType);
editorConfig.lang = "zh";
if (userId != null) editorConfig.user.id = userId;
if (userName != null) editorConfig.user.name = userName;
editorConfig.customization.goback.url = "";
changeType(mode, type);
}
public void changeType(String _mode, String _type) {
if (_mode != null) mode = _mode;
if (_type != null) type = _type;
Boolean canEdit = OnlineDocumentUtil.getEditedSuffix().contains(SymbolConstant.PERIOD + FileUtil.getSuffix(document.title));
editorConfig.mode = canEdit && !mode.equals("view") ? "edit" : "view";
document.permissions = new Permissions(mode, type, canEdit);
if (type.equals("embedded")) InitDesktop();
}
public static String GenerateRevisionId(String expectedKey) {
if (expectedKey.length() > 20)
expectedKey = Integer.toString(expectedKey.hashCode());
String key = expectedKey.replace("[^0-9-.a-zA-Z_=]", "_");
return key.substring(0, Math.min(key.length(), 20));
}
public void InitDesktop() {
editorConfig.InitDesktop(document.url);
}
}

View File

@ -30,6 +30,7 @@ import vip.xiaonuo.core.pojo.page.PageResult;
import vip.xiaonuo.sys.modular.file.entity.SysFileInfo; import vip.xiaonuo.sys.modular.file.entity.SysFileInfo;
import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam; import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam;
import vip.xiaonuo.sys.modular.file.result.SysFileInfoResult; import vip.xiaonuo.sys.modular.file.result.SysFileInfoResult;
import vip.xiaonuo.sys.modular.file.result.SysOnlineFileInfoResult;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.List; import java.util.List;
@ -147,4 +148,21 @@ public interface SysFileInfoService extends IService<SysFileInfo> {
* @date 2020/7/7 12:09 * @date 2020/7/7 12:09
*/ */
void download(SysFileInfoParam sysFileInfoParam, HttpServletResponse response); void download(SysFileInfoParam sysFileInfoParam, HttpServletResponse response);
/**
* 线
*
* @param sysFileInfoParam
* @author xuyuxiang
* @date 2021/3/24 10:02
*/
SysOnlineFileInfoResult onlineAddOrUpdate(SysFileInfoParam sysFileInfoParam);
/**
* 线
*
* @author xuyuxiang
* @date 2021/3/25 15:48
*/
void track();
} }

View File

@ -26,12 +26,16 @@ package vip.xiaonuo.sys.modular.file.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log; import cn.hutool.log.Log;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -39,30 +43,43 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import vip.xiaonuo.core.consts.CommonConstant;
import vip.xiaonuo.core.consts.SymbolConstant; import vip.xiaonuo.core.consts.SymbolConstant;
import vip.xiaonuo.core.context.login.LoginContextHolder;
import vip.xiaonuo.core.context.requestno.RequestNoContext; import vip.xiaonuo.core.context.requestno.RequestNoContext;
import vip.xiaonuo.core.exception.LibreOfficeException; import vip.xiaonuo.core.exception.LibreOfficeException;
import vip.xiaonuo.core.exception.ServiceException; import vip.xiaonuo.core.exception.ServiceException;
import vip.xiaonuo.core.factory.PageFactory; import vip.xiaonuo.core.factory.PageFactory;
import vip.xiaonuo.core.file.FileOperator; import vip.xiaonuo.core.file.FileOperator;
import vip.xiaonuo.core.file.modular.local.LocalFileOperator;
import vip.xiaonuo.core.pojo.login.SysLoginUser;
import vip.xiaonuo.core.pojo.page.PageResult; import vip.xiaonuo.core.pojo.page.PageResult;
import vip.xiaonuo.core.util.HttpServletUtil;
import vip.xiaonuo.core.util.LibreOfficeUtil; import vip.xiaonuo.core.util.LibreOfficeUtil;
import vip.xiaonuo.sys.modular.file.entity.SysFileInfo; import vip.xiaonuo.sys.modular.file.entity.SysFileInfo;
import vip.xiaonuo.sys.modular.file.enums.FileLocationEnum; import vip.xiaonuo.sys.modular.file.enums.SysFileLocationEnum;
import vip.xiaonuo.sys.modular.file.enums.SysFileInfoExceptionEnum; import vip.xiaonuo.sys.modular.file.enums.SysFileInfoExceptionEnum;
import vip.xiaonuo.sys.modular.file.mapper.SysFileInfoMapper; import vip.xiaonuo.sys.modular.file.mapper.SysFileInfoMapper;
import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam; import vip.xiaonuo.sys.modular.file.param.SysFileInfoParam;
import vip.xiaonuo.sys.modular.file.result.SysFileInfoResult; import vip.xiaonuo.sys.modular.file.result.SysFileInfoResult;
import vip.xiaonuo.sys.modular.file.result.SysOnlineFileInfoResult;
import vip.xiaonuo.sys.modular.file.service.SysFileInfoService; import vip.xiaonuo.sys.modular.file.service.SysFileInfoService;
import vip.xiaonuo.sys.modular.file.util.DownloadUtil; import vip.xiaonuo.sys.modular.file.util.DownloadUtil;
import vip.xiaonuo.sys.modular.file.util.OnlineDocumentUtil;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Scanner;
import static vip.xiaonuo.sys.config.FileConfig.DEFAULT_BUCKET; import static vip.xiaonuo.sys.config.FileConfig.DEFAULT_BUCKET;
@ -80,6 +97,46 @@ public class SysFileInfoServiceImpl extends ServiceImpl<SysFileInfoMapper, SysFi
@Resource @Resource
private FileOperator fileOperator; private FileOperator fileOperator;
@Override
public SysOnlineFileInfoResult onlineAddOrUpdate(SysFileInfoParam sysFileInfoParam) {
if(fileOperator instanceof LocalFileOperator) {
//文件后缀
String fileSuffix = sysFileInfoParam.getFileSuffix();
//文件名称
String fileOriginName = sysFileInfoParam.getFileOriginName();
//文件id
Long id = sysFileInfoParam.getId();
//参数错误
if(ObjectUtil.isAllEmpty(fileSuffix, fileOriginName, id)) {
throw new ServiceException(SysFileInfoExceptionEnum.ONLINE_EDIT_PARAM_ERROR);
}
//获取登录用户
SysLoginUser sysLoginUser = LoginContextHolder.me().getSysLoginUser();
SysFileInfo sysFileInfo;
SysOnlineFileInfoResult sysOnlineFileInfoResult;
//文件id不为空则表示编辑
if(ObjectUtil.isNotEmpty(id)) {
sysFileInfo = this.getById(id);
sysOnlineFileInfoResult= new SysOnlineFileInfoResult(Convert.toStr(sysFileInfo.getId()), sysFileInfo.getFileOriginName(), Convert.toStr(sysLoginUser.getId()), sysLoginUser.getName());
} else {
//否则表示新增
Boolean sample = sysFileInfoParam.getSample();
sysFileInfo = createDemo(fileSuffix, fileOriginName, sample, Convert.toStr(sysLoginUser.getId()), sysLoginUser.getName());
sysOnlineFileInfoResult= new SysOnlineFileInfoResult(Convert.toStr(sysFileInfo.getId()), fileOriginName + SymbolConstant.PERIOD + fileSuffix, Convert.toStr(sysLoginUser.getId()), sysLoginUser.getName());
}
//设置history
sysOnlineFileInfoResult.history = OnlineDocumentUtil.getHistory(sysOnlineFileInfoResult);
if(ObjectUtil.isAllNotEmpty(sysFileInfoParam.getMode(), sysFileInfoParam.getType())) {
sysOnlineFileInfoResult.changeType(sysFileInfoParam.getMode(), sysFileInfoParam.getType());
}
return sysOnlineFileInfoResult;
} else {
//暂时只支持本地文件
throw new ServiceException(SysFileInfoExceptionEnum.ONLINE_EDIT_SUPPORT_LOCAL_ONLY);
}
}
@Override @Override
public PageResult<SysFileInfo> page(SysFileInfoParam sysFileInfoParam) { public PageResult<SysFileInfo> page(SysFileInfoParam sysFileInfoParam) {
@ -101,6 +158,15 @@ public class SysFileInfoServiceImpl extends ServiceImpl<SysFileInfoMapper, SysFi
if (ObjectUtil.isNotEmpty(sysFileInfoParam.getFileOriginName())) { if (ObjectUtil.isNotEmpty(sysFileInfoParam.getFileOriginName())) {
queryWrapper.like(SysFileInfo::getFileOriginName, sysFileInfoParam.getFileOriginName()); queryWrapper.like(SysFileInfo::getFileOriginName, sysFileInfoParam.getFileOriginName());
} }
// 根据后缀查询
if(ObjectUtil.isNotEmpty(sysFileInfoParam.getFileSuffix())) {
if(sysFileInfoParam.getFileSuffix().contains(SymbolConstant.COMMA)) {
queryWrapper.in(SysFileInfo::getFileSuffix, Arrays.asList(sysFileInfoParam.getFileSuffix().split(SymbolConstant.COMMA)));
} else {
queryWrapper.eq(SysFileInfo::getFileSuffix, sysFileInfoParam.getFileSuffix());
}
}
} }
// 查询分页结果 // 查询分页结果
@ -194,7 +260,7 @@ public class SysFileInfoServiceImpl extends ServiceImpl<SysFileInfoMapper, SysFi
// 存储文件信息 // 存储文件信息
SysFileInfo sysFileInfo = new SysFileInfo(); SysFileInfo sysFileInfo = new SysFileInfo();
sysFileInfo.setId(fileId); sysFileInfo.setId(fileId);
sysFileInfo.setFileLocation(FileLocationEnum.LOCAL.getCode()); sysFileInfo.setFileLocation(SysFileLocationEnum.LOCAL.getCode());
sysFileInfo.setFileBucket(DEFAULT_BUCKET); sysFileInfo.setFileBucket(DEFAULT_BUCKET);
sysFileInfo.setFileObjectName(finalName); sysFileInfo.setFileObjectName(finalName);
sysFileInfo.setFileOriginName(originalFilename); sysFileInfo.setFileOriginName(originalFilename);
@ -318,4 +384,157 @@ public class SysFileInfoServiceImpl extends ServiceImpl<SysFileInfoMapper, SysFi
return sysFileInfo; return sysFileInfo;
} }
@Override
public void track() {
HttpServletRequest request = HttpServletUtil.getRequest();
HttpServletResponse response = HttpServletUtil.getResponse();
String fileObjectName = request.getParameter("fileObjectName");
String id = request.getParameter("id");
String storagePath = OnlineDocumentUtil.getStoragePath(id + SymbolConstant.PERIOD + FileUtil.getSuffix(fileObjectName));
String body = "";
Scanner scanner;
try {
scanner = new Scanner(request.getInputStream());
scanner.useDelimiter("\\A");
body = scanner.hasNext() ? scanner.next() : "";
scanner.close();
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jsonObj;
if (body.isEmpty()) {
log.error(">>> 读取文件request输入流为空");
return;
}
try {
jsonObj = JSONObject.parseObject(body);
} catch (Exception ex) {
log.error(">>> 文件信息body格式化错误");
return;
}
int status = (int) jsonObj.get("status");
String downloadUri = (String) jsonObj.get("url");
String changesUri = (String) jsonObj.get("changesurl");
String key = (String) jsonObj.get("key");
int saved = 0;
if (status == 2 || status == 3) {
//MustSave, Corrupted
try {
String histDir = OnlineDocumentUtil.getHistoryDir(OnlineDocumentUtil.getStoragePath(id));
String versionDir = OnlineDocumentUtil.getVersionDir(histDir, OnlineDocumentUtil.getFileVersion(histDir) + 1);
File ver = new File(versionDir);
File toSave = new File(storagePath);
if (!ver.exists()) ver.mkdirs();
toSave.renameTo(new File(versionDir + File.separator + "prev" + SymbolConstant.PERIOD + FileUtil.getSuffix(fileObjectName)));
DownloadUtil.downloadToFile(downloadUri, toSave);
DownloadUtil.downloadToFile(changesUri, new File(versionDir + File.separator + "diff.zip"));
String history = (String) jsonObj.get("changeshistory");
if (history == null && jsonObj.containsKey("history")) {
history = ((JSONObject) jsonObj.get("history")).toJSONString();
}
if (history != null && !history.isEmpty()) {
FileWriter fw = new FileWriter(new File(versionDir + File.separator + "changes.json"));
fw.write(history);
fw.close();
}
FileWriter fw = new FileWriter(new File(versionDir + File.separator + "key.txt"));
fw.write(key);
fw.close();
} catch (Exception ex) {
saved = 1;
}
}
try {
response.getWriter().write("{\"error\":" + saved + "}");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
*
* @param fileSuffix
* @param originalFilename
* @param sample
* @param userId id
* @param userName
* @author xuyuxiang
* @date 2021/3/24 11:01
*/
public SysFileInfo createDemo(String fileSuffix, String originalFilename, Boolean sample, String userId, String userName) {
// 文件名称拼接
originalFilename = originalFilename + SymbolConstant.PERIOD + fileSuffix;
// 模板名称
String demoName = (sample ? "sample." : "new.") + fileSuffix;
InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("assets/" + demoName);
// 生成文件的唯一id
Long fileId = IdWorker.getId();
// 生成文件的最终名称
String finalName = fileId + SymbolConstant.PERIOD + fileSuffix;
// 读取流
byte[] bytes = IoUtil.readBytes(stream);
// 将该模板文件存到存储桶
fileOperator.storageFile(DEFAULT_BUCKET, finalName, bytes);
// 创建元数据信息
createMeta(Convert.toStr(fileId), userId, userName);
// 计算文件大小kb
long fileSizeKb = Convert.toLong(NumberUtil.div(new BigDecimal(bytes.length), BigDecimal.valueOf(1024))
.setScale(0, BigDecimal.ROUND_HALF_UP));
// 计算文件大小信息
String fileSizeInfo = FileUtil.readableFileSize(bytes.length);
// 存储文件信息
SysFileInfo sysFileInfo = new SysFileInfo();
sysFileInfo.setId(fileId);
sysFileInfo.setFileLocation(SysFileLocationEnum.LOCAL.getCode());
sysFileInfo.setFileBucket(DEFAULT_BUCKET);
sysFileInfo.setFileObjectName(finalName);
sysFileInfo.setFileOriginName(originalFilename);
sysFileInfo.setFileSuffix(fileSuffix);
sysFileInfo.setFileSizeKb(fileSizeKb);
sysFileInfo.setFileSizeInfo(fileSizeInfo);
// 将新创建的文件保存到数据库
this.save(sysFileInfo);
return sysFileInfo;
}
/**
*
*
* @param fileId id
* @param userId id
* @param userName
* @author xuyuxiang
* @date 2021/3/24 11:19
*/
public void createMeta(String fileId, String userId, String userName) {
// 仅限本地文件
Object localClient = fileOperator.getClient();
if(ObjectUtil.isNull(localClient)) {
throw new ServiceException(SysFileInfoExceptionEnum.CLIENT_INIT_ERROR);
}
Dict localClientDict = (Dict) localClient;
// 拼接获取文档历史路径
String histDir = localClientDict.getStr("currentSavePath") + File.separator + DEFAULT_BUCKET + File.separator + fileId + "-hist";
if(!FileUtil.exist(histDir)) {
// 历史路径不存在则创建
File dir = new File(histDir);
dir.mkdir();
}
Dict dict = new Dict();
dict.put("created", DateUtil.now());
dict.put("id", (userId == null || userId.isEmpty()) ? -1 : userId);
dict.put("name", (userName == null || userName.isEmpty()) ? CommonConstant.UNKNOWN : userName);
File metaFile = new File(histDir + File.separator + "createdInfo.json");
FileUtil.writeString(JSONUtil.toJsonStr(dict), metaFile, Charset.defaultCharset());
}
} }

View File

@ -36,7 +36,9 @@ import vip.xiaonuo.sys.modular.file.enums.SysFileInfoExceptionEnum;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
/** /**
@ -86,4 +88,33 @@ public class DownloadUtil {
//下载文件 //下载文件
download(fileName, fileBytes, response); download(fileName, fileBytes, response);
} }
/**
* url
*
* @param url url
* @param file
* @author xuyuxiang
* @date 2021/3/25 16:51
*/
public static void downloadToFile(String url, File file) {
if (url == null || url.isEmpty()) throw new ServiceException(SysFileInfoExceptionEnum.DOWNLOAD_FILE_ERROR);
if (file == null) throw new ServiceException(SysFileInfoExceptionEnum.NOT_EXISTED_FILE);
try {
URL uri = new URL(url);
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) uri.openConnection();
InputStream stream = connection.getInputStream();
if (stream == null) {
throw new ServiceException(SysFileInfoExceptionEnum.FILE_STREAM_ERROR);
}
FileUtil.writeFromStream(stream, file);
connection.disconnect();
} catch (Exception e) {
log.error(">>> 下载文件异常,请求号为:{},具体信息为:{}", RequestNoContext.get(), e.getMessage());
throw new ServiceException(SysFileInfoExceptionEnum.DOWNLOAD_FILE_ERROR);
}
}
} }

View File

@ -0,0 +1,246 @@
/*
Copyright [2020] [https://www.xiaonuo.vip]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SnowyAPACHE LICENSE 2.0使
1.LICENSE
2.Snowy
3.
4. https://gitee.com/xiaonuobase/snowy-layui
5. https://gitee.com/xiaonuobase/snowy-layui
6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.file.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import vip.xiaonuo.core.consts.SymbolConstant;
import vip.xiaonuo.core.file.FileOperator;
import vip.xiaonuo.core.file.modular.local.LocalFileOperator;
import vip.xiaonuo.sys.config.FileConfig;
import vip.xiaonuo.sys.modular.file.result.SysOnlineFileInfoResult;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* 线
*
* @author xuyuxiang
* @date 2021/3/24 16:12
*/
public class OnlineDocumentUtil {
private static final String VIEWED_SUFFIX = ".pdf|.djvu|.xps";
private static final String EDITED_SUFFIX = ".docx|.xlsx|.csv|.pptx|.txt";
private static final String CONVERT_SUFFIX = ".docm|.dotx|.dotm|.dot|.doc|.odt|.fodt|.ott|.xlsm|.xltx|.xltm|.xlt|.xls|.ods|.fods|.ots|.pptm|.ppt|.ppsx|.ppsm|.pps|.potx|.potm|.pot|.odp|.fodp|.otp|.rtf|.mht|.html|.htm|.epub";
public static final List<String> DOCUMENT_SUFFIX = Arrays.asList(".doc", ".docx", ".docm", ".dot", ".dotx", ".dotm", ".odt", ".fodt", ".ott", ".rtf", ".txt", ".html", ".htm",
".mht", ".pdf", ".djvu", ".fb2", ".epub", ".xps");
public static final List<String> SPREADSHEET_SUFFIX = Arrays.asList(".xls", ".xlsx", ".xlsm", ".xlt", ".xltx", ".xltm", ".ods", ".fods", ".ots", ".csv");
public static final List<String> Presentation_SUFFIX = Arrays.asList(".pps", ".ppsx", ".ppsm",".ppt", ".pptx", ".pptm", ".pot", ".potx", ".potm", ".odp", ".fodp", ".otp");
public static List<String> getFileSuffix() {
List<String> res = new ArrayList<>();
res.addAll(getViewedSuffix());
res.addAll(getEditedSuffix());
res.addAll(getConvertSuffix());
return res;
}
public static List<String> getViewedSuffix() {
return Arrays.asList(VIEWED_SUFFIX.split("\\|"));
}
public static List<String> getEditedSuffix() {
return Arrays.asList(EDITED_SUFFIX.split("\\|"));
}
public static List<String> getConvertSuffix() {
return Arrays.asList(CONVERT_SUFFIX.split("\\|"));
}
public static String getFileType(String fileOriginName) {
String suffix = SymbolConstant.PERIOD + FileUtil.getSuffix(fileOriginName);
if (DOCUMENT_SUFFIX.contains(suffix)) return "Text";
if (SPREADSHEET_SUFFIX.contains(suffix)) return "Spreadsheet";
if (Presentation_SUFFIX.contains(suffix)) return "Presentation";
return "Text";
}
public static String getFileUri(String fileId, String fileName) {
try {
String filePath = "sysFileInfo/download?id="+ fileId +"&name=" + URLEncoder.encode(fileName, java.nio.charset.StandardCharsets.UTF_8.toString()).replace("+", "%20");
return filePath;
} catch (UnsupportedEncodingException e) {
return "";
}
}
public static String getCallback(String fileId, String fileSuffix) {
return "sysFileInfo/track?fileObjectName=" + fileId + SymbolConstant.PERIOD + fileSuffix + "&id=" + fileId;
}
public static String getStoragePath(String fileIdOrObjectName) {
String directory = FilesRootPath();
return directory + File.separator + FileConfig.DEFAULT_BUCKET + File.separator + fileIdOrObjectName;
}
public static String FilesRootPath() {
LocalFileOperator localFileOperator = (LocalFileOperator) SpringUtil.getBean(FileOperator.class);
Dict localClientDict = (Dict) localFileOperator.getClient();
String currentSavePath = localClientDict.getStr("currentSavePath");
File file = new File(currentSavePath);
if (!file.exists()) {
file.mkdirs();
}
return currentSavePath;
}
public static String[] getHistory(SysOnlineFileInfoResult sysOnlineFileInfoResult) {
String histDir = OnlineDocumentUtil.getHistoryDir(OnlineDocumentUtil.getStoragePath(sysOnlineFileInfoResult.getFileId()));
if (getFileVersion(histDir) > 0) {
Integer curVer = getFileVersion(histDir);
Set<Object> hist = new HashSet<Object>();
Map<String, Object> histData = new HashMap<String, Object>();
for (Integer i = 0; i <= curVer; i++) {
Map<String, Object> obj = new HashMap<String, Object>();
Map<String, Object> dataObj = new HashMap<String, Object>();
String verDir = getVersionDir(histDir, i + 1);
try {
String key = null;
key = i == curVer ? sysOnlineFileInfoResult.document.key : readFileToEnd(new File(verDir + File.separator + "key.txt"));
obj.put("key", key);
obj.put("version", i);
if (i == 0) {
String createdInfo = readFileToEnd(new File(histDir + File.separator + "createdInfo.json"));
JSONObject json = (JSONObject) JSONUtil.parse(createdInfo);
obj.put("created", json.get("created"));
Map<String, Object> user = new HashMap<String, Object>();
user.put("id", json.get("id"));
user.put("name", json.get("name"));
obj.put("user", user);
}
dataObj.put("key", key);
dataObj.put("url", i == curVer ? sysOnlineFileInfoResult.document.url : getStoragePath(verDir + File.separator + "prev" + FileUtil.getSuffix(sysOnlineFileInfoResult.getDocument().title)));
dataObj.put("version", i);
if (i > 0) {
JSONObject changes = (JSONObject) JSONUtil.parse(readFileToEnd(new File(getVersionDir(histDir, i) + File.separator + "changes.json")));
JSONObject change = (JSONObject) ((JSONArray) changes.get("changes")).get(0);
obj.put("changes", changes.get("changes"));
obj.put("serverVersion", changes.get("serverVersion"));
obj.put("created", change.get("created"));
obj.put("user", change.get("user"));
Map<String, Object> prev = (Map<String, Object>) histData.get(Integer.toString(i - 1));
Map<String, Object> prevInfo = new HashMap<String, Object>();
prevInfo.put("key", prev.get("key"));
prevInfo.put("url", prev.get("url"));
dataObj.put("previous", prevInfo);
dataObj.put("changesUrl", getStoragePath(getVersionDir(histDir, i) + File.separator + "diff.zip"));
}
hist.add(obj);
histData.put(Integer.toString(i), dataObj);
} catch (Exception ex) {
}
}
Map<String, Object> histObj = new HashMap<String, Object>();
histObj.put("currentVersion", curVer);
histObj.put("history", hist);
Gson gson = new Gson();
return new String[]{gson.toJson(histObj), gson.toJson(histData)};
}
return new String[]{"", ""};
}
public static String getHistoryDir(String storagePath) {
return storagePath += "-hist";
}
public static String getVersionDir(String histPath, Integer version) {
return histPath + File.separator + Integer.toString(version);
}
public static Integer getFileVersion(String historyPath) {
File dir = new File(historyPath);
if (!dir.exists()) return 0;
File[] dirs = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
return dirs.length;
}
private static String readFileToEnd(File file) {
String output = "";
try {
try(FileInputStream is = new FileInputStream(file))
{
Scanner scanner = new Scanner(is);
scanner.useDelimiter("\\A");
while (scanner.hasNext()) {
output += scanner.next();
}
scanner.close();
}
} catch (Exception e) { }
return output;
}
public static void deleteFileHistory(String fileId) {
// 判断文件存在不存在
String histDir = OnlineDocumentUtil.getHistoryDir(OnlineDocumentUtil.getStoragePath(fileId));
File hisDirFile = FileUtil.file(histDir);
if (!FileUtil.exist(hisDirFile)) {
return;
}
// 删除文件
FileUtil.del(hisDirFile);
}
}