diff --git a/Api/Ewide.Core/Service/File/Dto/FileOutput.cs b/Api/Ewide.Core/Service/File/Dto/FileOutput.cs index d9716dd..ae043d6 100644 --- a/Api/Ewide.Core/Service/File/Dto/FileOutput.cs +++ b/Api/Ewide.Core/Service/File/Dto/FileOutput.cs @@ -9,5 +9,6 @@ /// 文件Id /// public string Id { get; set; } + public System.DateTime CreatedTime { get; set; } } } diff --git a/Api/Ewide.Core/Service/File/SysFileService.cs b/Api/Ewide.Core/Service/File/SysFileService.cs index 1ab43b9..25c9447 100644 --- a/Api/Ewide.Core/Service/File/SysFileService.cs +++ b/Api/Ewide.Core/Service/File/SysFileService.cs @@ -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()) - .ToPagedListAsync(input.PageIndex, input.PageSize); + .ToPageData(input); return PageDataResult.PageResult(files); } diff --git a/web-react/src/assets/style/lib/table.less b/web-react/src/assets/style/lib/table.less index 6d9e08d..5a06a46 100644 --- a/web-react/src/assets/style/lib/table.less +++ b/web-react/src/assets/style/lib/table.less @@ -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 { diff --git a/web-react/src/assets/style/main.less b/web-react/src/assets/style/main.less index 6d56bb5..7cf67c9 100644 --- a/web-react/src/assets/style/main.less +++ b/web-react/src/assets/style/main.less @@ -501,7 +501,6 @@ flex-direction: column; width: 100%; - min-width: @container-width; height: 100%; @layout-header-height: 54px; diff --git a/web-react/src/components/authorized/index.jsx b/web-react/src/components/authorized/index.jsx index a325429..24c0d0e 100644 --- a/web-react/src/components/authorized/index.jsx +++ b/web-react/src/components/authorized/index.jsx @@ -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 <> diff --git a/web-react/src/components/query-table/index.jsx b/web-react/src/components/query-table/index.jsx index b842720..d0028bf 100644 --- a/web-react/src/components/query-table/index.jsx +++ b/web-react/src/components/query-table/index.jsx @@ -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, } diff --git a/web-react/src/pages/system/app/index.jsx b/web-react/src/pages/system/app/index.jsx index 8fdfcd0..458fb4c 100644 --- a/web-react/src/pages/system/app/index.jsx +++ b/web-react/src/pages/system/app/index.jsx @@ -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,51 +41,58 @@ 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) => (<> - {text ? '是' : '否'} - { - !record.active && - - - this.onSetDefault(record)} - > - 设为默认 - - - - } - ) + render: (text, record) => ( + <> + {text ? '是' : '否'} + {!record.active && ( + + + + this.onSetDefault(record)} + > + 设为默认 + + + + )} + + ), }, { 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, }, ] /** * 构造函数,在渲染前动态添加操作字段等 - * @param {*} props + * @param {*} props */ constructor(props) { super(props) @@ -98,20 +104,22 @@ export default class index extends Component { title: '操作', width: 150, dataIndex: 'actions', - render: (text, record) => ( - - this.onOpen(this.editForm, record)}>编辑 - - - this.onDelete(record)} - > - 删除 - - - ) + render: (text, record) => ( + + + this.onOpen(this.editForm, record)}>编辑 + + + this.onDelete(record)} + > + 删除 + + + + ), }) } } @@ -120,9 +128,9 @@ export default class index extends Component { * 阻止外部组件引发的渲染,提升性能 * 可自行添加渲染条件 * [必要] - * @param {*} props - * @param {*} state - * @returns + * @param {*} props + * @param {*} state + * @returns */ shouldComponentUpdate(props, state) { return !isEqual(this.state, state) @@ -136,20 +144,23 @@ export default class index extends Component { const { onLoading, onLoadData } = this.table.current onLoading() getDictData('common_status').then(res => { - this.setState({ - codes: res - }, () => { - onLoadData() - }) + this.setState( + { + codes: res, + }, + () => { + onLoadData() + } + ) }) } /** * 调用加载数据接口,可在调用前对query进行处理 * [异步,必要] - * @param {*} params - * @param {*} query - * @returns + * @param {*} params + * @param {*} query + * @returns */ loadData = async (params, query) => { const { data } = await apiAction.page({ @@ -161,15 +172,15 @@ export default class index extends Component { /** * 绑定字典数据 - * @param {*} code - * @param {*} name - * @returns + * @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) + const c = codes.find(p => p.code == code) if (c) { return c.value } @@ -179,20 +190,20 @@ export default class index extends Component { /** * 打开新增/编辑弹窗 - * @param {*} modal - * @param {*} record + * @param {*} modal + * @param {*} record */ onOpen(modal, record) { modal.current.open({ - record + record, }) } /** * 对表格上的操作进行统一处理 * [异步] - * @param {*} action - * @param {*} successMessage + * @param {*} action + * @param {*} successMessage */ async onAction(action, successMessage) { const { onLoading, onLoaded, onReloadData } = this.table.current @@ -212,21 +223,15 @@ export default class index extends Component { /** * 删除 - * @param {*} record + * @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 { + > + 新增{name} + } /> diff --git a/web-react/src/pages/system/file/index.jsx b/web-react/src/pages/system/file/index.jsx new file mode 100644 index 0000000..f5db6e0 --- /dev/null +++ b/web-react/src/pages/system/file/index.jsx @@ -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 => {text}, + }, + { + title: '文件后缀', + dataIndex: 'fileSuffix', + width: 120, + sorter: true, + render: text => {text}, + }, + { + title: '文件大小', + dataIndex: 'fileSizeKb', + width: 120, + sorter: true, + render: text => ( + <> + {text} + KB + + ), + }, + { + 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 => {text}, + }, + { + title: '唯一标识id', + dataIndex: 'fileObjectName', + width: 250, + ellipsis: { + showTitle: false, + }, + sorter: true, + render: text => {text}, + }, + { + 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) => ( + + this.onFileDownload(record)}>下载 + + this.onDelete(record)} + > + 删除 + + + {['png', 'jpeg', 'jpg', 'gif', 'tif', 'bmp'].includes( + record.fileSuffix + ) && this.onFilePreview(record)}>预览} + + ), + }) + } + } + + /** + * 阻止外部组件引发的渲染,提升性能 + * 可自行添加渲染条件 + * [必要] + * @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 ( + +
+ + + + + + + + + + + + + } + operator={ + + this.onFileUpload(e)} fileList={[]}> + + + + } + /> + + + +
+ ) + } +} diff --git a/web-react/src/views/main/_layout/content/index.jsx b/web-react/src/views/main/_layout/content/index.jsx index 2ebe6f2..f9aadc7 100644 --- a/web-react/src/views/main/_layout/content/index.jsx +++ b/web-react/src/views/main/_layout/content/index.jsx @@ -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,38 +41,41 @@ class ComponentDynamic extends Component { component = await import('views/error/404') } - this.setState({ - key: Math.random().toString(16).slice(2), - component: component.default - }, () => { - NProgress.done() - }) - + this.setState( + { + key: Math.random().toString(16).slice(2), + component: component.default, + }, + () => { + NProgress.done() + } + ) }) } render() { if (this.state.component) { - return - - Ewide Core ©2021 v1.0 - - - } - /> + return ( + + + Ewide Core ©2021 v1.0 + + + } + /> + ) } 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,60 +120,85 @@ 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 => { - return ( - - this.onReload(pane.key)}>重新加载 - - window.closeContentWindow(pane.key)}>关闭 - window.closeOtherContentWindow(pane.key)}>关闭其他标签页 - window.closeRightContentWindow(pane.key)}>关闭右侧标签页 - - } - > -
- {pane.icon && } - {pane.title} -
- - } - /> - ) - }) - } + {this.props.panes.map(pane => { + return ( + + this.onReload(pane.key)} + > + 重新加载 + + + + window.closeContentWindow(pane.key) + } + > + 关闭 + + + window.closeOtherContentWindow(pane.key) + } + > + 关闭其他标签页 + + + window.closeRightContentWindow(pane.key) + } + > + 关闭右侧标签页 + + + } + > +
+ {pane.icon && } + {pane.title} +
+ + } + /> + ) + })}
- { - this.props.panes.map(pane => { - return ( -
{ + return ( +
+ - this.panes.push(p)} - /> -
- ) - }) - } + param={pane.param} + onRef={p => this.panes.push(p)} + /> +
+ ) + })}