diff --git a/web-react/src/common/api/index.js b/web-react/src/common/api/index.js index deb59ed..0f80ffc 100644 --- a/web-react/src/common/api/index.js +++ b/web-react/src/common/api/index.js @@ -135,25 +135,19 @@ for (let key in urls) { } }) .catch(({ response }) => { - if (process.env.VUE_APP_NODE_ENV === 'development') { - const { data, status } = response - if (data.constructor === String) { - errorNotification({ - message: data, - code: status - }) - } else { - errorNotification(data) - } - if (data.code === STATUS.Unauthorized) { - handlerUnauthorized() - } - reject(data) - } else { + const { data, status } = response + if (data.constructor === String) { errorNotification({ - message: '系统发生错误,请联系管理员' + message: data, + code: status }) + } else { + errorNotification(data) } + if (data.code === STATUS.Unauthorized) { + handlerUnauthorized() + } + reject(data) }) }) } diff --git a/web-react/src/components/authorized/handler.js b/web-react/src/components/authorized/handler.js index 28267b7..9e30dbb 100644 --- a/web-react/src/components/authorized/handler.js +++ b/web-react/src/components/authorized/handler.js @@ -19,6 +19,8 @@ const authByArray = (auth, permissions) => { case Boolean: flags.push([p, '&&']) break + default: + break } }) @@ -77,6 +79,8 @@ const authByJson = (auth, permissions) => { case Array: flags.push(authByArray(deepName(app, key), permissions)) break + default: + break } } @@ -122,6 +126,8 @@ const auth = (auth) => { case Object: flag = authByJson(auth, permissions) break + default: + break } } diff --git a/web-react/src/components/modal-form/index.jsx b/web-react/src/components/modal-form/index.jsx index 040d6e2..defda6c 100644 --- a/web-react/src/components/modal-form/index.jsx +++ b/web-react/src/components/modal-form/index.jsx @@ -51,7 +51,7 @@ export default class ModalForm extends Component { * 打开弹窗 * @param {*} data */ - open(data) { + open(data = {}) { this.data = data this.setState({ visible: true }) } @@ -86,7 +86,10 @@ export default class ModalForm extends Component { */ async onClose() { const body = this.childNode.current - if (!body) return + if (!body) { + this.close() + return + } if (this.compareOnClose) { const formData = body.form.current.getFieldsValue() diff --git a/web-react/src/components/query-tree-layout/index.jsx b/web-react/src/components/query-tree-layout/index.jsx index acba62a..f7d4bf5 100644 --- a/web-react/src/components/query-tree-layout/index.jsx +++ b/web-react/src/components/query-tree-layout/index.jsx @@ -1,11 +1,271 @@ import React, { Component } from 'react' +import { Breadcrumb, Empty, Input, Layout, Spin, Tooltip, Tree } from 'antd' +import { AntIcon, Container } from 'components' export default class QueryTreeLayout extends Component { - render() { - return ( -
-
+ state = { + loading: false, + dataSource: [], + expandedKeys: [], + selectedKey: '', + autoExpandParent: true, + searchValue: '' + } + + 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() + } + } + + /** + * 加载数据 + * 调用外部传入的loadData函数,可在loadData中自行改变参数 + */ + async onLoadData() { + this.onLoading() + + const res = await this.props.loadData() + const data = this.generateKey(res) + this.list = [] + this.generateList(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: + } + }) + } + + /** + * 数据加载完成 + */ + onLoaded() { + this.setState({ loading: false }) + } + + 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 this.getParentKey(p.key, this.state.dataSource) + } + return null + }) + .filter((p, i, self) => p && self.indexOf(p) === i) + + this.setState({ + expandedKeys, + autoExpandParent: !!value, + searchValue: value + }) + } + + 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]) { + this.generateKey(p[this.replaceFields.children], Object.assign([], n)) + } + }) + return data + } + + 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]) { + this.generateList(data[i][this.replaceFields.children]) + } + } + } + + 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 (this.getParentKey(key, node[this.replaceFields.children])) { + parentKey = this.getParentKey(key, node[this.replaceFields.children]) + } + } + } + return parentKey; + } + + titleRender(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))} + {this.state.searchValue} + {title.substr(title.indexOf(this.state.searchValue) + this.state.searchValue.length)} + + } + return <>{title} + } + + 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) => {p}) + } + + render() { + + const props = { + treeData: this.state.dataSource, + expandedKeys: this.state.expandedKeys, + autoExpandParent: this.state.autoExpandParent + } + + const on = { + onExpand: (expandedKeys) => this.onExpand(expandedKeys), + onSelect: (selectedKeys) => this.onSelect(selectedKeys) + } + + return ( + + + +
+ this.onSearch(value)} + /> +
+
+
+ + this.setState({ expandedKeys: [] })} /> + +
+
+ }> + { + !this.state.loading && !this.state.dataSource.length ? + + : + this.titleRender(nodeData)} + /> + } + +
+
+ + + + {this.renderBreadcrumbItem()} + + + {this.props.children} + +
) } } diff --git a/web-react/src/pages/system/app/form.jsx b/web-react/src/pages/system/app/form.jsx index f857c23..1c5f440 100644 --- a/web-react/src/pages/system/app/form.jsx +++ b/web-react/src/pages/system/app/form.jsx @@ -1,9 +1,11 @@ import React, { Component } from 'react' -import { Form, Input, Spin } from 'antd' +import { Form, Input, InputNumber, Spin } from 'antd' import { AntIcon } from 'components' import { cloneDeep } from 'lodash' -const initialValues = {} +const initialValues = { + sort: 100 +} export default class form extends Component { @@ -29,12 +31,13 @@ export default class form extends Component { * 填充数据 * 可以在设置this.record之后对其作出数据结构调整 * [异步,必要] - * @param {*} record + * @param {*} params */ - async fillData(record) { + async fillData(params) { - this.record = cloneDeep(record) - /** */ + this.record = cloneDeep(params.record) + //#region 从后端转换成前段所需格式 + //#endregion this.form.current.setFieldsValue(this.record) this.setState({ @@ -57,7 +60,8 @@ export default class form extends Component { if (this.record) { postData.id = this.record.id } - /** */ + //#region 从前段转换后端所需格式 + //#endregion return postData } } @@ -79,6 +83,14 @@ export default class form extends Component { + + + diff --git a/web-react/src/pages/system/app/index.jsx b/web-react/src/pages/system/app/index.jsx index e4e21a5..515fece 100644 --- a/web-react/src/pages/system/app/index.jsx +++ b/web-react/src/pages/system/app/index.jsx @@ -144,7 +144,7 @@ export default class index extends Component { * @param {*} query * @returns */ - async loadData(params, query) { + loadData = async (params, query) => { const { data } = await apiAction.page({ ...params, ...query, @@ -176,7 +176,9 @@ export default class index extends Component { * @param {*} record */ onOpen(modal, record) { - modal.current.open(record) + modal.current.open({ + record + }) } /** @@ -240,7 +242,7 @@ export default class index extends Component { + >新增{name} } /> diff --git a/web-react/src/pages/system/org/form.jsx b/web-react/src/pages/system/org/form.jsx new file mode 100644 index 0000000..d3fe0ef --- /dev/null +++ b/web-react/src/pages/system/org/form.jsx @@ -0,0 +1,226 @@ +import React, { Component } from 'react' +import { Cascader, Form, Input, InputNumber, Select, Spin, TreeSelect } from 'antd' +import { AntIcon } from 'components' +import { cloneDeep } from 'lodash' +import getDicData from 'util/dic' +import { EMPTY_ID } from 'util/global' +import { api } from 'common/api' + +const initialValues = { + sort: 100 +} + +export default class form extends Component { + + state = { + // 加载状态 + loading: true, + + codes: { + orgType: [] + }, + + options: { + orgData: [], + areaData: [] + } + } + + // 表单实例 + form = React.createRef() + + // 初始化数据 + record = {} + + /** + * mount后回调 + */ + componentDidMount() { + this.props.created && this.props.created(this) + } + + /** + * 填充数据 + * 可以在设置this.record之后对其作出数据结构调整 + * [异步,必要] + * @param {*} params + */ + async fillData(params) { + + this.record = cloneDeep(params.record) + //#region 从后端转换成前段所需格式 + const orgData = await this.loadOrgData() + const areaData = await this.loadAreaData() + + const codes = await getDicData('org_type') + this.setState({ + codes, + options: { + orgData, + areaData + } + }) + + const areaCode = []; + const findCode = (data, level) => { + level = level || 0; + for (let i = 0; i < data.length; i++) { + const item = data[i]; + areaCode[level] = item.code; + + if (item.code === params.record.areaCode) { + areaCode.length = level + 1; + return true; + } + + if (item.children && item.children.length) { + const found = findCode(item.children, level + 1); + if (found) { + return true; + } + } + } + }; + + if (params.record && params.record.areaCode) { + findCode(areaData); + } + + this.record = { + pid: params.orgId, + ...this.record, + areaCode + } + this.record.areaCode = areaCode + //#endregion + + this.form.current.setFieldsValue(this.record) + + this.setState({ + loading: false + }) + } + + /** + * 获取数据 + * 可以对postData进行数据结构调整 + * [异步,必要] + * @returns + */ + async getData() { + const form = this.form.current + + const valid = await form.validateFields() + if (valid) { + const postData = form.getFieldsValue() + if (this.record) { + postData.id = this.record.id + } + //#region 从前段转换后端所需格式 + postData.areaCode = postData.areaCode[postData.areaCode.length - 1] + //#endregion + return postData + } + } + + //#region 自定义方法 + async loadOrgData() { + const { data } = await api.getOrgTree() + return [{ + id: EMPTY_ID, + parentId: undefined, + title: '顶级', + value: EMPTY_ID, + pid: undefined, + children: data, + }] + } + + async loadAreaData() { + const { data } = await api.getAreaTree() + const clearChiildren = (data) => { + data.forEach((item) => { + if (item.children && item.children.length) { + clearChiildren(item.children); + } else { + delete item.children; + } + }); + }; + clearChiildren(data); + return data + } + + onAreaCodeChange(selectedOptions) { + const data = selectedOptions[selectedOptions.length - 1] + this.form.current.setFieldsValue({ + name: data.name, + code: data.code + }) + } + //#endregion + + render() { + return ( +
+ }> +
+ + this.onAreaCodeChange(selectedOptions)} + /> + + + + + + + + + + + + + + + + + + + +
+
+
+ ) + } +} diff --git a/web-react/src/pages/system/org/index.jsx b/web-react/src/pages/system/org/index.jsx new file mode 100644 index 0000000..34c8a4d --- /dev/null +++ b/web-react/src/pages/system/org/index.jsx @@ -0,0 +1,279 @@ +import React, { Component } from 'react' +import { Button, Card, Form, Input, message as Message, Popconfirm } from 'antd' +import { AntIcon, Auth, Container, ModalForm, QueryTable, QueryTableActions, QueryTreeLayout } from 'components' +import { api } from 'common/api' +import auth from 'components/authorized/handler' +import { toCamelCase } from 'util/format' +import { isEqual } from 'lodash' +import getDicData from 'util/dic' +import FormBody from './form' + +const apiAction = { + tree: api.getOrgTree, + page: api.getOrgPage, + add: api.sysOrgAdd, + edit: api.sysOrgEdit, + delete: api.sysOrgDelete +} + +const name = '机构' + +export default class index extends Component { + + // 表格实例 + table = React.createRef() + + // 新增窗口实例 + addForm = React.createRef() + // 编辑窗口实例 + editForm = React.createRef() + + // 树选中节点 + selectId = undefined + + // 表格字段 + columns = [ + { + title: '机构名称', + width: '400px', + dataIndex: 'name', + sorter: true, + }, + { + title: '唯一编码', + width: '200px', + dataIndex: 'code', + sorter: true, + }, + { + title: '机构类型', + dataIndex: 'type', + sorter: true, + render: text => (<>{this.bindCodeValue(text, 'org_type')}) + }, + { + title: '排序', + width: '80px', + dataIndex: 'sort', + sorter: true, + }, + { + title: '备注', + dataIndex: 'remark', + sorter: true, + }, + ] + + /** + * 构造函数,在渲染前动态添加操作字段等 + * @param {*} props + */ + constructor(props) { + super(props) + + const flag = auth({ sysOrg: [['edit'], ['delete']] }) + + if (flag) { + this.columns.push({ + title: '操作', + width: 150, + dataIndex: 'actions', + render: (text, record) => ( + + this.onOpen(this.editForm, record)}>编辑 + + + this.onDelete(record)} + > + 删除 + + + ) + }) + } + } + + /** + * 阻止外部组件引发的渲染,提升性能 + * 可自行添加渲染条件 + * [必要] + * @param {*} props + * @param {*} state + * @returns + */ + shouldComponentUpdate(props, state) { + return !isEqual(this.state, state) + } + + /** + * 加载字典数据,之后开始加载表格数据 + * 如果必须要加载字典数据,可直接对表格设置autoLoad=true + */ + componentDidMount() { + this.table.current.onLoading() + getDicData('org_type').then(res => { + this.setState({ + codes: res + }, () => { + this.table.current.onLoadData() + }) + }) + } + + /** + * 调用加载数据接口,可在调用前对query进行处理 + * [异步,必要] + * @param {*} params + * @param {*} query + * @returns + */ + loadData = async (params, query) => { + + query = { + ...query, + pid: this.selectId + } + + const { data } = await apiAction.page({ + ...params, + ...query, + }) + return data + } + + /** + * 调用树结构数据接口 + * [异步,必要] + * @returns + */ + loadTreeData = async () => { + const { data } = await apiAction.tree() + return data + } + + /** + * 树节点选中事件 + * [必要] + * @param {*} id + */ + onSelectTree(id) { + this.selectId = id + this.table.current.onReloadData() + } + + /** + * 绑定字典数据 + * @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({ + orgId: this.selectId, + record + }) + } + + /** + * 对表格上的操作进行统一处理 + * [异步] + * @param {*} action + * @param {*} successMessage + */ + async onAction(action, successMessage) { + this.table.current.onLoading() + try { + await action + Message.success(successMessage) + this.table.current.onReloadData() + } catch { + this.table.current.onLoaded() + } + } + + /** + * 删除 + * @param {*} record + */ + onDelete(record) { + this.onAction( + apiAction.delete(record), + '删除成功' + ) + } + + //#region 自定义方法 + //#endregion + + render() { + return ( + this.onSelectTree(key)} + > + + + + + + + + } + operator={ + + } + /> + + + + this.table.current.onReloadData()} + > + + + + this.table.current.onReloadData()} + > + + + + ) + } +} diff --git a/web-react/src/store/reducer.js b/web-react/src/store/reducer.js index de2c558..4bb1151 100644 --- a/web-react/src/store/reducer.js +++ b/web-react/src/store/reducer.js @@ -11,6 +11,25 @@ const user = (state = {}, action) => { } } +const layout = (state = {}, action) => { + switch (action.type) { + // 打开窗口 + case 'OPEN_WINDOW': + return state + // 关闭窗口 + case 'CLOSE_WINDOW': + return state + // 重新加载窗口 + case 'RELOAD_WINDOW': + return state + // 侧边收起状态 + case 'TOGGLE_COLLAPSED': + return state + default: + return state + } +} + const dicData = (state = {}, action) => { switch (action.type) { case 'ADD_DIC_DATA': @@ -23,6 +42,7 @@ const dicData = (state = {}, action) => { const combine = combineReducers({ user, + layout, dicData }) diff --git a/web-react/src/util/format/index.js b/web-react/src/util/format/index.js index 86fd9b1..238c7fe 100644 --- a/web-react/src/util/format/index.js +++ b/web-react/src/util/format/index.js @@ -40,5 +40,11 @@ export const toCamelCase = (str) => { * @param {String} str */ export const toUnderScoreCase = (str) => { - + if (typeof str === 'string') { + str = str.replace(/[A-Z]/g, (match) => { + return `_${match}` + }).toLowerCase() + return str.startsWith('_') ? str.slice(1) : str + } + return str } \ No newline at end of file