@mass001/pro-components
Version:
A React Pro Components library based on Arco Design, including CTable and more
647 lines (636 loc) • 28.3 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var webReact = require('@arco-design/web-react');
var icon = require('@arco-design/web-react/icon');
/**
* 验证列配置的有效性
*/
function validateColumns(columns) {
if (!Array.isArray(columns) || columns.length === 0) {
console.warn('CTable: columns should be a non-empty array');
return false;
}
for (const column of columns) {
// 检查是否有title
if (!column.title) {
console.warn('CTable: column must have title', column);
return false;
}
// 对于操作列(有key但没有dataIndex)或者有render函数的列,不强制要求dataIndex
// 其他列必须有dataIndex
if (!column.dataIndex && !column.key && !column.render) {
console.warn('CTable: column must have dataIndex, key, or render function', column);
return false;
}
}
return true;
}
/**
* 转换列配置为内部使用的格式
*/
function transformColumns(columns) {
return columns.map(column => ({
column,
searchable: !column.hideInSearch && !!column.searchConfig,
sortable: !!column.sorter,
filterable: !!column.filters && column.filters.length > 0,
}));
}
/**
* 从列配置生成搜索配置
*/
function generateSearchConfig(columns) {
return columns
.filter(column => !column.hideInSearch && column.searchConfig)
.map(column => ({
name: String(column.dataIndex),
label: column.title,
type: column.searchConfig.type,
options: column.searchConfig.options,
placeholder: column.searchConfig.placeholder || `请输入${column.title}`,
}));
}
/**
* 获取列的默认渲染器
*/
function getDefaultRenderer(valueType) {
switch (valueType) {
case 'number':
return (value) => {
if (typeof value === 'number') {
return value.toLocaleString();
}
return value;
};
case 'date':
return (value) => {
if (value instanceof Date) {
return value.toLocaleDateString();
}
if (typeof value === 'string' || typeof value === 'number') {
return new Date(value).toLocaleDateString();
}
return value;
};
case 'dateTime':
return (value) => {
if (value instanceof Date) {
return value.toLocaleString();
}
if (typeof value === 'string' || typeof value === 'number') {
return new Date(value).toLocaleString();
}
return value;
};
case 'tag':
return (value) => {
if (Array.isArray(value)) {
return value.join(', ');
}
return value;
};
default:
return (value) => value;
}
}
/**
* 处理列的排序函数
*/
function createSorter(column) {
if (typeof column.sorter === 'function') {
return column.sorter;
}
if (column.sorter === true) {
return (a, b) => {
// 如果没有dataIndex,无法排序
if (!column.dataIndex)
return 0;
const aValue = a[column.dataIndex];
const bValue = b[column.dataIndex];
if (aValue === bValue)
return 0;
if (aValue == null)
return -1;
if (bValue == null)
return 1;
if (typeof aValue === 'number' && typeof bValue === 'number') {
return aValue - bValue;
}
return String(aValue).localeCompare(String(bValue));
};
}
return undefined;
}
/**
* 处理分页配置
*/
function processPaginationConfig(pagination, dataSourceLength) {
if (pagination === false) {
return false;
}
const defaultConfig = {
current: 1,
pageSize: 10,
total: dataSourceLength,
sizeCanChange: true,
sizeOptions: [10, 20, 50, 100],
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
showJumper: true,
};
if (!pagination) {
return defaultConfig;
}
// 先处理回调函数
const callbacks = {};
// 处理onChange回调 - 页码变化
if (pagination.onChange) {
callbacks.onChange = pagination.onChange;
}
// 处理onPageSizeChange回调 - 页面大小变化
if (pagination.onPageSizeChange) {
callbacks.onPageSizeChange = pagination.onPageSizeChange;
}
else if (pagination.onChange) {
// 如果没有提供onPageSizeChange但提供了onChange,
// 将页面大小变化也通过onChange处理
callbacks.onPageSizeChange = (size, current) => {
pagination.onChange?.(current, size);
};
}
const processedConfig = {
...defaultConfig,
...pagination,
// 确保关键属性使用正确的值
current: pagination.current || defaultConfig.current,
pageSize: pagination.pageSize || defaultConfig.pageSize,
total: pagination.total || dataSourceLength,
// 确保回调函数不被覆盖
...callbacks,
};
return processedConfig;
}
/**
* 验证分页参数
*/
function validatePaginationParams(current, pageSize, total) {
const errors = [];
if (current < 1) {
errors.push('当前页码不能小于1');
}
if (pageSize < 1) {
errors.push('页面大小不能小于1');
}
if (total < 0) {
errors.push('总数不能小于0');
}
const maxPage = Math.ceil(total / pageSize) || 1;
if (current > maxPage) {
errors.push(`当前页码不能大于最大页码 ${maxPage}`);
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* 计算分页信息
*/
function calculatePaginationInfo(current, pageSize, total) {
const maxPage = Math.ceil(total / pageSize) || 1;
const startIndex = (current - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize - 1, total - 1);
return {
current: Math.min(current, maxPage),
pageSize,
total,
maxPage,
startIndex,
endIndex,
hasNext: current < maxPage,
hasPrev: current > 1,
};
}
/**
* 获取当前页的数据
*/
function getCurrentPageData(dataSource, current, pageSize) {
const startIndex = (current - 1) * pageSize;
const endIndex = startIndex + pageSize;
return dataSource.slice(startIndex, endIndex);
}
const ColumnRenderer = ({ value, record, index, valueType = 'text', }) => {
const renderValue = () => {
if (value === null || value === undefined) {
return '-';
}
switch (valueType) {
case 'number':
if (typeof value === 'number') {
return value.toLocaleString();
}
return value;
case 'date':
if (value instanceof Date) {
return value.toLocaleDateString('zh-CN');
}
if (typeof value === 'string' || typeof value === 'number') {
const date = new Date(value);
return isNaN(date.getTime()) ? value : date.toLocaleDateString('zh-CN');
}
return value;
case 'dateTime':
if (value instanceof Date) {
return value.toLocaleString('zh-CN');
}
if (typeof value === 'string' || typeof value === 'number') {
const date = new Date(value);
return isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN');
}
return value;
case 'select':
return value;
case 'tag':
if (Array.isArray(value)) {
return (jsxRuntime.jsx("div", { children: value.map((item, idx) => (jsxRuntime.jsx(webReact.Tag, { style: { marginRight: 4 }, children: item }, idx))) }));
}
if (typeof value === 'string') {
return jsxRuntime.jsx(webReact.Tag, { children: value });
}
return value;
case 'text':
default:
return String(value);
}
};
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderValue() });
};
/**
* 验证搜索配置的有效性
*/
function validateSearchConfig(searchConfig) {
if (!Array.isArray(searchConfig)) {
console.warn('CTable: searchConfig should be an array');
return false;
}
for (const config of searchConfig) {
if (!config.name || !config.label || !config.type) {
console.warn('CTable: searchConfig item must have name, label and type', config);
return false;
}
if (config.type === 'select' && !config.options) {
console.warn('CTable: select type searchConfig must have options', config);
return false;
}
}
return true;
}
/**
* 处理搜索表单值
*/
function processSearchValues(values) {
const processed = {};
Object.keys(values).forEach(key => {
const value = values[key];
// 过滤空值
if (value !== undefined && value !== null && value !== '') {
// 处理日期范围
if (Array.isArray(value) && value.length === 2) {
processed[key] = value;
}
// 处理数字范围
else if (typeof value === 'object' && value.min !== undefined && value.max !== undefined) {
processed[key] = value;
}
// 处理普通值
else {
processed[key] = value;
}
}
});
return processed;
}
/**
* 重置搜索表单值
*/
function resetSearchValues(searchConfig) {
const resetValues = {};
searchConfig.forEach(config => {
switch (config.type) {
case 'input':
resetValues[config.name] = '';
break;
case 'select':
resetValues[config.name] = undefined;
break;
case 'dateRange':
resetValues[config.name] = [];
break;
case 'numberRange':
resetValues[config.name] = { min: undefined, max: undefined };
break;
default:
resetValues[config.name] = undefined;
}
});
return resetValues;
}
/**
* 生成搜索表单的初始值
*/
function getInitialSearchValues(searchConfig) {
return resetSearchValues(searchConfig);
}
/**
* 验证搜索值的格式
*/
function validateSearchValues(values, searchConfig) {
const errors = [];
searchConfig.forEach(config => {
const value = values[config.name];
if (value !== undefined && value !== null && value !== '') {
switch (config.type) {
case 'numberRange':
if (typeof value === 'object') {
const { min, max } = value;
if (min !== undefined && max !== undefined && min > max) {
errors.push(`${config.label}: 最小值不能大于最大值`);
}
}
break;
case 'dateRange':
if (Array.isArray(value) && value.length === 2) {
const [start, end] = value;
if (start && end && new Date(start) > new Date(end)) {
errors.push(`${config.label}: 开始日期不能晚于结束日期`);
}
}
break;
}
}
});
return {
valid: errors.length === 0,
errors,
};
}
const { Row, Col } = webReact.Grid;
const { RangePicker } = webReact.DatePicker;
const SearchForm = ({ searchConfig, onSearch, onReset, defaultCollapsed = true, }) => {
const [form] = webReact.Form.useForm();
const [collapsed, setCollapsed] = react.useState(defaultCollapsed);
// 验证搜索配置
const isValidConfig = react.useMemo(() => {
return validateSearchConfig(searchConfig);
}, [searchConfig]);
// 计算显示的字段数量
const visibleFields = react.useMemo(() => {
if (!collapsed)
return searchConfig;
return searchConfig.slice(0, 3); // 收起时只显示前3个字段
}, [searchConfig, collapsed]);
// 处理搜索
const handleSearch = react.useCallback(async () => {
try {
const values = await form.validate();
const processedValues = processSearchValues(values);
onSearch?.(processedValues);
}
catch (error) {
console.warn('Search form validation failed:', error);
}
}, [form, onSearch]);
// 处理重置
const handleReset = react.useCallback(() => {
const resetValues = resetSearchValues(searchConfig);
form.setFieldsValue(resetValues);
onReset?.();
}, [form, searchConfig, onReset]);
// 切换展开/收起
const toggleCollapsed = react.useCallback(() => {
setCollapsed(!collapsed);
}, [collapsed]);
// 渲染表单项
const renderFormItem = react.useCallback((config) => {
switch (config.type) {
case 'input':
return (jsxRuntime.jsx(webReact.Input, { placeholder: config.placeholder || `请输入${config.label}`, allowClear: true }));
case 'select':
return (jsxRuntime.jsx(webReact.Select, { placeholder: config.placeholder || `请选择${config.label}`, options: config.options || [], allowClear: true }));
case 'dateRange':
return (jsxRuntime.jsx(RangePicker, { placeholder: ['开始日期', '结束日期'], style: { width: '100%' } }));
case 'numberRange':
return (jsxRuntime.jsxs(webReact.Input.Group, { compact: true, children: [jsxRuntime.jsx(webReact.Form.Item, { field: `${config.name}.min`, noStyle: true, children: jsxRuntime.jsx(webReact.InputNumber, { placeholder: "\u6700\u5C0F\u503C", style: { width: '50%' } }) }), jsxRuntime.jsx(webReact.Form.Item, { field: `${config.name}.max`, noStyle: true, children: jsxRuntime.jsx(webReact.InputNumber, { placeholder: "\u6700\u5927\u503C", style: { width: '50%' } }) })] }));
default:
return (jsxRuntime.jsx(webReact.Input, { placeholder: config.placeholder || `请输入${config.label}`, allowClear: true }));
}
}, []);
if (!isValidConfig || searchConfig.length === 0) {
return null;
}
return (jsxRuntime.jsx("div", { className: "ctable-search-form", style: { marginBottom: 16 }, children: jsxRuntime.jsx(webReact.Form, { form: form, layout: "horizontal", labelCol: { span: 6 }, wrapperCol: { span: 18 }, autoComplete: "off", children: jsxRuntime.jsxs(Row, { gutter: 24, children: [visibleFields.map((config) => (jsxRuntime.jsx(Col, { span: 8, children: jsxRuntime.jsx(webReact.Form.Item, { label: config.label, field: config.name, rules: config.rules, children: renderFormItem(config) }) }, config.name))), jsxRuntime.jsx(Col, { span: 8, children: jsxRuntime.jsx(webReact.Form.Item, { label: " ", colon: false, children: jsxRuntime.jsxs(webReact.Space, { children: [jsxRuntime.jsx(webReact.Button, { type: "primary", icon: jsxRuntime.jsx(icon.IconSearch, {}), onClick: handleSearch, children: "\u641C\u7D22" }), jsxRuntime.jsx(webReact.Button, { icon: jsxRuntime.jsx(icon.IconRefresh, {}), onClick: handleReset, children: "\u91CD\u7F6E" }), searchConfig.length > 3 && (jsxRuntime.jsx(webReact.Button, { type: "text", icon: collapsed ? jsxRuntime.jsx(icon.IconDown, {}) : jsxRuntime.jsx(icon.IconUp, {}), onClick: toggleCollapsed, children: collapsed ? '展开' : '收起' }))] }) }) })] }) }) }));
};
const TableToolbar = ({ title, extra, actions = {}, customRender, }) => {
// 如果提供了自定义渲染函数,直接使用
if (customRender) {
return jsxRuntime.jsx("div", { className: "ctable-toolbar", children: customRender() });
}
const { refresh, export: exportAction, create } = actions;
const hasActions = refresh?.show || exportAction?.show || create?.show;
if (!title && !extra && !hasActions) {
return null;
}
return (jsxRuntime.jsxs("div", { className: "ctable-toolbar", style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
padding: '12px 0',
}, children: [jsxRuntime.jsx("div", { className: "ctable-toolbar-left", children: title && (jsxRuntime.jsx("h3", { style: { margin: 0, fontSize: 16, fontWeight: 500 }, children: title })) }), jsxRuntime.jsx("div", { className: "ctable-toolbar-right", children: jsxRuntime.jsxs(webReact.Space, { children: [refresh?.show && (jsxRuntime.jsx(webReact.Button, { icon: jsxRuntime.jsx(icon.IconRefresh, {}), onClick: refresh.onClick, loading: refresh.loading, children: "\u5237\u65B0" })), exportAction?.show && (jsxRuntime.jsx(webReact.Button, { icon: jsxRuntime.jsx(icon.IconDownload, {}), onClick: exportAction.onClick, loading: exportAction.loading, children: "\u5BFC\u51FA" })), (refresh?.show || exportAction?.show) && create?.show && (jsxRuntime.jsx(webReact.Divider, { type: "vertical" })), create?.show && (jsxRuntime.jsx(webReact.Button, { type: "primary", icon: jsxRuntime.jsx(icon.IconPlus, {}), onClick: create.onClick, children: create.text || '新建' })), extra] }) })] }));
};
class CTableErrorBoundary extends react.Component {
constructor(props) {
super(props);
this.handleRetry = () => {
this.setState({
hasError: false,
error: undefined,
errorInfo: undefined,
});
};
this.state = {
hasError: false,
};
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo,
});
// 调用错误回调
this.props.onError?.(error, errorInfo);
// 在开发环境下打印详细错误信息
if (process.env.NODE_ENV === 'development') {
console.error('CTable Error Boundary caught an error:', error);
console.error('Error Info:', errorInfo);
}
}
render() {
if (this.state.hasError) {
// 如果提供了自定义fallback,使用自定义的
if (this.props.fallback && this.state.error && this.state.errorInfo) {
return this.props.fallback(this.state.error, this.state.errorInfo);
}
// 默认错误UI
return (jsxRuntime.jsxs("div", { className: "ctable-error-boundary", style: { padding: '40px 20px' }, children: [jsxRuntime.jsx(webReact.Result, { status: "error", icon: jsxRuntime.jsx(icon.IconExclamationCircle, {}), title: "\u8868\u683C\u6E32\u67D3\u51FA\u9519", subTitle: process.env.NODE_ENV === 'development' && this.state.error
? `错误信息: ${this.state.error.message}`
: '抱歉,表格组件遇到了一个错误,请稍后重试。', extra: [
jsxRuntime.jsx(webReact.Button, { type: "primary", onClick: this.handleRetry, children: "\u91CD\u65B0\u52A0\u8F7D" }, "retry"),
] }), process.env.NODE_ENV === 'development' && this.state.error && (jsxRuntime.jsxs("details", { style: { marginTop: 20, fontSize: 12, color: '#666' }, children: [jsxRuntime.jsx("summary", { children: "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F\uFF08\u4EC5\u5F00\u53D1\u73AF\u5883\u663E\u793A\uFF09" }), jsxRuntime.jsx("pre", { style: {
marginTop: 10,
padding: 10,
backgroundColor: '#f5f5f5',
borderRadius: 4,
overflow: 'auto',
maxHeight: 200,
}, children: this.state.error.stack }), this.state.errorInfo && (jsxRuntime.jsx("pre", { style: {
marginTop: 10,
padding: 10,
backgroundColor: '#f5f5f5',
borderRadius: 4,
overflow: 'auto',
maxHeight: 200,
}, children: this.state.errorInfo.componentStack }))] }))] }));
}
return this.props.children;
}
}
const CTable = (props) => {
const { columns, dataSource, loading = false, rowKey = 'id', search, pagination, toolbar, toolBarRender, tableAlertRender, size = 'default', bordered = false, stripe = false, scroll, ...restProps } = props;
// 验证列配置
const isValidColumns = react.useMemo(() => {
return validateColumns(columns);
}, [columns]);
// 处理rowKey
const processedRowKey = react.useMemo(() => {
if (typeof rowKey === 'string') {
return (record) => record[rowKey] || record.id || Math.random().toString(36);
}
return rowKey;
}, [rowKey]);
// 处理列配置,集成ColumnRenderer和增强功能
const processedColumns = react.useMemo(() => {
if (!isValidColumns)
return [];
return columns.map(column => {
const baseColumn = {
title: column.title,
key: column.key || String(column.dataIndex || Math.random().toString(36)),
width: column.width,
fixed: column.fixed,
align: column.align,
filters: column.filters,
onFilter: column.onFilter,
};
// 只有当dataIndex存在时才设置
if (column.dataIndex) {
baseColumn.dataIndex = column.dataIndex;
}
// 处理排序
const sorter = createSorter(column);
if (sorter) {
baseColumn.sorter = sorter;
}
// 处理渲染函数
if (column.render) {
// 如果用户提供了自定义render函数,使用用户的
baseColumn.render = column.render;
}
else if (column.valueType && column.valueType !== 'text') {
// 如果指定了valueType且不是默认的text,使用ColumnRenderer
baseColumn.render = (value, record, index) => (jsxRuntime.jsx(ColumnRenderer, { value: value, record: record, index: index, valueType: column.valueType }));
}
return baseColumn;
});
}, [columns, isValidColumns]);
// 处理搜索配置
const searchConfig = react.useMemo(() => {
if (!search)
return [];
// 如果用户提供了自定义搜索配置,使用用户的
if (search.searchConfig) {
return search.searchConfig;
}
// 否则从列配置生成搜索配置
return generateSearchConfig(columns);
}, [search, columns]);
// 处理搜索回调
const handleSearch = react.useCallback((values) => {
search?.onSearch?.(values);
}, [search]);
// 处理重置回调
const handleReset = react.useCallback(() => {
search?.onReset?.();
}, [search]);
// 处理分页配置
const processedPagination = react.useMemo(() => {
const result = processPaginationConfig(pagination, dataSource.length);
// 调试信息
if (process.env.NODE_ENV === 'development') {
console.log('CTable分页配置:', {
原始配置: pagination,
处理后配置: result,
});
}
return result;
}, [pagination, dataSource.length]);
// 数据验证错误处理
if (!isValidColumns) {
return (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsxs("div", { style: {
padding: '40px 20px',
textAlign: 'center',
color: '#999',
border: '1px dashed #d9d9d9',
borderRadius: '6px',
backgroundColor: '#fafafa',
}, children: [jsxRuntime.jsx("div", { style: { fontSize: '16px', marginBottom: '8px' }, children: "\u26A0\uFE0F \u8868\u683C\u914D\u7F6E\u9519\u8BEF" }), jsxRuntime.jsx("div", { children: "\u8BF7\u68C0\u67E5columns\u914D\u7F6E\u662F\u5426\u6B63\u786E" }), process.env.NODE_ENV === 'development' && (jsxRuntime.jsx("div", { style: { marginTop: '12px', fontSize: '12px', color: '#666' }, children: "\u63D0\u793A\uFF1A\u6BCF\u4E2Acolumn\u5FC5\u987B\u5305\u542Btitle\u5C5E\u6027\uFF0C\u4EE5\u53CAdataIndex\u3001key\u6216render\u51FD\u6570\u4E2D\u7684\u81F3\u5C11\u4E00\u4E2A" }))] }) }));
}
// 数据源验证
if (!Array.isArray(dataSource)) {
return (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsxs("div", { style: {
padding: '40px 20px',
textAlign: 'center',
color: '#999',
border: '1px dashed #d9d9d9',
borderRadius: '6px',
backgroundColor: '#fafafa',
}, children: [jsxRuntime.jsx("div", { style: { fontSize: '16px', marginBottom: '8px' }, children: "\u26A0\uFE0F \u6570\u636E\u6E90\u9519\u8BEF" }), jsxRuntime.jsx("div", { children: "dataSource\u5FC5\u987B\u662F\u4E00\u4E2A\u6570\u7EC4" })] }) }));
}
return (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsxs("div", { className: "ctable-container", children: [search && searchConfig.length > 0 && (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsx(SearchForm, { searchConfig: searchConfig, onSearch: handleSearch, onReset: handleReset, defaultCollapsed: search.defaultCollapsed }) })), toolbar && (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsx(TableToolbar, { title: toolbar.title, extra: toolbar.extra, actions: toolbar.actions }) })), toolBarRender && (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsx("div", { className: "ctable-toolbar", style: { marginBottom: 16 }, children: toolBarRender() }) })), tableAlertRender && (jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsx("div", { className: "ctable-alert", style: { marginBottom: 16 }, children: tableAlertRender() }) })), jsxRuntime.jsx(CTableErrorBoundary, { children: jsxRuntime.jsx(webReact.Table, { columns: processedColumns, data: dataSource, loading: loading, rowKey: processedRowKey, pagination: processedPagination, size: size, border: bordered, stripe: stripe, scroll: scroll, ...restProps }) })] }) }));
};
exports.CTable = CTable;
exports.CTableErrorBoundary = CTableErrorBoundary;
exports.ColumnRenderer = ColumnRenderer;
exports.SearchForm = SearchForm;
exports.TableToolbar = TableToolbar;
exports.calculatePaginationInfo = calculatePaginationInfo;
exports.createSorter = createSorter;
exports.generateSearchConfig = generateSearchConfig;
exports.getCurrentPageData = getCurrentPageData;
exports.getDefaultRenderer = getDefaultRenderer;
exports.getInitialSearchValues = getInitialSearchValues;
exports.processPaginationConfig = processPaginationConfig;
exports.processSearchValues = processSearchValues;
exports.resetSearchValues = resetSearchValues;
exports.transformColumns = transformColumns;
exports.validateColumns = validateColumns;
exports.validatePaginationParams = validatePaginationParams;
exports.validateSearchConfig = validateSearchConfig;
exports.validateSearchValues = validateSearchValues;
//# sourceMappingURL=index.cjs.js.map