add 文件管理

This commit is contained in:
2021-06-23 17:56:29 +08:00
parent 3734dda2db
commit 32856f4757
9 changed files with 636 additions and 157 deletions

View File

@@ -9,5 +9,6 @@
/// 文件Id
/// </summary>
public string Id { get; set; }
public System.DateTime CreatedTime { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using Furion;
using Ewide.Core.Extension;
using Furion;
using Furion.DatabaseAccessor;
using Furion.DatabaseAccessor.Extensions;
using Furion.DependencyInjection;
@@ -50,8 +51,7 @@ namespace Ewide.Core.Service
.Where(input.FileLocation > 0, u => u.FileLocation == input.FileLocation)
.Where(fileBucket, u => EF.Functions.Like(u.FileBucket, $"%{input.FileBucket.Trim()}%"))
.Where(fileOriginName, u => EF.Functions.Like(u.FileOriginName, $"%{input.FileOriginName.Trim()}%"))
.Select(u => u.Adapt<FileOutput>())
.ToPagedListAsync(input.PageIndex, input.PageSize);
.ToPageData<SysFile, FileOutput>(input);
return PageDataResult<FileOutput>.PageResult(files);
}

View File

@@ -1,7 +1,12 @@
@import (reference) '../extend.less';
.yo-query-bar {
margin-bottom: @padding-md;
margin-bottom: @padding-xs;
.ant-form-inline {
.ant-form-item {
margin-bottom: @padding-xs;
}
}
}
.yo-action-bar {
@@ -71,6 +76,10 @@
}
}
.ant-table-sticky-scroll {
display: none;
}
.yo-table {
.ant-table {
margin: 0 !important;
@@ -153,6 +162,10 @@
border-top: @border-width-base @border-style-base @table-border-color;
}
}
&--row-no {
background-color: @table-header-bg;
}
}
.yo-table-actions {

View File

@@ -501,7 +501,6 @@
flex-direction: column;
width: 100%;
min-width: @container-width;
height: 100%;
@layout-header-height: 54px;

View File

@@ -46,7 +46,6 @@ const { getState, subscribe } = store
const stroePath = 'user'
export default class Auth extends Component {
state = getState(stroePath)
constructor(props) {
@@ -62,11 +61,10 @@ export default class Auth extends Component {
}
render() {
const flag = auth.call(this.state, this.props.auth)
if (flag) {
return this.props.children
return this.props.children || <></>
}
return <></>

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import { Form, Button, Table, Tooltip } from 'antd'
import { AntIcon } from 'components'
const propsMap = ['autoLoad', 'loadData', 'pageIndex', 'pageSize']
const propsMap = ['columns', 'autoLoad', 'loadData', 'pageIndex', 'pageSize']
const clearChildren = data => {
data.forEach(p => {
@@ -17,6 +17,17 @@ const clearChildren = data => {
return data
}
const rowNoColumn = {
title: '#',
dataIndex: 'rowNo',
width: 30,
fixed: true,
align: 'center',
ellipsis: true,
className: 'yo-table--row-no',
render: (text, record, index) => index + 1,
}
/**
* 渲染查询栏
* @returns
@@ -64,7 +75,7 @@ export default class QueryTable extends Component {
// 加载状态
loading: false,
// 表格类型
type: '',
type: 'tree',
// 数据
dataSource: [],
}
@@ -105,6 +116,8 @@ export default class QueryTable extends Component {
this.loadData =
typeof this.props.loadData === 'function' ? this.props.loadData : async () => {}
this.rowNumber = typeof this.props.rowNumber === 'boolean' ? this.props.rowNumber : true
if (this.props.pageIndex) {
this.pageIndex = this.props.pageIndex
this.pagination.current = this.pageIndex
@@ -113,6 +126,19 @@ export default class QueryTable extends Component {
this.pageSize = this.props.pageSize
this.pagination.pageSize = this.pageSize
}
// 默认排序
if (this.props.columns) {
for (const column of this.props.columns) {
if (column.defaultSortOrder) {
this.sorter = {
sortField: column.dataIndex,
sortOrder: column.defaultSortOrder,
}
break
}
}
}
}
/**
@@ -247,7 +273,9 @@ export default class QueryTable extends Component {
}
render() {
const { loading, dataSource } = this.state
const { rowNumber } = this
const { loading, dataSource, type } = this.state
const { query, operator, columns } = this.props
@@ -262,11 +290,19 @@ export default class QueryTable extends Component {
loading,
pagination: this.pagination,
dataSource,
columns: (columns || []).filter(p => !p.hidden),
columns: (() => {
const c = []
if (type !== 'tree' && rowNumber) {
c.push(rowNoColumn)
}
c.push(...(columns || []))
return c.filter(p => !p.hidden)
})(),
bordered: true,
size: 'middle',
rowKey: record => record.id || Math.random().toString(16).slice(2),
sticky: true,
scroll: { x: 'max-content' },
...attrs,
}

View File

@@ -15,18 +15,17 @@ const apiAction = {
edit: api.sysAppEdit,
delete: api.sysAppDelete,
setDefault: api.sysAppSetAsDefault
setDefault: api.sysAppSetAsDefault,
}
// 用于弹窗标题
const name = '应用'
export default class index extends Component {
state = {
codes: {
commonStatus: []
}
commonStatus: [],
},
}
// 表格实例
@@ -42,21 +41,25 @@ export default class index extends Component {
{
title: '应用名称',
dataIndex: 'name',
width: 300,
sorter: true,
},
{
title: '唯一编码',
dataIndex: 'code',
width: 300,
sorter: true,
},
{
title: '是否默认',
dataIndex: 'active',
width: 200,
sorter: true,
render: (text, record) => (<>
render: (text, record) => (
<>
{text ? '是' : '否'}
{
!record.active && <Auth auth="sysApp:setAsDefault">
{!record.active && (
<Auth auth="sysApp:setAsDefault">
<QueryTableActions>
<span></span>
<Popconfirm
@@ -68,18 +71,21 @@ export default class index extends Component {
</Popconfirm>
</QueryTableActions>
</Auth>
}
</>)
)}
</>
),
},
{
title: '状态',
dataIndex: 'status',
width: 100,
sorter: true,
render: text => (<>{this.bindCodeValue(text, 'common_status')}</>)
render: text => <>{this.bindCodeValue(text, 'common_status')}</>,
},
{
title: '排序',
dataIndex: 'sort',
width: 100,
sorter: true,
},
]
@@ -98,7 +104,8 @@ export default class index extends Component {
title: '操作',
width: 150,
dataIndex: 'actions',
render: (text, record) => (<QueryTableActions>
render: (text, record) => (
<QueryTableActions>
<Auth auth="sysApp:edit">
<a onClick={() => this.onOpen(this.editForm, record)}>编辑</a>
</Auth>
@@ -111,7 +118,8 @@ export default class index extends Component {
<a>删除</a>
</Popconfirm>
</Auth>
</QueryTableActions>)
</QueryTableActions>
),
})
}
}
@@ -136,11 +144,14 @@ export default class index extends Component {
const { onLoading, onLoadData } = this.table.current
onLoading()
getDictData('common_status').then(res => {
this.setState({
codes: res
}, () => {
this.setState(
{
codes: res,
},
() => {
onLoadData()
})
}
)
})
}
@@ -169,7 +180,7 @@ export default class index extends Component {
name = toCamelCase(name)
const codes = this.state.codes[name]
if (codes) {
const c = codes.find((p) => p.code === code)
const c = codes.find(p => p.code == code)
if (c) {
return c.value
}
@@ -184,7 +195,7 @@ export default class index extends Component {
*/
onOpen(modal, record) {
modal.current.open({
record
record,
})
}
@@ -215,18 +226,12 @@ export default class index extends Component {
* @param {*} record
*/
onDelete(record) {
this.onAction(
apiAction.delete(record),
'删除成功'
)
this.onAction(apiAction.delete(record), '删除成功')
}
//#region 自定义方法
async onSetDefault(record) {
this.onAction(
apiAction.setDefault(record),
'设置成功'
)
this.onAction(apiAction.setDefault(record), '设置成功')
}
//#endregion
@@ -255,7 +260,9 @@ export default class index extends Component {
<Button
icon={<AntIcon type="plus" />}
onClick={() => this.onOpen(this.addForm)}
>新增{name}</Button>
>
新增{name}
</Button>
</Auth>
}
/>

View File

@@ -0,0 +1,398 @@
import React, { Component } from 'react'
import {
Button,
Card,
Form,
Input,
message as Message,
Popconfirm,
Select,
Tag,
Tooltip,
Upload,
} from 'antd'
import { AntIcon, Auth, Container, PhotoPreview, QueryTable, QueryTableActions } from 'components'
import { api } from 'common/api'
import auth from 'components/authorized/handler'
import { isEqual } from 'lodash'
import getDictData from 'util/dic'
import { toCamelCase } from 'util/format'
import { ArrayBufferToBase64, GetFileName, PreviewFileArrayBuffer } from 'util/file'
/**
* 注释段[\/**\/]为必须要改
*/
/**
* 配置页面所需接口函数
*/
const apiAction = {
page: api.sysFileInfoPage,
delete: api.sysFileInfoDelete,
}
/**
* 用于弹窗标题
* [必要]
*/
const name = '/**/'
/**
* 统一配置权限标识
* [必要]
*/
const authName = 'sysFileInfo'
export default class index extends Component {
state = {
codes: {
fileStorageLocation: [],
},
uploading: false,
}
// 表格实例
table = React.createRef()
photoPreview = React.createRef()
columns = [
{
title: '文件名称',
dataIndex: 'fileOriginName',
width: 300,
ellipsis: {
showTitle: false,
},
sorter: true,
render: text => <Tooltip title={text}>{text}</Tooltip>,
},
{
title: '文件后缀',
dataIndex: 'fileSuffix',
width: 120,
sorter: true,
render: text => <Tag color="green">{text}</Tag>,
},
{
title: '文件大小',
dataIndex: 'fileSizeKb',
width: 120,
sorter: true,
render: text => (
<>
{text}
<small>KB</small>
</>
),
},
{
title: '存储位置',
dataIndex: 'fileLocation',
width: 120,
sorter: true,
render: text => this.bindCodeValue(text, 'file_storage_location'),
},
{
title: '文件仓库',
dataIndex: 'fileBucket',
width: 200,
ellipsis: {
showTitle: false,
},
sorter: true,
render: text => <Tooltip title={text}>{text}</Tooltip>,
},
{
title: '唯一标识id',
dataIndex: 'fileObjectName',
width: 250,
ellipsis: {
showTitle: false,
},
sorter: true,
render: text => <Tooltip title={text}>{text}</Tooltip>,
},
{
title: '上传时间',
dataIndex: 'createdTime',
width: 200,
sorter: true,
defaultSortOrder: 'descend',
},
]
/**
* 构造函数,在渲染前动态添加操作字段等
* @param {*} props
*/
constructor(props) {
super(props)
const flag = auth({ [authName]: 'delete' })
if (flag) {
this.columns.push({
title: '操作',
width: 150,
dataIndex: 'actions',
render: (text, record) => (
<QueryTableActions>
<a onClick={() => this.onFileDownload(record)}>下载</a>
<Auth auth={{ [authName]: 'delete' }}>
<Popconfirm
placement="topRight"
title="是否确认删除"
onConfirm={() => this.onDelete(record)}
>
<a>删除</a>
</Popconfirm>
</Auth>
{['png', 'jpeg', 'jpg', 'gif', 'tif', 'bmp'].includes(
record.fileSuffix
) && <a onClick={() => this.onFilePreview(record)}>预览</a>}
</QueryTableActions>
),
})
}
}
/**
* 阻止外部组件引发的渲染,提升性能
* 可自行添加渲染条件
* [必要]
* @param {*} props
* @param {*} state
* @returns
*/
shouldComponentUpdate(props, state) {
return !isEqual(this.state, state)
}
/**
* 加载字典数据,之后开始加载表格数据
* 如果必须要加载字典数据,可直接对表格设置autoLoad=true
*/
componentDidMount() {
const { onLoading, onLoadData } = this.table.current
onLoading()
getDictData('file_storage_location').then(codes => {
this.setState({ codes }, () => {
onLoadData()
})
})
}
/**
* 调用加载数据接口,可在调用前对query进行处理
* [异步,必要]
* @param {*} params
* @param {*} query
* @returns
*/
loadData = async (params, query) => {
const { data } = await apiAction.page({
...params,
...query,
})
return data
}
/**
* 绑定字典数据
* @param {*} code
* @param {*} name
* @returns
*/
bindCodeValue(code, name) {
name = toCamelCase(name)
const codes = this.state.codes[name]
if (codes) {
const c = codes.find(p => p.code == code)
if (c) {
return c.value
}
}
return null
}
/**
* 打开新增/编辑弹窗
* @param {*} modal
* @param {*} record
*/
onOpen(modal, record) {
modal.current.open({
record,
})
}
/**
* 对表格上的操作进行统一处理
* [异步]
* @param {*} action
* @param {*} successMessage
*/
async onAction(action, successMessage) {
const { onLoading, onLoaded, onReloadData } = this.table.current
onLoading()
try {
if (action) {
await action
}
if (successMessage) {
Message.success(successMessage)
}
onReloadData()
} catch {
onLoaded()
}
}
/**
* 删除
* @param {*} record
*/
onDelete(record) {
this.onAction(apiAction.delete(record), '删除成功')
}
//#region 自定义方法
async onFileUpload({ file }) {
this.setState({ uploading: true })
const table = this.table.current
table.onLoading()
const fd = new FormData()
fd.append('file', file)
try {
await api.sysFileInfoUpload(fd)
table.onReloadData()
} catch {
table.onLoaded()
} finally {
this.setState({ uploading: false })
}
}
async onFilePreview({ id }) {
const key = Math.random().toString(16).slice(2)
const hide = Message.loading({
key,
content: '正在获取文件...',
duration: 0,
})
const file = await PreviewFileArrayBuffer(id)
if (file) {
const base64 = await ArrayBufferToBase64(file)
var img = new Image()
img.onload = () => {
const items = [
{
src: base64,
w: img.naturalWidth,
h: img.naturalHeight,
},
]
this.photoPreview.current.initPhotoSwipe(items)
hide()
}
img.onerror = () => {
Message.error({
key,
content: '获取文件失败',
})
}
img.src = base64
} else {
Message.error({
key,
content: '获取文件失败',
})
}
}
async onFileDownload({ id }) {
const key = Math.random().toString(16).slice(2)
const hide = Message.loading({
key,
content: '正在获取文件...',
duration: 0,
})
try {
const { data, headers } = await api.sysFileInfoDownload({ id })
const url = window.URL.createObjectURL(data)
const fileName = GetFileName(headers['content-disposition'])
const a = document.createElement('a')
a.href = url
a.download = fileName
a.click()
window.URL.revokeObjectURL(url)
a.remove()
hide()
} catch {
Message.error({
key,
content: '下载文件失败',
})
}
}
//#endregion
render() {
const { codes, uploading } = this.state
return (
<Container mode="fluid">
<br />
<Card bordered={false}>
<QueryTable
ref={this.table}
autoLoad={false}
loadData={this.loadData}
columns={this.columns}
query={
<Auth auth={{ [authName]: 'page' }}>
<Form.Item label="文件名" name="fileOriginName">
<Input
autoComplete="off"
placeholder="请输入文件名"
className="w-200"
/>
</Form.Item>
<Form.Item label="存储位置" name="fileLocation">
<Select placeholder="请选择存储位置" className="w-200">
{codes.fileStorageLocation.map(item => (
<Select.Option key={item.code} value={item.code}>
{item.value}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="文件仓库" name="fileBucket">
<Input
autoComplete="off"
placeholder="请输入文件仓库"
className="w-200"
/>
</Form.Item>
</Auth>
}
operator={
<Auth auth={{ [authName]: 'add' }}>
<Upload customRequest={e => this.onFileUpload(e)} fileList={[]}>
<Button loading={uploading} icon={<AntIcon type="upload" />}>
上传文件
</Button>
</Upload>
</Auth>
}
/>
</Card>
<PhotoPreview ref={this.photoPreview} />
</Container>
)
}
}

View File

@@ -5,14 +5,15 @@ import 'nprogress/nprogress.css'
import AntIcon from 'components/ant-icon'
import { Container } from 'components'
NProgress.configure({ parent: '.ant-layout-content > .yo-tab-external-mount > .yo-tab-external-mount-content' });
NProgress.configure({
parent: '.ant-layout-content > .yo-tab-external-mount > .yo-tab-external-mount-content',
})
class ComponentDynamic extends Component {
state = {
// 组件内部组件的key,用于刷新
key: null,
component: null
component: null,
}
shouldComponentUpdate() {
@@ -32,8 +33,7 @@ class ComponentDynamic extends Component {
// 在这里使用setTimeout调用,是防止打开窗口时卡顿
setTimeout(async () => {
let component;
let component
try {
component = await import(`../../../../pages${this.props.path}`)
@@ -41,19 +41,22 @@ class ComponentDynamic extends Component {
component = await import('views/error/404')
}
this.setState({
this.setState(
{
key: Math.random().toString(16).slice(2),
component: component.default
}, () => {
component: component.default,
},
() => {
NProgress.done()
})
}
)
})
}
render() {
if (this.state.component) {
return <this.state.component
return (
<this.state.component
key={this.state.key}
{...this.props}
supportInfo={
@@ -64,15 +67,15 @@ class ComponentDynamic extends Component {
</Container>
}
/>
)
}
return <></>
}
}
export default class index extends Component {
state = {
actived: ''
actived: '',
}
panes = []
@@ -83,13 +86,13 @@ export default class index extends Component {
static getDerivedStateFromProps(props) {
return {
actived: props.actived
actived: props.actived,
}
}
onChange(activeKey) {
this.props.parent.setState({
actived: activeKey
actived: activeKey,
})
}
@@ -99,7 +102,7 @@ export default class index extends Component {
}
}
onReload = (key) => {
onReload = key => {
key = key || this.state.actived
const pane = this.panes.find(p => p.props.id === key)
if (pane) {
@@ -108,7 +111,6 @@ export default class index extends Component {
}
render() {
this.panes = []
return (
@@ -118,11 +120,10 @@ export default class index extends Component {
type="editable-card"
hideAdd
activeKey={this.state.actived}
onChange={(activeKey) => this.onChange(activeKey)}
onChange={activeKey => this.onChange(activeKey)}
onEdit={(targetKey, action) => this.onClose(targetKey, action)}
>
{
this.props.panes.map(pane => {
{this.props.panes.map(pane => {
return (
<Tabs.TabPane
closable={pane.closable}
@@ -132,11 +133,37 @@ export default class index extends Component {
trigger={['contextMenu']}
overlay={
<Menu>
<Menu.Item key="0" onClick={() => this.onReload(pane.key)}>重新加载</Menu.Item>
<Menu.Item
key="0"
onClick={() => this.onReload(pane.key)}
>
重新加载
</Menu.Item>
<Menu.Divider />
<Menu.Item key="1" onClick={() => window.closeContentWindow(pane.key)}>关闭</Menu.Item>
<Menu.Item key="2" onClick={() => window.closeOtherContentWindow(pane.key)}>关闭其他标签页</Menu.Item>
<Menu.Item key="3" onClick={() => window.closeRightContentWindow(pane.key)}>关闭右侧标签页</Menu.Item>
<Menu.Item
key="1"
onClick={() =>
window.closeContentWindow(pane.key)
}
>
关闭
</Menu.Item>
<Menu.Item
key="2"
onClick={() =>
window.closeOtherContentWindow(pane.key)
}
>
关闭其他标签页
</Menu.Item>
<Menu.Item
key="3"
onClick={() =>
window.closeRightContentWindow(pane.key)
}
>
关闭右侧标签页
</Menu.Item>
</Menu>
}
>
@@ -148,17 +175,18 @@ export default class index extends Component {
}
/>
)
})
}
})}
</Tabs>
<div className="yo-tab-external-mount-content">
{
this.props.panes.map(pane => {
{this.props.panes.map(pane => {
return (
<div
key={pane.key}
className={
(pane.key === this.state.actived ? 'yo-tab-external-tabpane-active' : 'yo-tab-external-tabpane-inactive') + ' yo-tab-external-tabpane'
(pane.key === this.state.actived
? 'yo-tab-external-tabpane-active'
: 'yo-tab-external-tabpane-inactive') +
' yo-tab-external-tabpane'
}
>
<ComponentDynamic
@@ -170,8 +198,7 @@ export default class index extends Component {
/>
</div>
)
})
}
})}
</div>
</div>
</Layout.Content>