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/README.md
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
- 🔍 **智能筛选** - 自动生成筛选器,支持范围查询
|
|
10
10
|
- 📊 **灵活表格** - 可配置列宽、排序、固定列等
|
|
11
11
|
- 🎨 **Raw JSON 模式** - 支持原始 JSON 编辑和查看
|
|
12
|
-
- 🔌 **自定义 API** - 支持自定义 API
|
|
12
|
+
- 🔌 **自定义 API** - 支持自定义 API 请求函数和动态 URL 参数
|
|
13
|
+
- 🌐 **动态 URL 参数** - 支持任意字段作为 URL 参数 (`:id`, `:orderNo`, `:customerName` 等)
|
|
14
|
+
- ⚡ **自定义操作** - 支持配置自定义按钮操作和 API 调用
|
|
13
15
|
- 📱 **响应式设计** - 适配移动端和桌面端
|
|
14
16
|
- 🎯 **TypeScript 支持** - 完整的类型定义
|
|
15
17
|
|
|
@@ -103,6 +105,53 @@ function App() {
|
|
|
103
105
|
export default App;
|
|
104
106
|
```
|
|
105
107
|
|
|
108
|
+
## 动态 URL 参数
|
|
109
|
+
|
|
110
|
+
支持在 API 配置中使用任意字段作为 URL 参数:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
const schema = {
|
|
114
|
+
api: {
|
|
115
|
+
list: '/api/orders',
|
|
116
|
+
create: '/api/orders',
|
|
117
|
+
update: '/api/orders/:id', // 使用 id 字段
|
|
118
|
+
delete: '/api/orders/:orderNo', // 使用 orderNo 字段
|
|
119
|
+
detail: '/api/orders/:id',
|
|
120
|
+
},
|
|
121
|
+
actions: [
|
|
122
|
+
{
|
|
123
|
+
key: 'track',
|
|
124
|
+
label: '物流跟踪',
|
|
125
|
+
type: 'custom',
|
|
126
|
+
api: {
|
|
127
|
+
url: '/api/tracking/:orderNo', // 使用 orderNo 字段
|
|
128
|
+
method: 'GET',
|
|
129
|
+
responseType: 'json'
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'notify',
|
|
134
|
+
label: '通知客户',
|
|
135
|
+
type: 'custom',
|
|
136
|
+
api: {
|
|
137
|
+
url: '/api/notify/:customerName', // 使用 customerName 字段
|
|
138
|
+
method: 'POST',
|
|
139
|
+
data: {
|
|
140
|
+
message: '您的订单状态已更新'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
// ... 其他配置
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 支持的占位符格式
|
|
150
|
+
- `:id` → 记录中的 `id` 字段值
|
|
151
|
+
- `:orderNo` → 记录中的 `orderNo` 字段值
|
|
152
|
+
- `:customerName` → 记录中的 `customerName` 字段值
|
|
153
|
+
- 任意 `:fieldName` → 记录中的 `fieldName` 字段值
|
|
154
|
+
|
|
106
155
|
## 自定义 API 请求
|
|
107
156
|
|
|
108
157
|
```tsx
|
package/dist/index.d.ts
CHANGED
|
@@ -150,6 +150,7 @@ interface ActionSchema {
|
|
|
150
150
|
content?: string;
|
|
151
151
|
};
|
|
152
152
|
api?: ActionApiConfig;
|
|
153
|
+
apiKey?: string;
|
|
153
154
|
}
|
|
154
155
|
interface PaginationConfig {
|
|
155
156
|
pageSize?: number;
|
|
@@ -165,6 +166,7 @@ interface CrudPageSchema {
|
|
|
165
166
|
update?: string;
|
|
166
167
|
delete?: string;
|
|
167
168
|
detail?: string;
|
|
169
|
+
[key: string]: string | ActionApiConfig | undefined;
|
|
168
170
|
};
|
|
169
171
|
fields: FieldSchema[];
|
|
170
172
|
actions?: ActionSchema[];
|
package/dist/index.esm.js
CHANGED
|
@@ -817,6 +817,23 @@ function buildUrl(template, record) {
|
|
|
817
817
|
return value !== undefined ? String(value) : match;
|
|
818
818
|
});
|
|
819
819
|
}
|
|
820
|
+
/** 处理模板数据,支持 {{fieldName}} 格式的变量替换 */
|
|
821
|
+
function processTemplateData(data, record) {
|
|
822
|
+
const result = {};
|
|
823
|
+
for (const [key, value] of Object.entries(data)) {
|
|
824
|
+
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
|
825
|
+
// 替换模板变量 {{fieldName}}
|
|
826
|
+
result[key] = value.replace(/\{\{(\w+)\}\}/g, (match, fieldName) => {
|
|
827
|
+
const fieldValue = record[fieldName];
|
|
828
|
+
return fieldValue !== undefined ? String(fieldValue) : match;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
result[key] = value;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
820
837
|
/** 通用请求封装 */
|
|
821
838
|
async function apiRequest(url, options) {
|
|
822
839
|
const res = await fetch(url, Object.assign({ headers: { 'Content-Type': 'application/json' } }, options));
|
|
@@ -846,8 +863,8 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
846
863
|
const rowKey = schema.rowKey || 'id';
|
|
847
864
|
// 使用传入的apiRequest或默认的
|
|
848
865
|
const request = customApiRequest || apiRequest;
|
|
849
|
-
//
|
|
850
|
-
const
|
|
866
|
+
// 初始数据引用
|
|
867
|
+
const initialDataRef = useRef(initialData);
|
|
851
868
|
const [data, setData] = useState([]);
|
|
852
869
|
const [loading, setLoading] = useState(false);
|
|
853
870
|
const [total, setTotal] = useState(0);
|
|
@@ -856,33 +873,20 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
856
873
|
const [pageSize, setPageSize] = useState(((_a = schema.pagination) === null || _a === void 0 ? void 0 : _a.pageSize) || 10);
|
|
857
874
|
const [modalState, setModalState] = useState({ open: false, mode: 'create' });
|
|
858
875
|
const [messageApi, contextHolder] = message.useMessage();
|
|
859
|
-
// ----------
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (key.endsWith('_min'))
|
|
866
|
-
return Number(row[key.slice(0, -4)]) >= Number(val);
|
|
867
|
-
if (key.endsWith('_max'))
|
|
868
|
-
return Number(row[key.slice(0, -4)]) <= Number(val);
|
|
869
|
-
if (key.endsWith('_start') || key.endsWith('_end'))
|
|
870
|
-
return true;
|
|
871
|
-
const rowVal = row[key];
|
|
872
|
-
if (Array.isArray(rowVal)) {
|
|
873
|
-
return Array.isArray(val) ? val.some(v => rowVal.includes(v)) : rowVal.includes(val);
|
|
874
|
-
}
|
|
875
|
-
if (typeof val === 'string')
|
|
876
|
-
return String(rowVal).toLowerCase().includes(val.toLowerCase());
|
|
877
|
-
return rowVal === val;
|
|
878
|
-
});
|
|
879
|
-
});
|
|
880
|
-
const start = (p - 1) * ps;
|
|
881
|
-
setData(filtered.slice(start, start + ps));
|
|
882
|
-
setTotal(filtered.length);
|
|
876
|
+
// ---------- 初始化数据 ----------
|
|
877
|
+
const initializeData = useCallback(() => {
|
|
878
|
+
if (initialDataRef.current.length > 0) {
|
|
879
|
+
setData(initialDataRef.current);
|
|
880
|
+
setTotal(initialDataRef.current.length);
|
|
881
|
+
}
|
|
883
882
|
}, []);
|
|
884
883
|
// ---------- 获取列表 ----------
|
|
885
884
|
const fetchList = useCallback(async (params = filterParams, p = page, ps = pageSize) => {
|
|
885
|
+
if (!schema.api.list) {
|
|
886
|
+
// 没有配置 API,使用初始数据
|
|
887
|
+
initializeData();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
886
890
|
setLoading(true);
|
|
887
891
|
try {
|
|
888
892
|
const query = new URLSearchParams({ page: String(p), pageSize: String(ps) });
|
|
@@ -895,14 +899,22 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
895
899
|
setData(list);
|
|
896
900
|
setTotal(tot);
|
|
897
901
|
}
|
|
898
|
-
catch (
|
|
899
|
-
|
|
900
|
-
|
|
902
|
+
catch (error) {
|
|
903
|
+
console.error('Failed to fetch list:', error);
|
|
904
|
+
messageApi.error('获取数据失败,请检查网络连接或联系管理员');
|
|
905
|
+
// 如果有初始数据,显示初始数据
|
|
906
|
+
if (initialDataRef.current.length > 0) {
|
|
907
|
+
initializeData();
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
setData([]);
|
|
911
|
+
setTotal(0);
|
|
912
|
+
}
|
|
901
913
|
}
|
|
902
914
|
finally {
|
|
903
915
|
setLoading(false);
|
|
904
916
|
}
|
|
905
|
-
}, [request, schema.api.list, filterParams, page, pageSize,
|
|
917
|
+
}, [request, schema.api.list, filterParams, page, pageSize, initializeData, messageApi]);
|
|
906
918
|
// 初始加载 & 参数变化时重新请求
|
|
907
919
|
useEffect(() => {
|
|
908
920
|
fetchList(filterParams, page, pageSize);
|
|
@@ -920,22 +932,20 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
920
932
|
}, []);
|
|
921
933
|
// ---------- 删除 ----------
|
|
922
934
|
const handleDelete = useCallback(async (record) => {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
935
|
+
if (!schema.api.delete) {
|
|
936
|
+
messageApi.error('删除功能未配置');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
await request(buildUrl(schema.api.delete, record), { method: 'DELETE' });
|
|
941
|
+
messageApi.success('删除成功');
|
|
942
|
+
fetchList();
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
console.error('Delete failed:', error);
|
|
946
|
+
messageApi.error('删除失败,请稍后重试');
|
|
934
947
|
}
|
|
935
|
-
|
|
936
|
-
localFilter(filterParams, page, pageSize);
|
|
937
|
-
messageApi.success('删除成功(演示模式)');
|
|
938
|
-
}, [request, schema.api.delete, rowKey, fetchList, localFilter, filterParams, page, pageSize, messageApi]);
|
|
948
|
+
}, [request, schema.api.delete, fetchList, messageApi]);
|
|
939
949
|
// ---------- 操作列点击 ----------
|
|
940
950
|
const handleAction = useCallback(async (action, record) => {
|
|
941
951
|
if (action.type === 'view') {
|
|
@@ -947,31 +957,55 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
947
957
|
else if (action.type === 'delete') {
|
|
948
958
|
handleDelete(record);
|
|
949
959
|
}
|
|
950
|
-
else if (action.type === 'custom'
|
|
960
|
+
else if (action.type === 'custom') {
|
|
951
961
|
// 处理自定义 action 的 API 调用
|
|
962
|
+
let apiConfig;
|
|
963
|
+
// 优先使用 apiKey 引用统一配置,向后兼容 api 直接配置
|
|
964
|
+
if (action.apiKey) {
|
|
965
|
+
const apiDef = schema.api[action.apiKey];
|
|
966
|
+
if (typeof apiDef === 'string') {
|
|
967
|
+
// 简单字符串 URL 配置
|
|
968
|
+
apiConfig = {
|
|
969
|
+
url: apiDef,
|
|
970
|
+
method: 'GET',
|
|
971
|
+
responseType: 'json'
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
else if (apiDef && typeof apiDef === 'object') {
|
|
975
|
+
// 完整的 API 配置对象
|
|
976
|
+
apiConfig = apiDef;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
else if (action.api) {
|
|
980
|
+
// 向后兼容:使用 action.api 配置
|
|
981
|
+
apiConfig = action.api;
|
|
982
|
+
}
|
|
983
|
+
if (!apiConfig) {
|
|
984
|
+
messageApi.error(`${action.label}未配置 API`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
952
987
|
try {
|
|
953
988
|
// 构建 URL,动态替换占位符
|
|
954
|
-
let url =
|
|
955
|
-
// 替换所有 :fieldName 格式的占位符
|
|
989
|
+
let url = apiConfig.url;
|
|
956
990
|
url = url.replace(/:(\w+)/g, (match, fieldName) => {
|
|
957
991
|
const value = record[fieldName];
|
|
958
992
|
return value !== undefined ? String(value) : match;
|
|
959
993
|
});
|
|
960
994
|
// 构建请求选项
|
|
961
995
|
const options = {
|
|
962
|
-
method:
|
|
963
|
-
headers: Object.assign({ 'Content-Type': 'application/json' },
|
|
996
|
+
method: apiConfig.method || 'GET',
|
|
997
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers)
|
|
964
998
|
};
|
|
965
|
-
//
|
|
966
|
-
if (
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
999
|
+
// 处理请求体数据
|
|
1000
|
+
if (apiConfig.data && ['POST', 'PUT', 'PATCH'].includes(apiConfig.method || 'GET')) {
|
|
1001
|
+
// 支持模板变量替换
|
|
1002
|
+
const processedData = processTemplateData(apiConfig.data, record);
|
|
1003
|
+
options.body = JSON.stringify(Object.assign(Object.assign({}, processedData), { recordId: record[rowKey], timestamp: new Date().toISOString() }));
|
|
970
1004
|
}
|
|
971
1005
|
// 调用 API
|
|
972
1006
|
const response = await request(url, options);
|
|
973
1007
|
// 处理特殊响应类型
|
|
974
|
-
if (
|
|
1008
|
+
if (apiConfig.responseType === 'blob') {
|
|
975
1009
|
// 处理文件下载
|
|
976
1010
|
const blob = new Blob([response]);
|
|
977
1011
|
const downloadUrl = URL.createObjectURL(blob);
|
|
@@ -1005,52 +1039,45 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
1005
1039
|
const handleFormOk = useCallback(async (values) => {
|
|
1006
1040
|
const isCreate = modalState.mode === 'create';
|
|
1007
1041
|
if (isCreate) {
|
|
1008
|
-
if (schema.api.create) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1042
|
+
if (!schema.api.create) {
|
|
1043
|
+
messageApi.error('新增功能未配置');
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
await request(schema.api.create, {
|
|
1048
|
+
method: 'POST',
|
|
1049
|
+
body: JSON.stringify(values),
|
|
1050
|
+
});
|
|
1051
|
+
messageApi.success('新增成功');
|
|
1052
|
+
setModalState({ open: false, mode: 'create' });
|
|
1053
|
+
fetchList();
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
console.error('Create failed:', error);
|
|
1057
|
+
messageApi.error('新增失败,请稍后重试');
|
|
1022
1058
|
}
|
|
1023
|
-
const newRecord = Object.assign({ [rowKey]: `local-${Date.now()}` }, values);
|
|
1024
|
-
localDataRef.current = [newRecord, ...localDataRef.current];
|
|
1025
|
-
localFilter(filterParams, 1, pageSize);
|
|
1026
|
-
setPage(1);
|
|
1027
|
-
messageApi.success('新增成功(演示模式)');
|
|
1028
1059
|
}
|
|
1029
1060
|
else {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1061
|
+
if (!schema.api.update) {
|
|
1062
|
+
messageApi.error('编辑功能未配置');
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
await request(buildUrl(schema.api.update, values), {
|
|
1067
|
+
method: 'PUT',
|
|
1068
|
+
body: JSON.stringify(values),
|
|
1069
|
+
});
|
|
1070
|
+
messageApi.success('编辑成功');
|
|
1071
|
+
setModalState({ open: false, mode: 'create' });
|
|
1072
|
+
fetchList();
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
console.error('Update failed:', error);
|
|
1076
|
+
messageApi.error('编辑失败,请稍后重试');
|
|
1045
1077
|
}
|
|
1046
|
-
localDataRef.current = localDataRef.current.map(r => r[rowKey] === id ? Object.assign(Object.assign({}, r), values) : r);
|
|
1047
|
-
localFilter(filterParams, page, pageSize);
|
|
1048
|
-
messageApi.success('编辑成功(演示模式)');
|
|
1049
1078
|
}
|
|
1050
|
-
setModalState({ open: false, mode: 'create' });
|
|
1051
1079
|
}, [
|
|
1052
|
-
request, modalState.mode, schema.api,
|
|
1053
|
-
fetchList, localFilter, filterParams, page, pageSize, messageApi,
|
|
1080
|
+
request, modalState.mode, schema.api, fetchList, messageApi,
|
|
1054
1081
|
]);
|
|
1055
1082
|
return (jsxs("div", { style: { padding: 24, background: '#f5f6fa', minHeight: '100vh' }, children: [contextHolder, jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }, children: [jsx(Title, { level: 4, style: { margin: 0 }, children: schema.title }), schema.api.create && (jsx(Button, { type: "primary", icon: jsx(PlusOutlined, {}), onClick: () => setModalState({ open: true, mode: 'create', record: undefined }), children: schema.createButtonLabel || '新增' }))] }), jsx(DynamicFilter, { schema: schema, onSearch: handleSearch, onReset: () => handleSearch({}) }), jsx(DynamicTable, { schema: schema, data: data, loading: loading, pagination: {
|
|
1056
1083
|
current: page,
|