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 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
- // 本地兜底数据(API 失败时使用)
850
- const localDataRef = useRef(initialData);
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
- // ---------- 本地过滤(API 失败时兜底) ----------
860
- const localFilter = useCallback((params, p, ps) => {
861
- const filtered = localDataRef.current.filter(row => {
862
- return Object.entries(params).every(([key, val]) => {
863
- if (val === undefined || val === null || val === '')
864
- return true;
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 (_a) {
899
- // API 不可用 本地演示模式
900
- localFilter(params, p, ps);
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, localFilter]);
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
- const id = record[rowKey];
924
- if (schema.api.delete) {
925
- try {
926
- await request(buildUrl(schema.api.delete, record), { method: 'DELETE' });
927
- messageApi.success('删除成功');
928
- fetchList();
929
- return;
930
- }
931
- catch (_a) {
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
- localDataRef.current = localDataRef.current.filter(r => r[rowKey] !== id);
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' && action.api) {
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 = action.api.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: action.api.method,
963
- headers: Object.assign({ 'Content-Type': 'application/json' }, action.api.headers)
996
+ method: apiConfig.method || 'GET',
997
+ headers: Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers)
964
998
  };
965
- // 添加请求体数据
966
- if (action.api.data && ['POST', 'PUT', 'PATCH'].includes(action.api.method)) {
967
- options.body = JSON.stringify(Object.assign(Object.assign({}, action.api.data), {
968
- // 可以添加动态数据
969
- recordId: record[rowKey], timestamp: new Date().toISOString() }));
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 (action.api.responseType === 'blob') {
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
- try {
1010
- await request(schema.api.create, {
1011
- method: 'POST',
1012
- body: JSON.stringify(values),
1013
- });
1014
- messageApi.success('新增成功');
1015
- setModalState({ open: false, mode: 'create' });
1016
- fetchList();
1017
- return;
1018
- }
1019
- catch (_a) {
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
- const id = values[rowKey];
1031
- if (schema.api.update) {
1032
- try {
1033
- await request(buildUrl(schema.api.update, values), {
1034
- method: 'PUT',
1035
- body: JSON.stringify(values),
1036
- });
1037
- messageApi.success('编辑成功');
1038
- setModalState({ open: false, mode: 'create' });
1039
- fetchList();
1040
- return;
1041
- }
1042
- catch (_b) {
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, rowKey,
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,