crud-page-react 0.0.2 → 0.0.5
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 +10 -0
- package/dist/index.esm.js +132 -90
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +132 -90
- package/dist/index.js.map +1 -1
- package/dist/types/schema.d.ts +10 -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
|
@@ -129,17 +129,27 @@ interface ActionPermission {
|
|
|
129
129
|
role?: string[];
|
|
130
130
|
condition?: string;
|
|
131
131
|
}
|
|
132
|
+
interface ActionApiConfig {
|
|
133
|
+
url: string;
|
|
134
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
135
|
+
data?: Record<string, unknown>;
|
|
136
|
+
headers?: Record<string, string>;
|
|
137
|
+
responseType?: 'json' | 'blob' | 'text';
|
|
138
|
+
}
|
|
132
139
|
interface ActionSchema {
|
|
133
140
|
key: string;
|
|
134
141
|
label: string;
|
|
135
142
|
type: ActionType;
|
|
136
143
|
icon?: string;
|
|
137
144
|
danger?: boolean;
|
|
145
|
+
color?: string;
|
|
138
146
|
permission?: ActionPermission;
|
|
147
|
+
condition?: Record<string, unknown>;
|
|
139
148
|
confirm?: {
|
|
140
149
|
title: string;
|
|
141
150
|
content?: string;
|
|
142
151
|
};
|
|
152
|
+
api?: ActionApiConfig;
|
|
143
153
|
}
|
|
144
154
|
interface PaginationConfig {
|
|
145
155
|
pageSize?: number;
|
package/dist/index.esm.js
CHANGED
|
@@ -810,9 +810,12 @@ function DynamicForm({ schema, mode, visible, initialValues, onSubmit, onCancel,
|
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
const { Title } = Typography;
|
|
813
|
-
/**
|
|
814
|
-
function buildUrl(template,
|
|
815
|
-
return template.replace(/:
|
|
813
|
+
/** 动态替换 URL 模板中的占位符 */
|
|
814
|
+
function buildUrl(template, record) {
|
|
815
|
+
return template.replace(/:(\w+)/g, (match, fieldName) => {
|
|
816
|
+
const value = record[fieldName];
|
|
817
|
+
return value !== undefined ? String(value) : match;
|
|
818
|
+
});
|
|
816
819
|
}
|
|
817
820
|
/** 通用请求封装 */
|
|
818
821
|
async function apiRequest(url, options) {
|
|
@@ -843,8 +846,8 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
843
846
|
const rowKey = schema.rowKey || 'id';
|
|
844
847
|
// 使用传入的apiRequest或默认的
|
|
845
848
|
const request = customApiRequest || apiRequest;
|
|
846
|
-
//
|
|
847
|
-
const
|
|
849
|
+
// 初始数据引用
|
|
850
|
+
const initialDataRef = useRef(initialData);
|
|
848
851
|
const [data, setData] = useState([]);
|
|
849
852
|
const [loading, setLoading] = useState(false);
|
|
850
853
|
const [total, setTotal] = useState(0);
|
|
@@ -853,33 +856,20 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
853
856
|
const [pageSize, setPageSize] = useState(((_a = schema.pagination) === null || _a === void 0 ? void 0 : _a.pageSize) || 10);
|
|
854
857
|
const [modalState, setModalState] = useState({ open: false, mode: 'create' });
|
|
855
858
|
const [messageApi, contextHolder] = message.useMessage();
|
|
856
|
-
// ----------
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (key.endsWith('_min'))
|
|
863
|
-
return Number(row[key.slice(0, -4)]) >= Number(val);
|
|
864
|
-
if (key.endsWith('_max'))
|
|
865
|
-
return Number(row[key.slice(0, -4)]) <= Number(val);
|
|
866
|
-
if (key.endsWith('_start') || key.endsWith('_end'))
|
|
867
|
-
return true;
|
|
868
|
-
const rowVal = row[key];
|
|
869
|
-
if (Array.isArray(rowVal)) {
|
|
870
|
-
return Array.isArray(val) ? val.some(v => rowVal.includes(v)) : rowVal.includes(val);
|
|
871
|
-
}
|
|
872
|
-
if (typeof val === 'string')
|
|
873
|
-
return String(rowVal).toLowerCase().includes(val.toLowerCase());
|
|
874
|
-
return rowVal === val;
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
const start = (p - 1) * ps;
|
|
878
|
-
setData(filtered.slice(start, start + ps));
|
|
879
|
-
setTotal(filtered.length);
|
|
859
|
+
// ---------- 初始化数据 ----------
|
|
860
|
+
const initializeData = useCallback(() => {
|
|
861
|
+
if (initialDataRef.current.length > 0) {
|
|
862
|
+
setData(initialDataRef.current);
|
|
863
|
+
setTotal(initialDataRef.current.length);
|
|
864
|
+
}
|
|
880
865
|
}, []);
|
|
881
866
|
// ---------- 获取列表 ----------
|
|
882
867
|
const fetchList = useCallback(async (params = filterParams, p = page, ps = pageSize) => {
|
|
868
|
+
if (!schema.api.list) {
|
|
869
|
+
// 没有配置 API,使用初始数据
|
|
870
|
+
initializeData();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
883
873
|
setLoading(true);
|
|
884
874
|
try {
|
|
885
875
|
const query = new URLSearchParams({ page: String(p), pageSize: String(ps) });
|
|
@@ -892,14 +882,22 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
892
882
|
setData(list);
|
|
893
883
|
setTotal(tot);
|
|
894
884
|
}
|
|
895
|
-
catch (
|
|
896
|
-
|
|
897
|
-
|
|
885
|
+
catch (error) {
|
|
886
|
+
console.error('Failed to fetch list:', error);
|
|
887
|
+
messageApi.error('获取数据失败,请检查网络连接或联系管理员');
|
|
888
|
+
// 如果有初始数据,显示初始数据
|
|
889
|
+
if (initialDataRef.current.length > 0) {
|
|
890
|
+
initializeData();
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
setData([]);
|
|
894
|
+
setTotal(0);
|
|
895
|
+
}
|
|
898
896
|
}
|
|
899
897
|
finally {
|
|
900
898
|
setLoading(false);
|
|
901
899
|
}
|
|
902
|
-
}, [request, schema.api.list, filterParams, page, pageSize,
|
|
900
|
+
}, [request, schema.api.list, filterParams, page, pageSize, initializeData, messageApi]);
|
|
903
901
|
// 初始加载 & 参数变化时重新请求
|
|
904
902
|
useEffect(() => {
|
|
905
903
|
fetchList(filterParams, page, pageSize);
|
|
@@ -917,24 +915,22 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
917
915
|
}, []);
|
|
918
916
|
// ---------- 删除 ----------
|
|
919
917
|
const handleDelete = useCallback(async (record) => {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
catch (_a) {
|
|
929
|
-
// 降级本地
|
|
930
|
-
}
|
|
918
|
+
if (!schema.api.delete) {
|
|
919
|
+
messageApi.error('删除功能未配置');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
await request(buildUrl(schema.api.delete, record), { method: 'DELETE' });
|
|
924
|
+
messageApi.success('删除成功');
|
|
925
|
+
fetchList();
|
|
931
926
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
927
|
+
catch (error) {
|
|
928
|
+
console.error('Delete failed:', error);
|
|
929
|
+
messageApi.error('删除失败,请稍后重试');
|
|
930
|
+
}
|
|
931
|
+
}, [request, schema.api.delete, fetchList, messageApi]);
|
|
936
932
|
// ---------- 操作列点击 ----------
|
|
937
|
-
const handleAction = useCallback((action, record) => {
|
|
933
|
+
const handleAction = useCallback(async (action, record) => {
|
|
938
934
|
if (action.type === 'view') {
|
|
939
935
|
setModalState({ open: true, mode: 'view', record });
|
|
940
936
|
}
|
|
@@ -944,57 +940,103 @@ const CrudPage = ({ schema, initialData = [], apiRequest: customApiRequest }) =>
|
|
|
944
940
|
else if (action.type === 'delete') {
|
|
945
941
|
handleDelete(record);
|
|
946
942
|
}
|
|
947
|
-
|
|
943
|
+
else if (action.type === 'custom' && action.api) {
|
|
944
|
+
// 处理自定义 action 的 API 调用
|
|
945
|
+
try {
|
|
946
|
+
// 构建 URL,动态替换占位符
|
|
947
|
+
let url = action.api.url;
|
|
948
|
+
// 替换所有 :fieldName 格式的占位符
|
|
949
|
+
url = url.replace(/:(\w+)/g, (match, fieldName) => {
|
|
950
|
+
const value = record[fieldName];
|
|
951
|
+
return value !== undefined ? String(value) : match;
|
|
952
|
+
});
|
|
953
|
+
// 构建请求选项
|
|
954
|
+
const options = {
|
|
955
|
+
method: action.api.method,
|
|
956
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, action.api.headers)
|
|
957
|
+
};
|
|
958
|
+
// 添加请求体数据
|
|
959
|
+
if (action.api.data && ['POST', 'PUT', 'PATCH'].includes(action.api.method)) {
|
|
960
|
+
options.body = JSON.stringify(Object.assign(Object.assign({}, action.api.data), {
|
|
961
|
+
// 可以添加动态数据
|
|
962
|
+
recordId: record[rowKey], timestamp: new Date().toISOString() }));
|
|
963
|
+
}
|
|
964
|
+
// 调用 API
|
|
965
|
+
const response = await request(url, options);
|
|
966
|
+
// 处理特殊响应类型
|
|
967
|
+
if (action.api.responseType === 'blob') {
|
|
968
|
+
// 处理文件下载
|
|
969
|
+
const blob = new Blob([response]);
|
|
970
|
+
const downloadUrl = URL.createObjectURL(blob);
|
|
971
|
+
const a = document.createElement('a');
|
|
972
|
+
a.href = downloadUrl;
|
|
973
|
+
a.download = `${action.key}.pdf`;
|
|
974
|
+
a.click();
|
|
975
|
+
URL.revokeObjectURL(downloadUrl);
|
|
976
|
+
messageApi.success(`${action.label}成功`);
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
// 处理 JSON 响应
|
|
980
|
+
const result = response;
|
|
981
|
+
if (result.success !== false) {
|
|
982
|
+
messageApi.success(`${action.label}成功`);
|
|
983
|
+
// 刷新数据
|
|
984
|
+
await fetchList();
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
messageApi.error(result.message || `${action.label}失败`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
console.error(`Action ${action.key} failed:`, error);
|
|
993
|
+
messageApi.error(`${action.label}失败`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}, [handleDelete, rowKey, request, messageApi, fetchList]);
|
|
948
997
|
// ---------- 新增 / 编辑提交 ----------
|
|
949
998
|
const handleFormOk = useCallback(async (values) => {
|
|
950
999
|
const isCreate = modalState.mode === 'create';
|
|
951
1000
|
if (isCreate) {
|
|
952
|
-
if (schema.api.create) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1001
|
+
if (!schema.api.create) {
|
|
1002
|
+
messageApi.error('新增功能未配置');
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
await request(schema.api.create, {
|
|
1007
|
+
method: 'POST',
|
|
1008
|
+
body: JSON.stringify(values),
|
|
1009
|
+
});
|
|
1010
|
+
messageApi.success('新增成功');
|
|
1011
|
+
setModalState({ open: false, mode: 'create' });
|
|
1012
|
+
fetchList();
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
console.error('Create failed:', error);
|
|
1016
|
+
messageApi.error('新增失败,请稍后重试');
|
|
966
1017
|
}
|
|
967
|
-
const newRecord = Object.assign({ [rowKey]: `local-${Date.now()}` }, values);
|
|
968
|
-
localDataRef.current = [newRecord, ...localDataRef.current];
|
|
969
|
-
localFilter(filterParams, 1, pageSize);
|
|
970
|
-
setPage(1);
|
|
971
|
-
messageApi.success('新增成功(演示模式)');
|
|
972
1018
|
}
|
|
973
1019
|
else {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1020
|
+
if (!schema.api.update) {
|
|
1021
|
+
messageApi.error('编辑功能未配置');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
await request(buildUrl(schema.api.update, values), {
|
|
1026
|
+
method: 'PUT',
|
|
1027
|
+
body: JSON.stringify(values),
|
|
1028
|
+
});
|
|
1029
|
+
messageApi.success('编辑成功');
|
|
1030
|
+
setModalState({ open: false, mode: 'create' });
|
|
1031
|
+
fetchList();
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
console.error('Update failed:', error);
|
|
1035
|
+
messageApi.error('编辑失败,请稍后重试');
|
|
989
1036
|
}
|
|
990
|
-
localDataRef.current = localDataRef.current.map(r => r[rowKey] === id ? Object.assign(Object.assign({}, r), values) : r);
|
|
991
|
-
localFilter(filterParams, page, pageSize);
|
|
992
|
-
messageApi.success('编辑成功(演示模式)');
|
|
993
1037
|
}
|
|
994
|
-
setModalState({ open: false, mode: 'create' });
|
|
995
1038
|
}, [
|
|
996
|
-
request, modalState.mode, schema.api,
|
|
997
|
-
fetchList, localFilter, filterParams, page, pageSize, messageApi,
|
|
1039
|
+
request, modalState.mode, schema.api, fetchList, messageApi,
|
|
998
1040
|
]);
|
|
999
1041
|
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: {
|
|
1000
1042
|
current: page,
|