crud-page-react 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +123 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +123 -96
- package/dist/index.js.map +1 -1
- package/dist/types/schema.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -819,6 +819,23 @@ function buildUrl(template, record) {
|
|
|
819
819
|
return value !== undefined ? String(value) : match;
|
|
820
820
|
});
|
|
821
821
|
}
|
|
822
|
+
/** 处理模板数据,支持 {{fieldName}} 格式的变量替换 */
|
|
823
|
+
function processTemplateData(data, record) {
|
|
824
|
+
const result = {};
|
|
825
|
+
for (const [key, value] of Object.entries(data)) {
|
|
826
|
+
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
|
827
|
+
// 替换模板变量 {{fieldName}}
|
|
828
|
+
result[key] = value.replace(/\{\{(\w+)\}\}/g, (match, fieldName) => {
|
|
829
|
+
const fieldValue = record[fieldName];
|
|
830
|
+
return fieldValue !== undefined ? String(fieldValue) : match;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
result[key] = value;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
822
839
|
/** 通用请求封装 */
|
|
823
840
|
async function apiRequest(url, options) {
|
|
824
841
|
const res = await fetch(url, Object.assign({ headers: { 'Content-Type': 'application/json' } }, options));
|
|
@@ -848,8 +865,8 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
848
865
|
const rowKey = schema.rowKey || 'id';
|
|
849
866
|
// 使用传入的apiRequest或默认的
|
|
850
867
|
const request = customApiRequest || apiRequest;
|
|
851
|
-
//
|
|
852
|
-
const
|
|
868
|
+
// 初始数据引用
|
|
869
|
+
const initialDataRef = react.useRef(initialData);
|
|
853
870
|
const [data, setData] = react.useState([]);
|
|
854
871
|
const [loading, setLoading] = react.useState(false);
|
|
855
872
|
const [total, setTotal] = react.useState(0);
|
|
@@ -858,33 +875,20 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
858
875
|
const [pageSize, setPageSize] = react.useState(((_a = schema.pagination) === null || _a === void 0 ? void 0 : _a.pageSize) || 10);
|
|
859
876
|
const [modalState, setModalState] = react.useState({ open: false, mode: 'create' });
|
|
860
877
|
const [messageApi, contextHolder] = antd.message.useMessage();
|
|
861
|
-
// ----------
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (key.endsWith('_min'))
|
|
868
|
-
return Number(row[key.slice(0, -4)]) >= Number(val);
|
|
869
|
-
if (key.endsWith('_max'))
|
|
870
|
-
return Number(row[key.slice(0, -4)]) <= Number(val);
|
|
871
|
-
if (key.endsWith('_start') || key.endsWith('_end'))
|
|
872
|
-
return true;
|
|
873
|
-
const rowVal = row[key];
|
|
874
|
-
if (Array.isArray(rowVal)) {
|
|
875
|
-
return Array.isArray(val) ? val.some(v => rowVal.includes(v)) : rowVal.includes(val);
|
|
876
|
-
}
|
|
877
|
-
if (typeof val === 'string')
|
|
878
|
-
return String(rowVal).toLowerCase().includes(val.toLowerCase());
|
|
879
|
-
return rowVal === val;
|
|
880
|
-
});
|
|
881
|
-
});
|
|
882
|
-
const start = (p - 1) * ps;
|
|
883
|
-
setData(filtered.slice(start, start + ps));
|
|
884
|
-
setTotal(filtered.length);
|
|
878
|
+
// ---------- 初始化数据 ----------
|
|
879
|
+
const initializeData = react.useCallback(() => {
|
|
880
|
+
if (initialDataRef.current.length > 0) {
|
|
881
|
+
setData(initialDataRef.current);
|
|
882
|
+
setTotal(initialDataRef.current.length);
|
|
883
|
+
}
|
|
885
884
|
}, []);
|
|
886
885
|
// ---------- 获取列表 ----------
|
|
887
886
|
const fetchList = react.useCallback(async (params = filterParams, p = page, ps = pageSize) => {
|
|
887
|
+
if (!schema.api.list) {
|
|
888
|
+
// 没有配置 API,使用初始数据
|
|
889
|
+
initializeData();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
888
892
|
setLoading(true);
|
|
889
893
|
try {
|
|
890
894
|
const query = new URLSearchParams({ page: String(p), pageSize: String(ps) });
|
|
@@ -897,14 +901,22 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
897
901
|
setData(list);
|
|
898
902
|
setTotal(tot);
|
|
899
903
|
}
|
|
900
|
-
catch (
|
|
901
|
-
|
|
902
|
-
|
|
904
|
+
catch (error) {
|
|
905
|
+
console.error('Failed to fetch list:', error);
|
|
906
|
+
messageApi.error('获取数据失败,请检查网络连接或联系管理员');
|
|
907
|
+
// 如果有初始数据,显示初始数据
|
|
908
|
+
if (initialDataRef.current.length > 0) {
|
|
909
|
+
initializeData();
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
setData([]);
|
|
913
|
+
setTotal(0);
|
|
914
|
+
}
|
|
903
915
|
}
|
|
904
916
|
finally {
|
|
905
917
|
setLoading(false);
|
|
906
918
|
}
|
|
907
|
-
}, [request, schema.api.list, filterParams, page, pageSize,
|
|
919
|
+
}, [request, schema.api.list, filterParams, page, pageSize, initializeData, messageApi]);
|
|
908
920
|
// 初始加载 & 参数变化时重新请求
|
|
909
921
|
react.useEffect(() => {
|
|
910
922
|
fetchList(filterParams, page, pageSize);
|
|
@@ -922,22 +934,20 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
922
934
|
}, []);
|
|
923
935
|
// ---------- 删除 ----------
|
|
924
936
|
const handleDelete = react.useCallback(async (record) => {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
937
|
+
if (!schema.api.delete) {
|
|
938
|
+
messageApi.error('删除功能未配置');
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
await request(buildUrl(schema.api.delete, record), { method: 'DELETE' });
|
|
943
|
+
messageApi.success('删除成功');
|
|
944
|
+
fetchList();
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
console.error('Delete failed:', error);
|
|
948
|
+
messageApi.error('删除失败,请稍后重试');
|
|
936
949
|
}
|
|
937
|
-
|
|
938
|
-
localFilter(filterParams, page, pageSize);
|
|
939
|
-
messageApi.success('删除成功(演示模式)');
|
|
940
|
-
}, [request, schema.api.delete, rowKey, fetchList, localFilter, filterParams, page, pageSize, messageApi]);
|
|
950
|
+
}, [request, schema.api.delete, fetchList, messageApi]);
|
|
941
951
|
// ---------- 操作列点击 ----------
|
|
942
952
|
const handleAction = react.useCallback(async (action, record) => {
|
|
943
953
|
if (action.type === 'view') {
|
|
@@ -949,31 +959,55 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
949
959
|
else if (action.type === 'delete') {
|
|
950
960
|
handleDelete(record);
|
|
951
961
|
}
|
|
952
|
-
else if (action.type === 'custom'
|
|
962
|
+
else if (action.type === 'custom') {
|
|
953
963
|
// 处理自定义 action 的 API 调用
|
|
964
|
+
let apiConfig;
|
|
965
|
+
// 优先使用 apiKey 引用统一配置,向后兼容 api 直接配置
|
|
966
|
+
if (action.apiKey) {
|
|
967
|
+
const apiDef = schema.api[action.apiKey];
|
|
968
|
+
if (typeof apiDef === 'string') {
|
|
969
|
+
// 简单字符串 URL 配置
|
|
970
|
+
apiConfig = {
|
|
971
|
+
url: apiDef,
|
|
972
|
+
method: 'GET',
|
|
973
|
+
responseType: 'json'
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
else if (apiDef && typeof apiDef === 'object') {
|
|
977
|
+
// 完整的 API 配置对象
|
|
978
|
+
apiConfig = apiDef;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
else if (action.api) {
|
|
982
|
+
// 向后兼容:使用 action.api 配置
|
|
983
|
+
apiConfig = action.api;
|
|
984
|
+
}
|
|
985
|
+
if (!apiConfig) {
|
|
986
|
+
messageApi.error(`${action.label}未配置 API`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
954
989
|
try {
|
|
955
990
|
// 构建 URL,动态替换占位符
|
|
956
|
-
let url =
|
|
957
|
-
// 替换所有 :fieldName 格式的占位符
|
|
991
|
+
let url = apiConfig.url;
|
|
958
992
|
url = url.replace(/:(\w+)/g, (match, fieldName) => {
|
|
959
993
|
const value = record[fieldName];
|
|
960
994
|
return value !== undefined ? String(value) : match;
|
|
961
995
|
});
|
|
962
996
|
// 构建请求选项
|
|
963
997
|
const options = {
|
|
964
|
-
method:
|
|
965
|
-
headers: Object.assign({ 'Content-Type': 'application/json' },
|
|
998
|
+
method: apiConfig.method || 'GET',
|
|
999
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers)
|
|
966
1000
|
};
|
|
967
|
-
//
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1001
|
+
// 处理请求体数据
|
|
1002
|
+
if (apiConfig.data && ['POST', 'PUT', 'PATCH'].includes(apiConfig.method || 'GET')) {
|
|
1003
|
+
// 支持模板变量替换
|
|
1004
|
+
const processedData = processTemplateData(apiConfig.data, record);
|
|
1005
|
+
options.body = JSON.stringify(Object.assign(Object.assign({}, processedData), { recordId: record[rowKey], timestamp: new Date().toISOString() }));
|
|
972
1006
|
}
|
|
973
1007
|
// 调用 API
|
|
974
1008
|
const response = await request(url, options);
|
|
975
1009
|
// 处理特殊响应类型
|
|
976
|
-
if (
|
|
1010
|
+
if (apiConfig.responseType === 'blob') {
|
|
977
1011
|
// 处理文件下载
|
|
978
1012
|
const blob = new Blob([response]);
|
|
979
1013
|
const downloadUrl = URL.createObjectURL(blob);
|
|
@@ -1007,52 +1041,45 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
1007
1041
|
const handleFormOk = react.useCallback(async (values) => {
|
|
1008
1042
|
const isCreate = modalState.mode === 'create';
|
|
1009
1043
|
if (isCreate) {
|
|
1010
|
-
if (schema.api.create) {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1044
|
+
if (!schema.api.create) {
|
|
1045
|
+
messageApi.error('新增功能未配置');
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
await request(schema.api.create, {
|
|
1050
|
+
method: 'POST',
|
|
1051
|
+
body: JSON.stringify(values),
|
|
1052
|
+
});
|
|
1053
|
+
messageApi.success('新增成功');
|
|
1054
|
+
setModalState({ open: false, mode: 'create' });
|
|
1055
|
+
fetchList();
|
|
1056
|
+
}
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
console.error('Create failed:', error);
|
|
1059
|
+
messageApi.error('新增失败,请稍后重试');
|
|
1024
1060
|
}
|
|
1025
|
-
const newRecord = Object.assign({ [rowKey]: `local-${Date.now()}` }, values);
|
|
1026
|
-
localDataRef.current = [newRecord, ...localDataRef.current];
|
|
1027
|
-
localFilter(filterParams, 1, pageSize);
|
|
1028
|
-
setPage(1);
|
|
1029
|
-
messageApi.success('新增成功(演示模式)');
|
|
1030
1061
|
}
|
|
1031
1062
|
else {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1063
|
+
if (!schema.api.update) {
|
|
1064
|
+
messageApi.error('编辑功能未配置');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
await request(buildUrl(schema.api.update, values), {
|
|
1069
|
+
method: 'PUT',
|
|
1070
|
+
body: JSON.stringify(values),
|
|
1071
|
+
});
|
|
1072
|
+
messageApi.success('编辑成功');
|
|
1073
|
+
setModalState({ open: false, mode: 'create' });
|
|
1074
|
+
fetchList();
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
console.error('Update failed:', error);
|
|
1078
|
+
messageApi.error('编辑失败,请稍后重试');
|
|
1047
1079
|
}
|
|
1048
|
-
localDataRef.current = localDataRef.current.map(r => r[rowKey] === id ? Object.assign(Object.assign({}, r), values) : r);
|
|
1049
|
-
localFilter(filterParams, page, pageSize);
|
|
1050
|
-
messageApi.success('编辑成功(演示模式)');
|
|
1051
1080
|
}
|
|
1052
|
-
setModalState({ open: false, mode: 'create' });
|
|
1053
1081
|
}, [
|
|
1054
|
-
request, modalState.mode, schema.api,
|
|
1055
|
-
fetchList, localFilter, filterParams, page, pageSize, messageApi,
|
|
1082
|
+
request, modalState.mode, schema.api, fetchList, messageApi,
|
|
1056
1083
|
]);
|
|
1057
1084
|
return (jsxRuntime.jsxs("div", { style: { padding: 24, background: '#f5f6fa', minHeight: '100vh' }, children: [contextHolder, jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }, children: [jsxRuntime.jsx(Title, { level: 4, style: { margin: 0 }, children: schema.title }), schema.api.create && (jsxRuntime.jsx(antd.Button, { type: "primary", icon: jsxRuntime.jsx(icons.PlusOutlined, {}), onClick: () => setModalState({ open: true, mode: 'create', record: undefined }), children: schema.createButtonLabel || '新增' }))] }), jsxRuntime.jsx(DynamicFilter, { schema: schema, onSearch: handleSearch, onReset: () => handleSearch({}) }), jsxRuntime.jsx(DynamicTable, { schema: schema, data: data, loading: loading, pagination: {
|
|
1058
1085
|
current: page,
|