sunmao-sdk
Version:
榫卯-开箱即用赋能-sdk
848 lines (798 loc) • 23.7 kB
JavaScript
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);