为什么都没了
This commit is contained in:
30
framework/web-react/src/components/ant-icon/index.jsx
Normal file
30
framework/web-react/src/components/ant-icon/index.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react'
|
||||
import * as Icon from '@ant-design/icons'
|
||||
|
||||
export default class AntIcon extends Component {
|
||||
render() {
|
||||
const type = (this.props.type || '').toUpperCase()
|
||||
|
||||
if (type) {
|
||||
if (
|
||||
type.indexOf('OUTLINED') >= 0 ||
|
||||
type.indexOf('FILLED') >= 0 ||
|
||||
type.indexOf('TWOTONE') >= 0
|
||||
) {
|
||||
const I = Icon[this.props.type]
|
||||
return I ? <I {...this.props} /> : false
|
||||
} else {
|
||||
const t =
|
||||
type
|
||||
.split('-')
|
||||
.map(p => {
|
||||
return p[0] + p.slice(1).toLowerCase()
|
||||
})
|
||||
.join('') + 'Outlined'
|
||||
const I = Icon[t]
|
||||
return I ? <I {...this.props} /> : false
|
||||
}
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
257
framework/web-react/src/components/authority-view/index.jsx
Normal file
257
framework/web-react/src/components/authority-view/index.jsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Card, Checkbox, Descriptions, Empty, Popover, Spin, Tooltip } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import { EMPTY_ID } from 'util/global'
|
||||
|
||||
function generateList(data) {
|
||||
data.forEach(item => {
|
||||
if (item.children && item.children.length) {
|
||||
generateList.call(this, item.children)
|
||||
}
|
||||
this.list.push(item)
|
||||
})
|
||||
}
|
||||
|
||||
function getVisible() {
|
||||
const checked = this.list.filter(item => item.checked)
|
||||
const caseChildren = checked.filter(item => item.visibleParent || item.type != 2)
|
||||
const visibleParents = []
|
||||
// 递归寻找父级
|
||||
const findVisibleParents = children => {
|
||||
const parents = []
|
||||
children.forEach(item => {
|
||||
if (item.parentId) {
|
||||
const parent = this.list.find(p => p.id === item.parentId)
|
||||
if (parent) {
|
||||
parents.push(parent)
|
||||
visibleParents.push(parent)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (parents.length) {
|
||||
findVisibleParents(parents)
|
||||
}
|
||||
}
|
||||
|
||||
findVisibleParents(caseChildren)
|
||||
|
||||
const checkedIds = checked.map(item => item.id)
|
||||
const visibleParentsIds = visibleParents.map(item => item.id)
|
||||
|
||||
const result = checkedIds
|
||||
visibleParentsIds.forEach(item => {
|
||||
if (!result.includes(item)) {
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function renderDescriptions(data) {
|
||||
return data.map(item => {
|
||||
return item.children && item.children.length
|
||||
? renderItem.call(this, item)
|
||||
: renderCheckbox.call(this, item)
|
||||
})
|
||||
}
|
||||
|
||||
function renderItem(data) {
|
||||
return (
|
||||
<Descriptions bordered column={1} contentStyle={{ padding: 0 }}>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<Checkbox
|
||||
value={data.id}
|
||||
checked={data.checked}
|
||||
indeterminate={data.indeterminate}
|
||||
onChange={e => this.onChange(e, data)}
|
||||
>
|
||||
{data.title}
|
||||
</Checkbox>
|
||||
}
|
||||
>
|
||||
<Card bordered={false}>{renderDescriptions.call(this, data.children)}</Card>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)
|
||||
}
|
||||
|
||||
function renderCheckbox(data) {
|
||||
const grid = (
|
||||
<label className="ant-card-grid ant-card-grid-hoverable">
|
||||
<Checkbox value={data.id} checked={data.checked} onChange={e => this.onChange(e, data)}>
|
||||
{data.title}
|
||||
</Checkbox>
|
||||
{data.visibleParent && data.type == 2 && (
|
||||
<Tooltip placement="bottom" title="选中此项才会显示菜单">
|
||||
<AntIcon type="eye" style={{ color: '#1890ff' }} className="mr-xxs" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="text-gray">{data.permission}</div>
|
||||
</label>
|
||||
)
|
||||
return data.remark ? (
|
||||
<Popover placement="topLeft" content={data.remark}>
|
||||
{grid}
|
||||
</Popover>
|
||||
) : (
|
||||
grid
|
||||
)
|
||||
}
|
||||
|
||||
export default class AuthorityView extends Component {
|
||||
state = {
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
}
|
||||
|
||||
list = []
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.autoLoad = typeof this.props.autoLoad === 'boolean' ? this.props.autoLoad : true
|
||||
this.loadData =
|
||||
typeof this.props.loadData === 'function' ? this.props.loadData : async () => {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载数据
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.autoLoad) {
|
||||
this.onLoadData()
|
||||
}
|
||||
}
|
||||
|
||||
onLoadData = async () => {
|
||||
this.setState({ loading: true })
|
||||
|
||||
const res = await this.loadData()
|
||||
|
||||
this.list = []
|
||||
generateList.call(this, res)
|
||||
|
||||
if (this.props.defaultSelectedKeys) {
|
||||
this.list.map(item => {
|
||||
if (
|
||||
this.props.defaultSelectedKeys.includes(item.id) &&
|
||||
(!item.children || !item.children.length)
|
||||
) {
|
||||
this.onSelect(true, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dataSource: res,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onReloadData = () => {
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
onChange = (e, item) => {
|
||||
if (e && item) {
|
||||
this.onSelect(e.target.checked, item)
|
||||
}
|
||||
|
||||
const visible = getVisible.call(this)
|
||||
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(
|
||||
// 返回所有选中
|
||||
this.list.filter(p => p.checked).map(p => p.id),
|
||||
// 返回所有选中和半选
|
||||
this.list.filter(p => p.checked || p.indeterminate).map(p => p.id),
|
||||
// 返回所有选中和半选,但是不返回没有子级选中visibleParent的半选
|
||||
visible
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onSelect = (check, item) => {
|
||||
item.checked = check
|
||||
item.indeterminate = false
|
||||
if (item.children && item.children.length) {
|
||||
this.onChangeChildren(item.checked, item.children)
|
||||
}
|
||||
if (item.parentId) {
|
||||
this.onChangeParent(item.checked, item.parentId)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dataSource: this.list.filter(p => p.parentId === EMPTY_ID),
|
||||
})
|
||||
}
|
||||
|
||||
onChangeParent = (checked, parentId) => {
|
||||
const parent = this.list.find(p => p.id === parentId)
|
||||
if (parent) {
|
||||
const checkedCount = parent.children.filter(p => p.checked).length
|
||||
const indeterminateCount = parent.children.filter(p => p.indeterminate).length
|
||||
if (checkedCount === parent.children.length) {
|
||||
// 全选
|
||||
parent.checked = true
|
||||
parent.indeterminate = false
|
||||
} else if (!checkedCount && !indeterminateCount) {
|
||||
// 全不选
|
||||
parent.checked = false
|
||||
parent.indeterminate = false
|
||||
} else {
|
||||
// 半选
|
||||
parent.checked = false
|
||||
parent.indeterminate = true
|
||||
}
|
||||
this.onChangeParent(checked, parent.parentId)
|
||||
}
|
||||
}
|
||||
|
||||
onChangeChildren = (checked, children) => {
|
||||
children.forEach(p => {
|
||||
p.checked = checked
|
||||
p.indeterminate = false
|
||||
if (p.children && p.children.length) {
|
||||
this.onChangeChildren(checked, p.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="yo-authority-view">
|
||||
<Spin spinning={this.state.loading} indicator={<AntIcon type="loading" />}>
|
||||
{!this.state.loading ? (
|
||||
<Descriptions bordered column={1} className="yo-authority-view--container">
|
||||
{this.state.dataSource.map(item => {
|
||||
return (
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<Checkbox
|
||||
value={item.id}
|
||||
checked={item.checked}
|
||||
indeterminate={item.indeterminate}
|
||||
onChange={e => this.onChange(e, item)}
|
||||
>
|
||||
{item.title}
|
||||
</Checkbox>
|
||||
}
|
||||
>
|
||||
{renderDescriptions.call(this, item.children)}
|
||||
</Descriptions.Item>
|
||||
)
|
||||
})}
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Empty className="mt-lg mb-lg" />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
141
framework/web-react/src/components/authorized/handler.js
Normal file
141
framework/web-react/src/components/authorized/handler.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import store from 'store'
|
||||
|
||||
const { getState } = store
|
||||
|
||||
const stroePath = 'user'
|
||||
|
||||
const authByArray = (auth, permissions) => {
|
||||
|
||||
const flags = []
|
||||
|
||||
auth.forEach(p => {
|
||||
switch (p.constructor) {
|
||||
case String:
|
||||
flags.push([permissions.includes(p), '&&'])
|
||||
break
|
||||
case Array:
|
||||
flags.push([authByArray(p, permissions), '||'])
|
||||
break
|
||||
case Boolean:
|
||||
flags.push([p, '&&'])
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let result
|
||||
|
||||
flags.forEach((p, i) => {
|
||||
if (p[1] === '&&') {
|
||||
if (i === 0) {
|
||||
result = true
|
||||
}
|
||||
if (result) {
|
||||
result = p[0]
|
||||
}
|
||||
} else {
|
||||
if (i === 0) {
|
||||
result = false
|
||||
}
|
||||
if (!result) {
|
||||
result = p[0]
|
||||
}
|
||||
}
|
||||
//result = p[1] === '&&' ? result && p[0] : result || p[0]
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const authByJson = (auth, permissions) => {
|
||||
|
||||
let result = true
|
||||
|
||||
const flags = []
|
||||
|
||||
const deepName = (arr, key) => {
|
||||
arr.forEach((p, i) => {
|
||||
switch (p.constructor) {
|
||||
case String:
|
||||
arr[i] = `${key}:${p}`
|
||||
break
|
||||
case Array:
|
||||
p = deepName(p, key)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
for (let key in auth) {
|
||||
const app = auth[key]
|
||||
switch (app.constructor) {
|
||||
case String:
|
||||
flags.push(permissions.includes(`${key}:${app}`))
|
||||
break
|
||||
case Array:
|
||||
flags.push(authByArray(deepName(app, key), permissions))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
flags.forEach(p => {
|
||||
result = result && p
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
const auth = (auth) => {
|
||||
|
||||
let info = this
|
||||
|
||||
if (!info || !Object.keys(info).length) {
|
||||
info = getState(stroePath)
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员
|
||||
*/
|
||||
if (info.adminType === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
const permissions = info.permissions
|
||||
|
||||
if (!permissions) {
|
||||
return false
|
||||
}
|
||||
|
||||
let flag = false
|
||||
|
||||
if (auth) {
|
||||
switch (auth.constructor) {
|
||||
case String:
|
||||
flag = permissions.includes(auth)
|
||||
break
|
||||
case Array:
|
||||
flag = authByArray(auth, permissions)
|
||||
break
|
||||
case Object:
|
||||
flag = authByJson(auth, permissions)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return flag
|
||||
}
|
||||
|
||||
export default auth
|
||||
72
framework/web-react/src/components/authorized/index.jsx
Normal file
72
framework/web-react/src/components/authorized/index.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* auth: 允许的权限
|
||||
* authExclude: 排除的权限
|
||||
*
|
||||
* auth的几种传值方式
|
||||
* 1.String
|
||||
* 例: auth="sysApp:page"
|
||||
* 直接传入字符串,对单项权限进行验证
|
||||
*
|
||||
* 2.Array
|
||||
* 2.1.单项权限
|
||||
* 例: auth={['sysApp:page']}
|
||||
* 2.2.并且关系多项权限
|
||||
* 例: auth={['sysApp:page', 'sysApp:add']}
|
||||
* 数组中传入多个字符串
|
||||
* 此时验证的是同时拥有"sysApp:page"和"sysApp:add"两项权限才会渲染
|
||||
* 2.3.或者关系多项权限
|
||||
* 例: auth={[['sysApp:page', 'sysApp:add'], ['sysApp:edit']]}
|
||||
* 二维数组结构,内部数组之间为并且关系
|
||||
* 此时验证的是"sysApp:page"&"sysApp:add"||"sysApp:edit"
|
||||
* 注意:或者的条件必须包括在数组中,暴露在外则判定为并且
|
||||
* 2.4.可直接传入布尔值
|
||||
* 例: auth={['sysApp:page', 1 === 1]}
|
||||
* auth={[['sysApp:page', 'sysApp:add'], [1 === 1]]}
|
||||
*
|
||||
* 3.Json
|
||||
* 如果觉得多项权限时每次都要写应用编号比较繁琐,可对Array形式进行简化
|
||||
* 3.1.单项权限
|
||||
* 例: auth={{ sysApp: 'page' }}
|
||||
* 3.2.并且关系多项权限
|
||||
* 例: auth={{ sysApp: ['page', 'add'] }}
|
||||
* 3.3.或者关系多项权限
|
||||
* 例: auth={{ sysApp: [['page', 'add'], ['edit']]}}
|
||||
* 3.4.可直接传入布尔值
|
||||
* 例: auth={{ sysApp: ['page', 1 === 1] }}
|
||||
* auth={{ sysApp: [['page', 'add'], [1 === 1]] }}
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react'
|
||||
import store from 'store'
|
||||
import auth from './handler'
|
||||
|
||||
const { getState, subscribe } = store
|
||||
|
||||
const stroePath = 'user'
|
||||
|
||||
export default class Auth extends Component {
|
||||
state = getState(stroePath)
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.unsubscribe = subscribe(stroePath, () => {
|
||||
this.setState(getState(stroePath))
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe()
|
||||
}
|
||||
|
||||
render() {
|
||||
const flag = auth.call(this.state, this.props.auth)
|
||||
|
||||
if (flag) {
|
||||
return this.props.children || <></>
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
143
framework/web-react/src/components/business/house-log/index.jsx
Normal file
143
framework/web-react/src/components/business/house-log/index.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Button, Descriptions, Spin, Steps, Timeline } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import { api } from 'common/api'
|
||||
import getDictData from 'util/dic'
|
||||
import { toCamelCase } from 'util/format'
|
||||
|
||||
const ellipsisType = [3, 4, 6]
|
||||
|
||||
export default class houseLog extends Component {
|
||||
state = {
|
||||
loading: true,
|
||||
codes: {
|
||||
houseLogType: [],
|
||||
},
|
||||
data: [],
|
||||
ellipsis: true,
|
||||
}
|
||||
|
||||
ellipsisFlag = []
|
||||
|
||||
async componentDidMount() {
|
||||
const { id, infoId, taskId } = this.props
|
||||
const state = { loading: false }
|
||||
|
||||
state.codes = await getDictData('house_log_type')
|
||||
|
||||
if (id) {
|
||||
const { data } = await api.houseLogList({ id })
|
||||
state.data = data
|
||||
} else if (infoId) {
|
||||
const { data } = await api.houseLogListByInfoId({ id: infoId })
|
||||
state.data = data
|
||||
} else if (taskId) {
|
||||
const { data } = await api.houseLogListByTaskId({ id: taskId })
|
||||
state.data = data
|
||||
}
|
||||
this.setState(state)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, data, ellipsis } = this.state
|
||||
|
||||
let button = false
|
||||
|
||||
return (
|
||||
<Spin spinning={loading} indicator={<AntIcon type="loading" />} className="h-400-min">
|
||||
<Timeline mode="left">
|
||||
{data.map((item, i) => {
|
||||
let show = true
|
||||
if (
|
||||
ellipsisType.includes(item.type) &&
|
||||
!this.ellipsisFlag.includes(item.type)
|
||||
) {
|
||||
this.ellipsisFlag.push(item.type)
|
||||
} else if (
|
||||
ellipsisType.includes(item.type) &&
|
||||
this.ellipsisFlag.includes(item.type)
|
||||
) {
|
||||
show = false
|
||||
}
|
||||
if (show || !ellipsis) {
|
||||
return (
|
||||
<Timeline.Item
|
||||
key={i}
|
||||
dot={
|
||||
item.type === 6 ? (
|
||||
<AntIcon type="close-circle" className="text-error" />
|
||||
) : (
|
||||
[
|
||||
,
|
||||
<AntIcon type="clock-circle" />,
|
||||
<AntIcon
|
||||
type="check-circle"
|
||||
className="text-success"
|
||||
/>,
|
||||
][item.status]
|
||||
)
|
||||
}
|
||||
>
|
||||
<h6 className="h6">
|
||||
<b>
|
||||
{['等待', '正在', ''][item.status] +
|
||||
this.bindCodeValue(item.type, 'house_log_type')}
|
||||
</b>
|
||||
</h6>
|
||||
<p className="text-gray">{item.finishedTime}</p>
|
||||
<Descriptions
|
||||
column={1}
|
||||
colon={false}
|
||||
labelStyle={{ opacity: 0.5 }}
|
||||
>
|
||||
{item.remark && (
|
||||
<Descriptions.Item>{item.remark}</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="操作人">
|
||||
{item.targetUserNames.split(',').join(' / ')}
|
||||
</Descriptions.Item>
|
||||
{item.finishedUserName && (
|
||||
<Descriptions.Item label="实际操作人">
|
||||
{item.finishedUserName}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</Timeline.Item>
|
||||
)
|
||||
} else if (!button) {
|
||||
button = true
|
||||
return (
|
||||
<Timeline.Item
|
||||
className="pt-md pb-md"
|
||||
dot={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AntIcon type="ellipsis" />}
|
||||
onClick={() => this.setState({ ellipsis: false })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<br />
|
||||
</Timeline.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
})}
|
||||
</Timeline>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export default class ComponentDynamic extends Component {
|
||||
|
||||
state = {
|
||||
key: null,
|
||||
component: null
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadComponent()
|
||||
}
|
||||
|
||||
async loadComponent() {
|
||||
let component;
|
||||
|
||||
try {
|
||||
if (this.props.is) {
|
||||
if (this.props.is.constructor === Function) {
|
||||
// 导入函数
|
||||
component = await this.props.is()
|
||||
} else {
|
||||
// 导入路径,必须是src以下节点,如 pages/home
|
||||
component = await import(`../../${this.props.is}`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
component = await import(`views/error/404`)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
key: Math.random().toString(16).slice(2),
|
||||
component: component.default
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const props = cloneDeep(this.props)
|
||||
|
||||
delete props.is
|
||||
|
||||
if (this.state.component) {
|
||||
return <this.state.component key={this.state.key} {...props} />
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
23
framework/web-react/src/components/container/index.jsx
Normal file
23
framework/web-react/src/components/container/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export default class Container extends Component {
|
||||
getMode(mode) {
|
||||
const c = 'container'
|
||||
const modes = ['xxs', 'xs', 'sm', 'md', 'fluid']
|
||||
if (modes.includes(mode)) {
|
||||
return `${c}-${mode}`
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mode, className, children } = this.props
|
||||
|
||||
let containerName = this.getMode(mode)
|
||||
if (className) {
|
||||
containerName = [containerName, className].join(' ')
|
||||
}
|
||||
|
||||
return <section className={containerName}>{children}</section>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Component } from 'react'
|
||||
import BraftEditor from 'braft-editor'
|
||||
import 'braft-editor/dist/index.css'
|
||||
|
||||
export default class index extends Component {
|
||||
state = {
|
||||
editorState: '',
|
||||
outputHTML: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* mount后回调
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.isLivinig = true
|
||||
this.toParent()
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.isLivinig = false
|
||||
}
|
||||
handleChange = editorState => {
|
||||
const outputHTML = editorState.toHTML()
|
||||
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
editorState: editorState,
|
||||
outputHTML,
|
||||
})
|
||||
|
||||
onChange && onChange(outputHTML)
|
||||
}
|
||||
|
||||
setEditorContentAsync = () => {
|
||||
this.isLivinig &&
|
||||
this.setState({
|
||||
editorState: BraftEditor.createEditorState(this.props.value),
|
||||
})
|
||||
}
|
||||
|
||||
//给父控件 调用这个方法 因为只有父控件才能掌握页面加载完的时间
|
||||
toParent = () => {
|
||||
this.props.parent.getChildrenMsg(this.setEditorContentAsync)
|
||||
}
|
||||
render() {
|
||||
const { editorState } = this.state
|
||||
const controls = ['bold', 'italic', 'underline', 'text-color', 'separator']
|
||||
|
||||
return (
|
||||
<BraftEditor
|
||||
value={editorState}
|
||||
controls={controls}
|
||||
onChange={this.handleChange}
|
||||
placeholder={'请输入内容'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Col, Row, Select } from 'antd'
|
||||
import { ChromePicker } from 'react-color'
|
||||
|
||||
export default class index extends Component {
|
||||
select = React.createRef()
|
||||
|
||||
state = {
|
||||
color: null,
|
||||
}
|
||||
|
||||
onChange(color) {
|
||||
this.setState({ color: color.hex })
|
||||
}
|
||||
|
||||
onChangeComplete(color) {
|
||||
this.props.onChange && this.props.onChange(color.hex)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { color } = this.state
|
||||
|
||||
const { value } = this.props
|
||||
|
||||
return (
|
||||
<Row gutter={8}>
|
||||
<Col flex="1">
|
||||
<Select
|
||||
ref={this.select}
|
||||
dropdownRender={() => (
|
||||
<ChromePicker
|
||||
color={color || value || '#000'}
|
||||
disableAlpha
|
||||
onChange={color => this.onChange(color)}
|
||||
onChangeComplete={color => this.onChangeComplete(color)}
|
||||
/>
|
||||
)}
|
||||
showArrow={false}
|
||||
{...this.props}
|
||||
value={value}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="32px">
|
||||
<div
|
||||
className="color-selector--palette"
|
||||
style={{ backgroundColor: color || value || '#000' }}
|
||||
onClick={() => {
|
||||
this.select.current.focus()
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Col, Input, InputNumber, Row } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
|
||||
export default class index extends Component {
|
||||
render() {
|
||||
const { unit, placeholder, value, onChange } = this.props
|
||||
|
||||
console.log(value)
|
||||
|
||||
return (
|
||||
<Input.Group>
|
||||
<Row align="middle">
|
||||
<Col flex="1">
|
||||
<InputNumber
|
||||
{...this.props}
|
||||
className="w-100-p"
|
||||
placeholder={placeholder && placeholder[0]}
|
||||
value={value && value[0]}
|
||||
onChange={e => {
|
||||
const result = [e, value && value[1]]
|
||||
onChange && onChange(result)
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<span className="yo-addon">
|
||||
<AntIcon className="text-gray" type="swap-right" />
|
||||
</span>
|
||||
<Col flex="1">
|
||||
<InputNumber
|
||||
{...this.props}
|
||||
className="w-100-p"
|
||||
placeholder={placeholder && placeholder[1]}
|
||||
value={value && value[1]}
|
||||
onChange={e => {
|
||||
const result = [value && value[0], e]
|
||||
onChange && onChange(result)
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
{unit && <span className="yo-addon">{unit}</span>}
|
||||
</Row>
|
||||
</Input.Group>
|
||||
)
|
||||
}
|
||||
}
|
||||
34
framework/web-react/src/components/icon-selector/icons.js
Normal file
34
framework/web-react/src/components/icon-selector/icons.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const icons = [
|
||||
{
|
||||
key: 'directional',
|
||||
title: '方向性图标',
|
||||
icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
|
||||
},
|
||||
{
|
||||
key: 'suggested',
|
||||
title: '提示建议性图标',
|
||||
icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
|
||||
},
|
||||
{
|
||||
key: 'editor',
|
||||
title: '编辑类图标',
|
||||
icons: ['edit', 'form', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'column-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
|
||||
},
|
||||
{
|
||||
key: 'data',
|
||||
title: '数据类图标',
|
||||
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
|
||||
},
|
||||
{
|
||||
key: 'brand_logo',
|
||||
title: '网站通用图标',
|
||||
icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interaction', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
|
||||
},
|
||||
{
|
||||
key: 'application',
|
||||
title: '品牌和标识',
|
||||
icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingding', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
|
||||
}
|
||||
]
|
||||
|
||||
export default icons
|
||||
87
framework/web-react/src/components/icon-selector/index.jsx
Normal file
87
framework/web-react/src/components/icon-selector/index.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Card, Drawer, Tabs } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import icons from './icons'
|
||||
|
||||
export default class IconSelector extends Component {
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
activeKey: icons[0].key,
|
||||
selected: ''
|
||||
}
|
||||
|
||||
open = (icon) => {
|
||||
if (icon) {
|
||||
const activeKey = (icons.find(p => p.icons.includes(icon)) || icons[0]).key
|
||||
this.setState({
|
||||
visible: true,
|
||||
activeKey,
|
||||
selected: icon
|
||||
})
|
||||
} else {
|
||||
this.setState({ visible: true })
|
||||
}
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.setState({ visible: false })
|
||||
}
|
||||
|
||||
onSelectIcon = (icon) => {
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(icon)
|
||||
}
|
||||
this.close()
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { visible, activeKey, selected } = this.state
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
visible={visible}
|
||||
className="yo-icon-selector"
|
||||
title="选择图标"
|
||||
width={600}
|
||||
destroyOnClose
|
||||
onClose={() => this.close()}
|
||||
>
|
||||
<Tabs
|
||||
tabPosition="left"
|
||||
activeKey={activeKey}
|
||||
onChange={(activeKey) => this.setState({ activeKey })}
|
||||
>
|
||||
{
|
||||
icons.map(iconGroup => {
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
key={iconGroup.key}
|
||||
tab={iconGroup.title}
|
||||
>
|
||||
<Card>
|
||||
{
|
||||
iconGroup.icons.map(icon => {
|
||||
return (
|
||||
<Card.Grid
|
||||
key={icon}
|
||||
className={selected === icon ? 'yo-icon--selected' : ''}
|
||||
onClick={() => this.onSelectIcon(icon)}
|
||||
>
|
||||
<AntIcon type={icon} style={{ fontSize: '36px' }} />
|
||||
<span>{icon}</span>
|
||||
</Card.Grid>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
}
|
||||
53
framework/web-react/src/components/image/index.jsx
Normal file
53
framework/web-react/src/components/image/index.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Avatar } from 'antd'
|
||||
import { isEqual } from 'lodash'
|
||||
import { PreviewFileBase64 } from 'util/file'
|
||||
|
||||
const getSrc = async id => {
|
||||
if (id) {
|
||||
const base64 = await PreviewFileBase64(id)
|
||||
return base64
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export default class Image extends Component {
|
||||
state = {
|
||||
src: '',
|
||||
}
|
||||
|
||||
id = ''
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.setSrc()
|
||||
}
|
||||
|
||||
shouldComponentUpdate(props, state) {
|
||||
// props变更或state.src变更时进行渲染
|
||||
return !isEqual(this.props, props) || this.state.src !== state.src
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.id !== this.props.id && this.props.id) {
|
||||
this.setSrc()
|
||||
}
|
||||
}
|
||||
|
||||
setSrc = async () => {
|
||||
if (this.props.id) {
|
||||
this.id = this.props.id
|
||||
const src = await getSrc(this.id)
|
||||
this.setState({ src })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.type === 'avatar') {
|
||||
return <Avatar src={this.state.src} {...this.props} />
|
||||
} else {
|
||||
return <img src={this.state.src} {...this.props} alt="" />
|
||||
}
|
||||
}
|
||||
}
|
||||
19
framework/web-react/src/components/index.js
Normal file
19
framework/web-react/src/components/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export { default as AntIcon } from './ant-icon'
|
||||
export { default as AuthorityView } from './authority-view'
|
||||
export { default as Auth } from './authorized'
|
||||
export { default as BraftEditor } from './form/braft-editor'
|
||||
export { default as ColorSelector } from './form/color-selector'
|
||||
export { default as ComponentDynamic } from './component-dynamic'
|
||||
export { default as Container } from './container'
|
||||
export { default as IconSelector } from './icon-selector'
|
||||
export { default as Image } from './image'
|
||||
export { default as InputNumberRange } from './form/input-number-range'
|
||||
export { default as ModalForm } from './modal-form'
|
||||
export { default as NoticeDetail } from './notice-detail'
|
||||
export { default as PhotoPreview } from './photo-preview'
|
||||
export { default as QueryList } from './query-list'
|
||||
export { default as QueryTable } from './query-table'
|
||||
export { default as QueryTableActions } from './query-table-actions'
|
||||
export { default as QueryTreeLayout } from './query-tree-layout'
|
||||
|
||||
export { default as HouseLog } from './business/house-log'
|
||||
234
framework/web-react/src/components/modal-form/index.jsx
Normal file
234
framework/web-react/src/components/modal-form/index.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Button, Drawer, message as Message, Modal } from 'antd'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
|
||||
/**
|
||||
* 渲染对话框
|
||||
* @param {*} props
|
||||
* @param {*} on
|
||||
* @param {*} childWithProps
|
||||
* @returns
|
||||
*/
|
||||
function renderModal(props, on, childWithProps) {
|
||||
on = {
|
||||
...on,
|
||||
onCancel: () => this.onClose(),
|
||||
}
|
||||
|
||||
const { buttons } = this.props
|
||||
|
||||
const _buttons = [
|
||||
<Button onClick={() => on.onCancel()}>取消</Button>,
|
||||
<Button loading={props.confirmLoading} onClick={() => on.onOk()} type="primary">
|
||||
确认
|
||||
</Button>,
|
||||
]
|
||||
|
||||
if (Array.isArray(buttons)) {
|
||||
for (const { index, button } of buttons) {
|
||||
_buttons.splice(index, 0, button(this.getData, this.close))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal className="yo-modal-form" {...props} {...on} footer={_buttons}>
|
||||
{childWithProps}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染抽屉
|
||||
* @param {*} props
|
||||
* @param {*} on
|
||||
* @param {*} childWithProps
|
||||
* @returns
|
||||
*/
|
||||
function renderDrawer(props, on, childWithProps) {
|
||||
on = {
|
||||
...on,
|
||||
onClose: () => this.onClose(),
|
||||
}
|
||||
|
||||
// react在这里会对该组件不存在的props抛出异常
|
||||
;['action', 'onSuccess', 'onOk', 'confirmLoading'].forEach(p => {
|
||||
delete props[p]
|
||||
})
|
||||
|
||||
return (
|
||||
<Drawer className="yo-drawer-form" {...props} onClose={() => on.onClose()}>
|
||||
<div className="yo-drawer-form--body">{childWithProps}</div>
|
||||
<div className="ant-drawer-footer">
|
||||
<Button onClick={on.onClose}>取消</Button>
|
||||
<Button loading={this.state.confirmLoading} onClick={on.onOk} type="primary">
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default class ModalForm extends Component {
|
||||
state = {
|
||||
// 弹窗显示/隐藏
|
||||
visible: false,
|
||||
// 提交状态
|
||||
confirmLoading: false,
|
||||
}
|
||||
|
||||
// 子元素实例
|
||||
childNode = React.createRef()
|
||||
|
||||
// 弹窗类型
|
||||
type = 'modal'
|
||||
|
||||
// 从外部传入的数据
|
||||
data = null
|
||||
// 数据结构调整后的快照
|
||||
snapshot = null
|
||||
|
||||
// 完成操作
|
||||
action = async () => {}
|
||||
|
||||
// 是否在关闭时校验数据改变
|
||||
compareOnClose = true
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
if (this.props.type) {
|
||||
if (!['modal', 'drawer'].includes(this.props.type))
|
||||
throw new Error('props [type] error')
|
||||
this.type = this.props.type
|
||||
}
|
||||
|
||||
if (this.props.action) {
|
||||
this.action = this.props.action
|
||||
}
|
||||
|
||||
if (typeof this.props.compareOnClose === 'boolean') {
|
||||
this.compareOnClose = this.props.compareOnClose
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param {*} data
|
||||
*/
|
||||
open = (data = {}) => {
|
||||
this.data = data
|
||||
this.setState({ visible: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
close = () => {
|
||||
this.setState({ visible: false })
|
||||
}
|
||||
|
||||
getData = async () => {
|
||||
const body = this.childNode.current
|
||||
if (!body || !body.getData) throw Error('为获取到子表单')
|
||||
|
||||
return await body.getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 子元素创建后回调
|
||||
* 对子元素数据进行填充,(如需关闭时对比)之后再获取结构调整后的数据快照
|
||||
* @returns
|
||||
*/
|
||||
onCreated = async () => {
|
||||
const body = this.childNode.current
|
||||
if (!body || !body.fillData) return
|
||||
|
||||
await body.fillData(this.data)
|
||||
// 保存此时的form内容为快照
|
||||
if (this.compareOnClose && body.form && body.form.current) {
|
||||
this.snapshot = cloneDeep(body.form.current.getFieldsValue())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消编辑
|
||||
* (如需关闭时对比)获取当前数据结构与快照对比
|
||||
* @returns
|
||||
*/
|
||||
onClose = async () => {
|
||||
const body = this.childNode.current
|
||||
if (!body) {
|
||||
this.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.compareOnClose && body.form && body.form.current) {
|
||||
const formData = body.form.current.getFieldsValue()
|
||||
if (!isEqual(this.snapshot, formData)) {
|
||||
Modal.confirm({
|
||||
title: '是否确认关闭',
|
||||
content: '当前内容已更改,是否确认不保存并且关闭',
|
||||
onOk: () => {
|
||||
this.close()
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成编辑
|
||||
* 校验并获取结构调整后的数据,调用this.action进行操作
|
||||
* @returns
|
||||
*/
|
||||
onOk = async () => {
|
||||
const body = this.childNode.current
|
||||
if (!body || !this.action || !body.getData) return
|
||||
|
||||
this.setState({ confirmLoading: true })
|
||||
try {
|
||||
const postData = await body.getData()
|
||||
|
||||
const result = await this.action(postData)
|
||||
if (!result || result.success) {
|
||||
if (result && result.success) {
|
||||
Message.success(this.props.successMessage || '保存成功')
|
||||
}
|
||||
this.close()
|
||||
if (typeof this.props.onSuccess === 'function') {
|
||||
this.props.onSuccess(postData)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.setState({ confirmLoading: false })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
visible: this.state.visible,
|
||||
destroyOnClose: true,
|
||||
|
||||
confirmLoading: this.state.confirmLoading,
|
||||
}
|
||||
|
||||
const on = {
|
||||
onOk: () => this.onOk(),
|
||||
}
|
||||
|
||||
const childWithProps = React.cloneElement(React.Children.only(this.props.children), {
|
||||
created: childNode => {
|
||||
this.childNode.current = childNode
|
||||
this.onCreated()
|
||||
},
|
||||
})
|
||||
|
||||
return this.type === 'modal'
|
||||
? renderModal.call(this, props, on, childWithProps)
|
||||
: renderDrawer.call(this, props, on, childWithProps)
|
||||
}
|
||||
}
|
||||
122
framework/web-react/src/components/notice-detail/index.jsx
Normal file
122
framework/web-react/src/components/notice-detail/index.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Col, Divider, Modal, Row, Spin, Upload } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import { api } from 'common/api'
|
||||
import { BlobToBase64, GetFileName, PreviewFile } from 'util/file'
|
||||
import store from 'store'
|
||||
|
||||
const { dispatch } = store
|
||||
|
||||
export default class index extends Component {
|
||||
state = {
|
||||
detailVisible: false,
|
||||
detailLoading: false,
|
||||
detailData: {},
|
||||
fileList: [],
|
||||
}
|
||||
|
||||
onOpenDetail = async id => {
|
||||
this.setState({ detailLoading: true, detailVisible: true })
|
||||
const { data } = await api.sysNoticeDetail({ id })
|
||||
|
||||
let fileList = []
|
||||
if (data) {
|
||||
const { attachments } = data
|
||||
if (attachments) {
|
||||
const fileIds = attachments.split(',')
|
||||
for (const fileId of fileIds) {
|
||||
try {
|
||||
const file = await PreviewFile(fileId)
|
||||
const base64 = await BlobToBase64(file)
|
||||
fileList.push({
|
||||
uid: fileId,
|
||||
response: fileId,
|
||||
name: file.name,
|
||||
url: base64,
|
||||
status: 'done',
|
||||
})
|
||||
} catch {
|
||||
const { data: file } = await api.sysFileInfoDetail({ id: fileId })
|
||||
fileList.push({
|
||||
uid: fileId,
|
||||
response: '文件已丢失',
|
||||
name: file.fileOriginName,
|
||||
status: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'READ_NOTICE',
|
||||
id,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
detailLoading: false,
|
||||
detailData: data,
|
||||
fileList,
|
||||
})
|
||||
}
|
||||
|
||||
onCloseDetail() {
|
||||
this.setState({ detailVisible: false, detailData: {}, fileList: [] })
|
||||
}
|
||||
|
||||
async onFileDownload(file) {
|
||||
const { data, headers } = await api.sysFileInfoDownload({ id: file.response })
|
||||
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()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { detailLoading, detailVisible, detailData, fileList } = this.state
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={1000}
|
||||
footer={false}
|
||||
visible={detailVisible}
|
||||
onCancel={() => this.onCloseDetail()}
|
||||
>
|
||||
<Spin spinning={detailLoading} indicator={<AntIcon type="loading" />}>
|
||||
<div className="h3 mt-lg">{detailData.title}</div>
|
||||
<Divider />
|
||||
<div
|
||||
className="pt-lg pb-lg"
|
||||
dangerouslySetInnerHTML={{ __html: detailData.content }}
|
||||
></div>
|
||||
<Divider />
|
||||
{!!fileList.length && (
|
||||
<>
|
||||
<Row align="top">
|
||||
<span className="text-gray mt-sm mr-xs">附件</span>
|
||||
<Col flex="1">
|
||||
<Upload
|
||||
fileList={fileList}
|
||||
showUploadList={{
|
||||
showDownloadIcon: true,
|
||||
}}
|
||||
onDownload={file => this.onFileDownload(file)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Row justify="space-between" className="text-gray">
|
||||
<span>发布人:{detailData.publicUserName}</span>
|
||||
<span>发布时间:{detailData.publicTime} </span>
|
||||
</Row>
|
||||
</Spin>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
74
framework/web-react/src/components/photo-preview/index.jsx
Normal file
74
framework/web-react/src/components/photo-preview/index.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react'
|
||||
import 'photoswipe/dist/photoswipe.css'
|
||||
import 'photoswipe/dist/default-skin/default-skin.css'
|
||||
import PhotoSwipe from 'photoswipe'
|
||||
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default'
|
||||
|
||||
const defaultOptions = {
|
||||
index: 0,
|
||||
bgOpacity: 0.75,
|
||||
}
|
||||
|
||||
export default class PhotoPreview extends Component {
|
||||
initPhotoSwipe = (items = [], options = {}) => {
|
||||
const pswpElement = this.refs.pswp
|
||||
const gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
})
|
||||
gallery.init()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="pswp" tabIndex="-1" ref="pswp">
|
||||
<div className="pswp__bg"></div>
|
||||
<div className="pswp__scroll-wrap">
|
||||
<div className="pswp__container">
|
||||
<div className="pswp__item"></div>
|
||||
<div className="pswp__item"></div>
|
||||
<div className="pswp__item"></div>
|
||||
</div>
|
||||
<div className="pswp__ui pswp__ui--hidden">
|
||||
<div className="pswp__top-bar">
|
||||
<div className="pswp__counter"></div>
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title="Close (Esc)"
|
||||
></button>
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title="Toggle fullscreen"
|
||||
></button>
|
||||
<button
|
||||
className="pswp__button pswp__button--zoom"
|
||||
title="Zoom in/out"
|
||||
></button>
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
<div className="pswp__preloader__cut">
|
||||
<div className="pswp__preloader__donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div className="pswp__share-tooltip"></div>
|
||||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title="Previous (arrow left)"
|
||||
></button>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title="Next (arrow right)"
|
||||
></button>
|
||||
<div className="pswp__caption">
|
||||
<div className="pswp__caption__center"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
270
framework/web-react/src/components/query-list/index.jsx
Normal file
270
framework/web-react/src/components/query-list/index.jsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Button, Form, List, Pagination, Spin, Tooltip } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const propsMap = ['autoLoad', 'loadData', 'pageIndex', 'pageSize']
|
||||
|
||||
/**
|
||||
* 渲染查询栏
|
||||
* @returns
|
||||
*/
|
||||
function renderQueryBar() {
|
||||
const { query, moreQuery } = this.props
|
||||
|
||||
return (
|
||||
<div className="yo-query-bar">
|
||||
<Form
|
||||
layout="inline"
|
||||
ref={this.form}
|
||||
onFinish={value => this.onQuery(value)}
|
||||
initialValues={this.props.queryInitialValues}
|
||||
>
|
||||
{query}
|
||||
<Form.Item>
|
||||
<Button.Group className="mr-xs">
|
||||
<Button htmlType="submit" type="primary" icon={<AntIcon type="search" />}>
|
||||
查询
|
||||
</Button>
|
||||
<Tooltip placement="bottom" title="重置查询">
|
||||
<Button
|
||||
onClick={() => this.onResetQuery()}
|
||||
icon={<AntIcon type="undo" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
{moreQuery && <Button>更多查询条件</Button>}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default class QueryList extends Component {
|
||||
state = {
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
|
||||
ping: [],
|
||||
}
|
||||
|
||||
// 查询表单实例
|
||||
form = React.createRef()
|
||||
|
||||
// 查询值
|
||||
query = {}
|
||||
|
||||
// 分页器配置
|
||||
pagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
size: 'small',
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: total => `总共${total}条数据`,
|
||||
}
|
||||
|
||||
// 默认选中页码
|
||||
pageIndex = 1
|
||||
// 默认页面尺寸
|
||||
pageSize = 10
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.autoLoad = typeof this.props.autoLoad === 'boolean' ? this.props.autoLoad : true
|
||||
this.loadData =
|
||||
typeof this.props.loadData === 'function' ? this.props.loadData : async () => {}
|
||||
|
||||
if (this.props.pageIndex) {
|
||||
this.pageIndex = this.props.pageIndex
|
||||
this.pagination.current = this.pageIndex
|
||||
}
|
||||
if (this.props.pageSize) {
|
||||
this.pageSize = this.props.pageSize
|
||||
this.pagination.pageSize = this.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载数据
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.autoLoad) {
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onResizeScroll)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onResizeScroll)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
* 调用外部传入的loadData函数,可在loadData中自行改变参数
|
||||
*/
|
||||
onLoadData = async () => {
|
||||
this.onLoading()
|
||||
|
||||
const res = await this.loadData(
|
||||
{
|
||||
pageIndex: this.pagination.current,
|
||||
pageSize: this.pagination.pageSize,
|
||||
},
|
||||
cloneDeep(this.query)
|
||||
)
|
||||
|
||||
this.setState(
|
||||
{
|
||||
dataSource: res.rows || res.data || res.items,
|
||||
},
|
||||
() => {
|
||||
this.onResizeScroll()
|
||||
}
|
||||
)
|
||||
|
||||
this.pagination.total = res.totalCount
|
||||
|
||||
this.onLoaded()
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据开始加载
|
||||
*/
|
||||
onLoading = () => {
|
||||
this.setState({ loading: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据加载完成
|
||||
*/
|
||||
onLoaded = () => {
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行查询
|
||||
* 返回表单字段值,加载数据,并且返回到第一页
|
||||
* @param {*} values
|
||||
*/
|
||||
onQuery = values => {
|
||||
this.query = values
|
||||
this.onReloadData(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置查询
|
||||
* 初始化表单字段值,加载数据,并返回到第一页
|
||||
*/
|
||||
onResetQuery = () => {
|
||||
this.form.current.resetFields()
|
||||
this.query = {}
|
||||
this.onReloadData(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载表格数据
|
||||
* @param {Boolean} resetPage 是否重置页码
|
||||
*/
|
||||
onReloadData = (resetPage = false) => {
|
||||
if (resetPage) {
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
current: this.pageIndex,
|
||||
}
|
||||
}
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
onListChange = (current, pageSize) => {
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
current,
|
||||
pageSize,
|
||||
}
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
onResizeScroll = () => {
|
||||
this.onListScrollX({ target: this.refs['scroll-x'] })
|
||||
}
|
||||
|
||||
onListScrollX(e) {
|
||||
const { offsetWidth, scrollWidth, scrollLeft } = e.target
|
||||
const ping = []
|
||||
if (offsetWidth + scrollLeft < scrollWidth && scrollLeft > 0) {
|
||||
ping.push(...['left', 'right'])
|
||||
} else if (offsetWidth + scrollLeft < scrollWidth) {
|
||||
ping.push('right')
|
||||
} else if (offsetWidth + scrollLeft >= scrollWidth && offsetWidth < scrollWidth) {
|
||||
ping.push('left')
|
||||
}
|
||||
this.setState({ ping })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, dataSource, ping } = this.state
|
||||
|
||||
const attrs = {}
|
||||
Object.keys(this.props).forEach(key => {
|
||||
if (!propsMap.includes(key)) {
|
||||
attrs[key] = this.props[key]
|
||||
}
|
||||
})
|
||||
|
||||
const props = {
|
||||
dataSource: this.state.dataSource,
|
||||
rowKey: record => record.id || Math.random().toString(16).slice(2),
|
||||
...attrs,
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
{this.props.query && renderQueryBar.call(this)}
|
||||
<div className="yo-action-bar">
|
||||
<div className="yo-action-bar--actions">{this.props.operator}</div>
|
||||
<div className="yo-action-bar--actions">
|
||||
<Button.Group>
|
||||
<Tooltip placement="bottom" title="刷新">
|
||||
<Button
|
||||
onClick={() => this.onReloadData()}
|
||||
type="text"
|
||||
icon={<AntIcon type="reload" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
<div className="yo-list">
|
||||
<Spin spinning={loading} indicator={<AntIcon type="loading" />}>
|
||||
<div
|
||||
className={['yo-list-container', ...ping.map(p => `yo-list--ping-${p}`)]
|
||||
.filter(p => p)
|
||||
.join(' ')}
|
||||
>
|
||||
<div
|
||||
className="yo-list--scroll"
|
||||
ref="scroll-x"
|
||||
onScroll={e => this.onListScrollX(e)}
|
||||
>
|
||||
<List {...props} />
|
||||
</div>
|
||||
</div>
|
||||
{!!dataSource && !!dataSource.length && (
|
||||
<Pagination
|
||||
size="small"
|
||||
{...this.pagination}
|
||||
onChange={(current, pageSize) =>
|
||||
this.onListChange(current, pageSize)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Divider } from 'antd'
|
||||
|
||||
function renderActions() {
|
||||
const { children } = this.props
|
||||
const actions = []
|
||||
|
||||
Array.isArray(children)
|
||||
? children
|
||||
.filter(action => action)
|
||||
.forEach((action, i) => {
|
||||
actions.push(action, <Divider type="vertical" key={i} />)
|
||||
})
|
||||
: actions.push(children)
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
export default class QueryTableActions extends Component {
|
||||
componentDidMount() {
|
||||
// 删除多余的间隔线
|
||||
const className = 'ant-divider ant-divider-vertical'
|
||||
let series = false
|
||||
for (const node of this.refs.inner.childNodes) {
|
||||
if (
|
||||
(series && node.className == className) ||
|
||||
(!node.nextElementSibling && node.className == className)
|
||||
) {
|
||||
node.style.display = 'none'
|
||||
series = false
|
||||
} else if (node.className == className) {
|
||||
series = true
|
||||
} else {
|
||||
series = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="yo-table-actions">
|
||||
<div className="yo-table-actions--inner" ref="inner">
|
||||
{renderActions.call(this)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
426
framework/web-react/src/components/query-table/index.jsx
Normal file
426
framework/web-react/src/components/query-table/index.jsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Form, Button, Table, Tooltip, Drawer } from 'antd'
|
||||
import { AntIcon } from 'components'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
|
||||
const propsMap = ['columns', 'autoLoad', 'loadData', 'pageIndex', 'pageSize']
|
||||
|
||||
const clearChildren = data => {
|
||||
data.forEach(p => {
|
||||
if (p.children) {
|
||||
if (p.children.length) {
|
||||
p.children = clearChildren(p.children)
|
||||
} else {
|
||||
delete p.children
|
||||
}
|
||||
}
|
||||
})
|
||||
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
|
||||
*/
|
||||
function renderQueryBar() {
|
||||
const { query, moreQuery, onQueryChange, queryInitialValues } = this.props
|
||||
|
||||
return (
|
||||
<div className="yo-query-bar">
|
||||
<Form
|
||||
layout="inline"
|
||||
ref={this.queryForm}
|
||||
onFinish={value => this.onQuery(value)}
|
||||
initialValues={queryInitialValues}
|
||||
onValuesChange={(changedValues, allValues) =>
|
||||
onQueryChange &&
|
||||
this.queryForm.current.setFieldsValue(onQueryChange(changedValues, allValues))
|
||||
}
|
||||
>
|
||||
{query}
|
||||
<Form.Item>
|
||||
<Button.Group className="mr-xs">
|
||||
<Button htmlType="submit" type="primary" icon={<AntIcon type="search" />}>
|
||||
查询
|
||||
</Button>
|
||||
<Tooltip placement="bottom" title="重置查询">
|
||||
<Button
|
||||
onClick={() => this.onResetQuery(this.queryForm)}
|
||||
icon={<AntIcon type="undo" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
{moreQuery && (
|
||||
<Button type="text" onClick={() => this.onOpenMoreQuery()}>
|
||||
更多查询条件
|
||||
</Button>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderMoreQueryBody() {
|
||||
const { moreQueryVisible } = this.state
|
||||
|
||||
const { moreQuery, onQueryChange, queryInitialValues } = this.props
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width={700}
|
||||
title="查询"
|
||||
className="yo-drawer-form"
|
||||
visible={moreQueryVisible}
|
||||
forceRender
|
||||
onClose={() => this.setState({ moreQueryVisible: false })}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
ref={this.moreQueryForm}
|
||||
initialValues={queryInitialValues}
|
||||
onValuesChange={(changedValues, allValues) =>
|
||||
onQueryChange &&
|
||||
this.moreQueryForm.current.setFieldsValue(
|
||||
onQueryChange(changedValues, allValues)
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="yo-drawer-form--body">{moreQuery}</div>
|
||||
<div className="ant-drawer-footer">
|
||||
<Button.Group>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
icon={<AntIcon type="search" />}
|
||||
onClick={() =>
|
||||
this.onQuery(this.moreQueryForm.current.getFieldsValue())
|
||||
}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Tooltip placement="top" title="重置查询">
|
||||
<Button
|
||||
onClick={() => this.onResetQuery(this.moreQueryForm)}
|
||||
icon={<AntIcon type="undo" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</Form>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function renderTable(props, on) {
|
||||
return <Table className="yo-table" {...props} {...on} />
|
||||
}
|
||||
|
||||
export default class QueryTable extends Component {
|
||||
state = {
|
||||
// 加载状态
|
||||
loading: false,
|
||||
// 表格类型
|
||||
type: 'tree',
|
||||
// 数据
|
||||
dataSource: [],
|
||||
|
||||
moreQueryVisible: false,
|
||||
}
|
||||
|
||||
// 查询表单实例
|
||||
queryForm = React.createRef()
|
||||
// 更多查询表单实例
|
||||
moreQueryForm = React.createRef()
|
||||
|
||||
// 查询值
|
||||
query = {}
|
||||
|
||||
// 分页器配置
|
||||
pagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
size: 'small',
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: total => `总共${total}条数据`,
|
||||
position: ['bottomLeft'],
|
||||
}
|
||||
|
||||
// 默认选中页码
|
||||
pageIndex = 1
|
||||
// 默认页面尺寸
|
||||
pageSize = 10
|
||||
|
||||
// 排序字段
|
||||
sorter = {
|
||||
sortField: '',
|
||||
sortOrder: '',
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.autoLoad = typeof this.props.autoLoad === 'boolean' ? this.props.autoLoad : true
|
||||
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
|
||||
}
|
||||
if (this.props.pageSize) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载数据
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.autoLoad) {
|
||||
this.onLoadData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
* 调用外部传入的loadData函数,可在loadData中自行改变参数
|
||||
*/
|
||||
onLoadData = async () => {
|
||||
this.onLoading()
|
||||
|
||||
if (isEqual(this.query, {}) && this.props.queryInitialValues) {
|
||||
this.query = this.props.queryInitialValues
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.loadData(
|
||||
{
|
||||
pageIndex: this.pagination.current,
|
||||
pageSize: this.pagination.pageSize,
|
||||
...this.sorter,
|
||||
},
|
||||
cloneDeep(this.query)
|
||||
)
|
||||
if (res.rows || res.data || res.items) {
|
||||
this.setState({
|
||||
type: 'table',
|
||||
dataSource: res.rows || res.data || res.items,
|
||||
})
|
||||
|
||||
this.pagination.total = res.totalCount
|
||||
} else if (res) {
|
||||
this.setState({
|
||||
type: 'tree',
|
||||
dataSource: clearChildren(res),
|
||||
})
|
||||
|
||||
this.pagination = false
|
||||
}
|
||||
} finally {
|
||||
this.onLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据开始加载
|
||||
*/
|
||||
onLoading = () => {
|
||||
this.setState({
|
||||
loading: {
|
||||
indicator: <AntIcon type="loading" />,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据加载完成
|
||||
*/
|
||||
onLoaded = () => {
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行查询
|
||||
* 返回表单字段值,加载数据,并且返回到第一页
|
||||
* @param {*} values
|
||||
*/
|
||||
onQuery = values => {
|
||||
const { queryInitialValues } = this.props
|
||||
|
||||
this.query = {
|
||||
...queryInitialValues,
|
||||
...this.query,
|
||||
...values,
|
||||
}
|
||||
this.onReloadData(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置查询
|
||||
* 初始化表单字段值,加载数据,并返回到第一页
|
||||
*/
|
||||
onResetQuery = from => {
|
||||
const { queryInitialValues, onQueryChange, query, moreQuery } = this.props
|
||||
|
||||
let queryValues = {}
|
||||
if (from === this.queryForm) {
|
||||
from.current.resetFields()
|
||||
queryValues = moreQuery ? this.moreQueryForm.current.getFieldsValue() : {}
|
||||
} else if (from === this.moreQueryForm) {
|
||||
from.current.resetFields()
|
||||
queryValues = query ? this.queryForm.current.getFieldsValue() : {}
|
||||
}
|
||||
|
||||
this.query = {
|
||||
...queryInitialValues,
|
||||
...queryValues,
|
||||
}
|
||||
|
||||
const changedValues = cloneDeep(this.query)
|
||||
onQueryChange && onQueryChange(changedValues, changedValues)
|
||||
this.onReloadData(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载表格数据
|
||||
* @param {Boolean} resetPage 是否重置页码
|
||||
*/
|
||||
onReloadData = (resetPage = false) => {
|
||||
if (resetPage) {
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
current: this.pageIndex,
|
||||
}
|
||||
}
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格分页/筛选/排序
|
||||
* @param {*} pagination
|
||||
* @param {*} filters
|
||||
* @param {*} sorter
|
||||
*/
|
||||
onTableChange = (pagination, filters, sorter) => {
|
||||
this.pagination = {
|
||||
...pagination,
|
||||
showTotal: total => `总共${total}条数据`,
|
||||
}
|
||||
this.sorter = {
|
||||
sortField: sorter.field,
|
||||
sortOrder: sorter.order,
|
||||
}
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
onAddRow = (record = {}) => {
|
||||
let { dataSource } = this.state
|
||||
if (!dataSource.find(item => !item.id)) {
|
||||
dataSource = [...dataSource, record]
|
||||
this.setState({
|
||||
dataSource,
|
||||
})
|
||||
return dataSource.length - 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onOpenMoreQuery = () => {
|
||||
this.setState({ moreQueryVisible: true })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { rowNumber } = this
|
||||
|
||||
const { loading, dataSource, type } = this.state
|
||||
|
||||
const { query, moreQuery, operator, columns, expandable, expandedRowRender } = this.props
|
||||
|
||||
const attrs = {}
|
||||
Object.keys(this.props).forEach(key => {
|
||||
if (!propsMap.includes(key)) {
|
||||
attrs[key] = this.props[key]
|
||||
}
|
||||
})
|
||||
|
||||
const props = {
|
||||
loading,
|
||||
pagination: this.pagination,
|
||||
dataSource,
|
||||
columns: (() => {
|
||||
const c = []
|
||||
if (type !== 'tree' && rowNumber & !expandable & !expandedRowRender) {
|
||||
//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,
|
||||
}
|
||||
|
||||
const on = {
|
||||
onChange: (...args) => this.onTableChange.apply(this, args),
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
{query && renderQueryBar.call(this)}
|
||||
<div className="yo-action-bar">
|
||||
<div className="yo-action-bar--actions">{operator}</div>
|
||||
<div className="yo-action-bar--actions">
|
||||
<Button.Group>
|
||||
<Tooltip placement="bottom" title="刷新">
|
||||
<Button
|
||||
onClick={() => this.onReloadData()}
|
||||
type="text"
|
||||
icon={<AntIcon type="reload" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
{this.props.editable ? (
|
||||
<Form ref={this.props.form}>{renderTable.call(this, props, on)}</Form>
|
||||
) : (
|
||||
renderTable.call(this, props, on)
|
||||
)}
|
||||
{moreQuery && renderMoreQueryBody.call(this)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
337
framework/web-react/src/components/query-tree-layout/index.jsx
Normal file
337
framework/web-react/src/components/query-tree-layout/index.jsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Breadcrumb, Button, Col, Empty, Input, Layout, Row, Spin, Tooltip, Tree } from 'antd'
|
||||
import { AntIcon, Container } from 'components'
|
||||
import { SIDER_BREAK_POINT } from 'util/global'
|
||||
|
||||
function generateKey(data, level) {
|
||||
const n = level || [0]
|
||||
n.push(0)
|
||||
data.forEach((p, i) => {
|
||||
n[n.length - 1] = i
|
||||
p.key = n.join('-')
|
||||
p.scopedSlots = { title: 'title' }
|
||||
if (p[this.replaceFields.children]) {
|
||||
generateKey.call(this, p[this.replaceFields.children], Object.assign([], n))
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
function generateList(data) {
|
||||
// 这里获取不到Key
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const { key } = data[i]
|
||||
this.list.push({
|
||||
key,
|
||||
[this.replaceFields.value]: data[i][this.replaceFields.value],
|
||||
[this.replaceFields.title]: data[i][this.replaceFields.title],
|
||||
})
|
||||
if (data[i][this.replaceFields.children]) {
|
||||
generateList.call(this, data[i][this.replaceFields.children])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getParentKey(key, tree) {
|
||||
let parentKey
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i]
|
||||
if (node[this.replaceFields.children]) {
|
||||
if (node[this.replaceFields.children].some(item => item.key === key)) {
|
||||
parentKey = node.key
|
||||
} else if (getParentKey.call(this, key, node[this.replaceFields.children])) {
|
||||
parentKey = getParentKey.call(this, key, node[this.replaceFields.children])
|
||||
}
|
||||
}
|
||||
}
|
||||
return parentKey
|
||||
}
|
||||
|
||||
function renderTitle(nodeData) {
|
||||
const title = nodeData[this.replaceFields.title]
|
||||
if (this.state.searchValue && title.indexOf(this.state.searchValue) > -1) {
|
||||
return (
|
||||
<>
|
||||
{title.substr(0, title.indexOf(this.state.searchValue))}
|
||||
<span style={{ color: '#f50' }}>{this.state.searchValue}</span>
|
||||
{title.substr(
|
||||
title.indexOf(this.state.searchValue) + this.state.searchValue.length
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <>{title}</>
|
||||
}
|
||||
|
||||
function renderBreadcrumbItem() {
|
||||
const path = ['顶级']
|
||||
|
||||
const findPath = (data, level) => {
|
||||
level = level || 1
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i]
|
||||
|
||||
path[level] = item[this.replaceFields.title]
|
||||
|
||||
if (item[this.replaceFields.value] === this.state.selectedKey) {
|
||||
path.length = level + 1
|
||||
return true
|
||||
}
|
||||
|
||||
if (item[this.replaceFields.children] && item[this.replaceFields.children].length) {
|
||||
const found = findPath(item[this.replaceFields.children], level + 1)
|
||||
if (found) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.selectedKey) {
|
||||
findPath(this.state.dataSource)
|
||||
}
|
||||
|
||||
return path.map((p, i) => <Breadcrumb.Item key={i}>{p}</Breadcrumb.Item>)
|
||||
}
|
||||
|
||||
export default class QueryTreeLayout extends Component {
|
||||
state = {
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
expandedKeys: [],
|
||||
selectedKey: '',
|
||||
autoExpandParent: true,
|
||||
searchValue: '',
|
||||
|
||||
collapsed: false,
|
||||
showSider: false,
|
||||
}
|
||||
|
||||
list = []
|
||||
|
||||
defaultExpandedKeys = []
|
||||
|
||||
replaceFields = {
|
||||
value: 'id',
|
||||
title: 'title',
|
||||
children: 'children',
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.autoLoad = typeof this.props.autoLoad === 'boolean' ? this.props.autoLoad : true
|
||||
this.loadData =
|
||||
typeof this.props.loadData === 'function' ? this.props.loadData : async () => {}
|
||||
|
||||
this.defaultExpanded =
|
||||
typeof this.props.defaultExpanded === 'boolean' ? this.props.defaultExpanded : false
|
||||
|
||||
if (this.props.replaceFields) {
|
||||
this.replaceFields = this.props.replaceFields
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载数据
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.autoLoad) {
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onResizeLayout)
|
||||
this.onResizeLayout()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onResizeLayout)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
* 调用外部传入的loadData函数,可在loadData中自行改变参数
|
||||
*/
|
||||
onLoadData = async () => {
|
||||
this.onLoading()
|
||||
|
||||
const res = await this.props.loadData()
|
||||
const data = generateKey.call(this, res)
|
||||
this.list = []
|
||||
generateList.call(this, data)
|
||||
|
||||
let expandedKeys = []
|
||||
if (this.defaultExpanded) {
|
||||
expandedKeys = this.list.map(p => p.key)
|
||||
}
|
||||
this.setState({
|
||||
dataSource: data,
|
||||
expandedKeys,
|
||||
})
|
||||
this.onLoaded()
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据开始加载
|
||||
*/
|
||||
onLoading = () => {
|
||||
this.setState({
|
||||
loading: {
|
||||
indicator: <AntIcon type="loading" />,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据加载完成
|
||||
*/
|
||||
onLoaded = () => {
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
onReloadData = () => {
|
||||
this.onLoadData()
|
||||
}
|
||||
|
||||
onExpand = expandedKeys => {
|
||||
this.setState({
|
||||
expandedKeys,
|
||||
autoExpandParent: false,
|
||||
})
|
||||
}
|
||||
|
||||
onSelect = selectedKeys => {
|
||||
const selectedIds = []
|
||||
selectedKeys.forEach(p => {
|
||||
const data = this.list.find(m => m.key === p)
|
||||
selectedIds.push(data[this.replaceFields.value])
|
||||
})
|
||||
this.setState({
|
||||
selectedKey: selectedIds[0],
|
||||
})
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(selectedIds[0])
|
||||
}
|
||||
}
|
||||
|
||||
onSearch = value => {
|
||||
const expandedKeys = this.list
|
||||
.map(p => {
|
||||
if (p[this.replaceFields.title].indexOf(value) > -1) {
|
||||
return getParentKey.call(this, p.key, this.state.dataSource)
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((p, i, self) => p && self.indexOf(p) === i)
|
||||
|
||||
this.setState({
|
||||
expandedKeys,
|
||||
autoExpandParent: !!value,
|
||||
searchValue: value,
|
||||
})
|
||||
}
|
||||
|
||||
onResizeLayout = () => {
|
||||
this.setState({
|
||||
collapsed: window.innerWidth <= SIDER_BREAK_POINT,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, dataSource, expandedKeys, autoExpandParent, collapsed, showSider } =
|
||||
this.state
|
||||
|
||||
const props = {
|
||||
treeData: dataSource,
|
||||
expandedKeys,
|
||||
autoExpandParent,
|
||||
}
|
||||
|
||||
const on = {
|
||||
onExpand: expandedKeys => this.onExpand(expandedKeys),
|
||||
onSelect: selectedKeys => this.onSelect(selectedKeys),
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="yo-tree-layout">
|
||||
<Layout.Sider
|
||||
width="240px"
|
||||
className={[
|
||||
collapsed ? 'yo-tree-layout--collapsed' : '',
|
||||
showSider ? 'open' : '',
|
||||
]
|
||||
.filter(p => p)
|
||||
.join(' ')}
|
||||
onMouseLeave={() => this.setState({ showSider: false })}
|
||||
>
|
||||
<Layout.Header>
|
||||
<div className="header-actions">
|
||||
<Input.Search
|
||||
allowClear={true}
|
||||
placeholder="请输入检索关键字"
|
||||
onSearch={value => this.onSearch(value)}
|
||||
/>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
<div className="yo-tree-layout--bar">
|
||||
<Tooltip placement="bottom" title="刷新">
|
||||
<AntIcon type="undo" onClick={() => this.onReloadData()} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" title="折叠全部">
|
||||
<AntIcon
|
||||
type="switcher"
|
||||
onClick={() => this.setState({ expandedKeys: [] })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="yo-tree-layout--content">
|
||||
<Spin
|
||||
spinning={loading}
|
||||
indicator={<AntIcon type="loading" />}
|
||||
wrapperClassName="h-100-p"
|
||||
>
|
||||
{!loading && !dataSource.length ? (
|
||||
<Empty
|
||||
image={
|
||||
<div className="text-center pt-md">
|
||||
<AntIcon className="h3 mt-xl mb-md" type="smile" />
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
}
|
||||
description={false}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
{...props}
|
||||
{...on}
|
||||
titleRender={nodeData => renderTitle.call(this, nodeData)}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
<Layout.Content>
|
||||
<Container mode="fluid">
|
||||
<Row gutter={16} align="middle">
|
||||
{collapsed && (
|
||||
<Col>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AntIcon type="menu" />}
|
||||
onClick={() => this.setState({ showSider: true })}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
<Col flex="1">
|
||||
<Breadcrumb className="mt-md mb-md">
|
||||
{renderBreadcrumbItem.call(this)}
|
||||
</Breadcrumb>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{this.props.children}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user