UNPKG

sunmao-sdk

Version:

榫卯-开箱即用赋能-sdk

848 lines (798 loc) 23.7 kB
import React, { useEffect, useImperativeHandle, forwardRef, useMemo, useState, useRef } from "react"; import { useSet } from "./hooks"; import { Table, Button, Radio, Dropdown, Menu, ConfigProvider, Popconfirm, message, Tooltip } from "antd"; import { SettingOutlined } from "@ant-design/icons"; import zhCN from "antd/es/locale/zh_CN"; // import "antd/dist/antd.css"; import "./index.css"; import { buildSchema, isObj, parseFunctionValue, filterParams } from "./utils"; import defaultWidgets from "./widgets"; import more from "../../assets/more.png"; import FR from "../FR"; import { getBtnConfig, splitParams } from "../../utils/commonUtils"; import * as formUtils from "../../utils/formUtils"; import { Resizable } from "react-resizable"; // 定义头部组件 const ResizableTitle = props => { const { onResize, onResizeStop, width, ...restProps } = props; if (!width) { return <th {...restProps} />; } return ( <Resizable width={width} height={0} handle={ <span className="react-resizable-handle" onClick={e => { e.stopPropagation(); }} /> } onResizeStop={onResizeStop} onResize={onResize} draggableOpts={{ enableUserSelectHack: false }} > <th {...restProps} /> </Resizable> ); }; const DEFAULT_PAGE_SIZE = 10; const useTable = (_schema, api) => { // 这边可以useMemo一下 const schema = buildSchema(_schema, api) || {}; const { searchConfig = {} } = schema; // 模块级参数,可被覆盖、不可缺失 const initParams = useRef({}); const [state, set] = useSet({ loading: false, search: searchConfig.initialValue, tab: searchConfig.tab, valid: [], data: [], selectedKeys: [], selectedRows: [], // 当允许多选,且有选中行时,为选中的所有的行的值 pagination: { current: 1, pageSize: DEFAULT_PAGE_SIZE } }); const { pagination, tab, valid } = state; const searchApi = useMemo(() => { let _searchApi; try { if (!searchConfig.api) return; if (isObj(searchConfig.api)) { _searchApi = searchConfig.api.action; initParams.current = { ...searchConfig.initData }; } else { _searchApi = searchConfig.api[tab].action; initParams.current = { ...searchConfig.initData, ...searchConfig.api[tab]?.params }; // _tabParams属于静态传承 , 优先级高 } } catch (error) { set({ loading: false }); } return _searchApi; }, [tab, searchConfig]); const doSearch = ( { current = 1, pageSize = pagination.pageSize, search = state.search } = { current: 1, pageSize: pagination.pageSize, search: state.search } ) => { const _pagination = { current, pageSize }; if (valid?.length) return; set({ loading: true, selectedKeys: [], selectedRows: [] }); // let searchApi,_tabParams; 改至 useMemo // try { // if (!searchConfig.api) return; // if (isObj(searchConfig.api)) { // searchApi = searchConfig.api.action; // } else { // searchApi = searchConfig.api[tab].action; // _tabParams = searchConfig.api[tab].params; // _tabParams属于静态传承 , 优先级高 // } // } catch (error) { // set({ loading: false }); // } if (typeof searchApi === "function") { searchApi({ pageNo: _pagination.current, // pageIndex: _pagination.current, // pageIndex不可用,后端对pageIndex的定义存在不同含义,导致分页问题 ...splitParams(filterParams({ ...initParams.current, ...search })), ..._pagination }) .then(res => { const { data, total, pageSize } = res; set({ loading: false, data, pagination: { ..._pagination, total, pageSize: pageSize || DEFAULT_PAGE_SIZE } }); }) .catch(err => { console.log(err); set({ loading: false }); }); } else { console.error("searchConfig.api中的action非函数", searchApi); set({ loading: false }); } }; const refresh = current => { doSearch({ current: current || 1, pageSize: pagination.pageSize }); }; return { initParams: initParams.current, // 初始params值 state, set, schema, doSearch, refresh }; }; const TableRender = ({ schema: _schema, api, widgets, searchWidgets }, ref) => { const { state, set, schema, doSearch, refresh, initParams } = useTable( _schema, api ); const { loading, data, selectedKeys, selectedRows, pagination, search, tab } = state; const finalParams = useMemo(() => { return splitParams(filterParams({ ...initParams, ...search })); }, [initParams, search]); const { searchConfig = {}, tableConfig = {}, actionConfig = {}, columns = [], actionList, buttonMenu, pagination: propsPagination, aclCheckPermissionResults, tableTitle, title, ...rest } = schema; const [lastTitle, setTitle] = useState(tableTitle); const { resizeColumns, customColumns, customSearch, setShowCustomColumns, setShowCustomSearch, resizeTable, rowSelection, getCheckboxProps, rowSelectionProps: rowSelectionPropsCustomer } = tableConfig; const rowSelectionProps = rowSelection ? { type: "checkbox", getCheckboxProps, selectedRowKeys: selectedKeys, onChange: (keys, records) => { set({ selectedKeys: keys, selectedRows: records }); }, ...rowSelectionPropsCustomer } : null; const _widgets = { ...defaultWidgets, ...widgets }; const renderActionList = (record, list, DropItem) => ( <div className="tr-action-list" style={{ display: "flex", justifyContent: "center" }} > {list.map((item, idx) => { let content = item.text; try { if (item.widget) { content = _widgets[item.widget]({ ...record, ...item }); } } catch (e) {} let disEval = false; try { disEval = item[item.disEval] || eval(item.disEval); } catch {} const disabled = parseFunctionValue(item && item.disabled, record) || disEval; const btnHandle = () => handleAction(item, { ...finalParams, ...record }, disabled); const { again, btnProps } = getBtnConfig( disEval, item.funcType, item.aclPermissionCode, aclCheckPermissionResults ); let btnStyle = {}; try { btnStyle = JSON.parse(item.btnStyle); } catch (e) {} const endBtn = again ? ( <Popconfirm key={idx.toString()} title={item?.content?.tips || "确定进行该操作!"} disabled={disabled} onConfirm={btnHandle} {...btnProps} > <Button type="link" size="small" style={{ marginRight: 8 }} disabled={disabled} {...btnStyle} > {item.text} </Button> </Popconfirm> ) : ( <Button key={idx.toString()} type="link" size="small" style={{ marginRight: 8 }} disabled={disabled} onClick={btnHandle} {...btnStyle} > {item.text} </Button> ); return disabled ? ( <Tooltip title={item.disEvalTips || "禁用不可点击"}> {endBtn} </Tooltip> ) : ( endBtn ); })} {DropItem} </div> ); // 超过三个就展示下拉 const renderAction = (text, record) => { if (!actionList) return false; const actList = actionList.filter(item => { let isHidden = false; try { isHidden = record[item.isHidden] || eval(item.isHidden); } catch {} return !isHidden; }); const limit = actionConfig.showCount || 3; const len = actList.length; if (len <= limit) { return renderActionList(record, actList); } else { const firstFew = actList.slice(0, limit - 1); const dropList = actList.slice(limit - 1); const menu = ( <Menu> {dropList.map((item, idx) => { let disEval = false; try { disEval = item[item.disEval] || eval(item.disEval); } catch {} const disabled = parseFunctionValue(item && item.disabled, record) || disEval; const btnHandle = () => handleAction(item, { ...finalParams, ...record }, disabled); const { again, btnProps } = getBtnConfig( disEval, item.funcType, item.aclPermissionCode, aclCheckPermissionResults ); let btnStyle = {}; try { btnStyle = JSON.parse(item.btnStyle); } catch (e) {} const endBtn = ( <Menu.Item key={idx.toString()} disabled={disabled}> {again ? ( <Popconfirm title={item?.content?.tips || "确定进行该操作!"} disabled={disabled} onConfirm={btnHandle} {...btnProps} > <Button type="link" size="small" style={{ marginRight: 8 }} disabled={disabled} {...btnStyle} > {item.text} </Button> </Popconfirm> ) : ( <Button type="link" size="small" style={{ marginRight: 8 }} disabled={disabled} onClick={btnHandle} {...btnStyle} > {item.text} </Button> )} </Menu.Item> ); return disabled ? ( <Tooltip title={item.disEvalTips || "禁用不可点击"}> {endBtn} </Tooltip> ) : ( endBtn ); })} </Menu> ); const DropItem = ( <Dropdown overlay={menu}> <a onClick={e => e.preventDefault()}> <img alt="more" style={{ width: "15px" }} src={more} /> </a> </Dropdown> ); return renderActionList(record, firstFew, DropItem); } }; // 操作区配置 let ActionList; let actionWidth = 60; // 预留宽度,避免挤压低于60, 尽量保证最窄 60px if (actionList?.length) { // 优化操作区按键width const len = actionList.length; // 实际显示按键数 const showCount = Math.min(len, actionConfig.showCount); // 按键边距 actionWidth += (showCount - 2) * 10; // 显示按键字数 let wordsCount = 0; for (let i = 0; i < showCount; i++) { wordsCount += actionList[i].text.length; } actionWidth += wordsCount * 15; ActionList = { title: "操作", width: actionWidth, align: "center", key: "sunmao_action", ...actionConfig, render: renderAction }; } // defaultColumns、cols 同步初始化 上级传入 columns const defaultColumns = useRef(columns); const [cols, setCols] = useState(columns); // 用于动态显示宽度滑动 useMemo(() => { // 上级传入 columns 变更后,重置 受控显示cols if (JSON.stringify(defaultColumns.current) != JSON.stringify(columns)) { defaultColumns.current = columns; setCols(columns); } }, [columns]); // 动态修改宽度 const handleResize = index => (_, { size }) => { const newColumns = [...cols]; newColumns[index] = { ...newColumns[index], width: size.width }; setCols(newColumns); }; const handleResizeStop = index => (_, { size }) => { // 保存最新表头宽度数据 resizeTable(cols); }; let resizableColumns = { columns: [] }; // 根据resizeColumns组装 columns if (resizeColumns) { // 设置侦听函数 const mergeColumns = cols.map((col, index) => { return { ...col, onHeaderCell: column => ({ width: Math.max(column.width, 80), onResize: handleResize(index), onResizeStop: handleResizeStop(index) }) }; }); resizableColumns.columns = ActionList ? [...mergeColumns, ActionList] : mergeColumns; resizableColumns.components = { header: { cell: ResizableTitle } }; resizableColumns.scroll = { x: formUtils.getTableWidth(mergeColumns) + actionWidth }; } else { resizableColumns.columns = ActionList ? [...cols, ActionList] : cols; } useEffect(() => { if (searchConfig.autoSearch) { // 设置,则自动刷新 doSearch(); } }, []); // eslint-disable-line const removeSearch = () => { // 恢复默认 使用 initialValue ; initData为页面级参数、可被search覆盖,不可缺失 set({ search: searchConfig.initialValue }); searchConfig.autoSearch && doSearch({ search: searchConfig.initialValue }); }; const onTabChange = e => { try { set({ tab: parseInt(e.target.value) }); // 切换即刷新 setTimeout(() => { doSearch(); }, 100); } catch (e) { console.error(e); } }; const handleTableChange = ( { current, pageSize }, // 分页 tableFilterParams, // 过滤 { order, field }, // 排序 temp2 ) => { // 初始化pagination传参 let params = { current: 1, pageSize }; // 初始化排序传参 if (order && field) { // 排序类型 排序字段 params.search = { ...finalParams, ...search, // finalParams中过滤了 ‘’等空值字段,此处回填一下 sortOrder: order, sortField: field, ...tableFilterParams // TODOdev 单选需要将数组[]转为 string }; } else { // 设置过滤传参 params.search = { ...finalParams, ...search, // finalParams中过滤了 ‘’等空值字段,此处回填一下 ...tableFilterParams }; } // 除页码变更,其余采用初始化传参 if (pagination.current !== current) { params.current = current; } // console.log("------handleTableChange", params); doSearch(params); }; const handleAction = (item, record, disabled = false) => { if (disabled) return; if (item && item.action && typeof item.action === "function") { item.action(record, refresh, item); } else if (!item.widget) { console.error("传入的点击事件不是一个函数", item); } }; const handleMenuAction = (action, params) => { if (typeof action === "function") { // 是否批量操作,批量操作无选中时拦截 if (!params?.selectedKeys?.length && params?.handleInfo?.interceptTips) { message.error(params.handleInfo.interceptTips); return; } action(params); } else { console.error("传入的点击事件不是一个函数", action); } }; const handleSearch = () => doSearch(); const getState = () => { return { ...state }; }; // 开放给外部使用的方法和属性 useImperativeHandle(ref, () => ({ setTitle, refresh, setState: set, getState })); const SearchBtn = () => ( <div className="flex justify-end w-100"> <Button className="mr2" type="primary" onClick={handleSearch}> 查询 </Button> <Button onClick={removeSearch}>重置</Button> {customSearch && ( <Tooltip title="可自定义展示查询项"> <SettingOutlined className="ml2" onClick={() => setShowCustomSearch(true)} /> </Tooltip> )} </div> ); const SearchFromSchema = searchConfig.schema ? { ...searchConfig.schema } : undefined; let showSearch = true; try { // 当查询项都隐藏或 无查询项时,隐藏搜索ui showSearch = Object.values(SearchFromSchema.propsSchema.properties).some( v => !v["ui:hidden"] ); if (showSearch) { const calcWidth = schema => { let width = 100; try { const wList = Object.values(schema.propsSchema.properties) .filter(v => v["ui:hidden"] !== true) .map(v => v["ui:width"]); const idx = wList.lastIndexOf(undefined); const effectiveList = wList .slice(idx + 1) .map(item => Number(item.substring(0, item.length - 1))); const len = effectiveList.reduce((a, b) => { const sum = a + b; if (sum > 100) return Math.min(100, b); return sum; }, 0); width = 100 - len; if (width < 10) { // 如果剩下太少了,就换行 width = 100; } return width + "%"; } catch (error) {} return width + "%"; }; SearchFromSchema.propsSchema.properties.searchBtn = { "ui:widget": "searchBtn", "ui:className": "search-btn", "ui:width": calcWidth(SearchFromSchema) }; } } catch (error) {} const renderTopBtn = (btn, idx, btnType = "default") => { const { text, label, funcType, content, renderFunc: eventRender } = btn; const btnName = text || label; // 添加隐藏功能 let isHidden = false; try { isHidden = finalParams[btn.isHidden] || eval(btn.isHidden); if (isHidden) return null; } catch {} const params = { data, selectedKeys, selectedRows, refresh, handleInfo: btn, search: { ...finalParams, requestJson: JSON.stringify(finalParams) } }; let disEval = false; try { disEval = finalParams[btn.disEval] || eval(btn.disEval); } catch {} const btnHandle = () => handleMenuAction(btn.action, params); const { again, btnProps } = getBtnConfig( disEval, btn.funcType, btn.aclPermissionCode, aclCheckPermissionResults ); try { if (React.isValidElement(btn(params))) return ( <div style={{ marginRight: 8 }} key={idx.toString()}> {btn(params)} </div> ); } catch (e) {} if (btn.widget) { try { return ( <div style={{ marginRight: 8 }} key={idx.toString()}> {_widgets[btn.widget](params)} </div> ); } catch (e) {} } let btnStyle = {}; try { btnStyle = JSON.parse(btn.btnStyle); } catch (e) {} let endBtn; let buttonProps = { style: { marginRight: 8 }, disabled: disEval, type: btnType, ...btnStyle }; if (funcType === "37") { const menu = ( <Menu> {content.handleItems.map((item, idx) => { const menuBtn = renderTopBtn(item, idx, "link"); return <Menu.Item key={idx.toString()}>{menuBtn}</Menu.Item>; })} </Menu> ); endBtn = ( <Dropdown key={idx.toString()} overlay={menu} placement="top"> {eventRender ? ( eventRender(btn, buttonProps) ) : ( <Button type="primary" style={{ marginRight: 8 }}> {btnName} </Button> )} </Dropdown> ); } else if (again) { endBtn = ( <Popconfirm key={idx.toString()} disabled={disEval} title={btn?.content?.tips || "确定进行该操作!"} onConfirm={btnHandle} {...btnProps} > {eventRender ? ( eventRender(btn, buttonProps) ) : ( <Button {...buttonProps}>{btnName}</Button> )} </Popconfirm> ); } else { const compProps = { key: idx.toString(), onClick: btnHandle, ...buttonProps }; endBtn = eventRender ? ( eventRender(btn, compProps) ) : ( <Button {...compProps}>{btnName}</Button> ); } return disEval ? ( <Tooltip title={btn.disEvalTips || "禁用不可点击"}>{endBtn}</Tooltip> ) : ( endBtn ); }; return ( <ConfigProvider locale={zhCN}> <div className=""> {showSearch && ( <div className="tr-search"> {searchConfig.searchTopRender && searchConfig.searchTopRender(finalParams)} <FR {...SearchFromSchema} formData={search} onChange={v => set({ search: v })} onValidate={v => set({ valid: v })} widgets={{ searchBtn: SearchBtn, ...searchWidgets }} /> </div> )} <div className="tr-table-wrapper"> <div className="tr-table-tabs"> <h3 className="tr-table-title">{lastTitle || title}</h3> <TableTabs tab={tab} api={searchConfig.api} onTabChange={onTabChange} /> <div style={{ display: "flex", flexDirection: "row-reverse", alignItems: "center", paddingRight: 8 }} > {customColumns && ( <Tooltip title="可自定义展示表列"> <SettingOutlined onClick={() => setShowCustomColumns(true)} /> </Tooltip> )} {buttonMenu && Array.isArray(buttonMenu) && buttonMenu.map(renderTopBtn)} </div> </div> <Table className="components-table-demo-resizable-column" loading={loading} dataSource={data} onChange={handleTableChange} pagination={{ ...pagination, // onChange: handlePageChange, 改为 handleTableChange // showSizeChanger: false, showTotal: total => `共 ${total} 条`, ...propsPagination // 用于修改比如showTotal等 }} rowSelection={rowSelectionProps} {...rest} {...resizableColumns} // 核心功能,支持表头拖动布局;优先级高 /> </div> </div> </ConfigProvider> ); }; const TableTabs = ({ api, tab, onTabChange }) => { const _tab = tab || 0; if (api && api.text) return <div className="tr-single-tab">{api.text}</div>; if (api && Array.isArray(api)) { if (api.length === 1) return <div className="tr-single-tab">{api[0].text}</div>; return ( <Radio.Group onChange={onTabChange} defaultValue={_tab.toString()} className="mb2 full-flex" > {api.map((item, i) => { return ( <Radio.Button key={item.text} value={i.toString()}> {item.text} </Radio.Button> ); })} </Radio.Group> ); } return <div className="tr-single-tab" />; // 给一个空的占位 }; export default forwardRef(TableRender);