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 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
- /** 替换 URL 模板中的 :id */
814
- function buildUrl(template, id) {
815
- return template.replace(/:id/, String(id));
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
- // 本地兜底数据(API 失败时使用)
847
- const localDataRef = useRef(initialData);
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
- // ---------- 本地过滤(API 失败时兜底) ----------
857
- const localFilter = useCallback((params, p, ps) => {
858
- const filtered = localDataRef.current.filter(row => {
859
- return Object.entries(params).every(([key, val]) => {
860
- if (val === undefined || val === null || val === '')
861
- return true;
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 (_a) {
896
- // API 不可用 本地演示模式
897
- localFilter(params, p, ps);
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, localFilter]);
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
- const id = record[rowKey];
921
- if (schema.api.delete) {
922
- try {
923
- await request(buildUrl(schema.api.delete, id), { method: 'DELETE' });
924
- messageApi.success('删除成功');
925
- fetchList();
926
- return;
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
- localDataRef.current = localDataRef.current.filter(r => r[rowKey] !== id);
933
- localFilter(filterParams, page, pageSize);
934
- messageApi.success('删除成功(演示模式)');
935
- }, [request, schema.api.delete, rowKey, fetchList, localFilter, filterParams, page, pageSize, messageApi]);
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
- }, [handleDelete]);
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
- try {
954
- await request(schema.api.create, {
955
- method: 'POST',
956
- body: JSON.stringify(values),
957
- });
958
- messageApi.success('新增成功');
959
- setModalState({ open: false, mode: 'create' });
960
- fetchList();
961
- return;
962
- }
963
- catch (_a) {
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
- const id = values[rowKey];
975
- if (schema.api.update) {
976
- try {
977
- await request(buildUrl(schema.api.update, id), {
978
- method: 'PUT',
979
- body: JSON.stringify(values),
980
- });
981
- messageApi.success('编辑成功');
982
- setModalState({ open: false, mode: 'create' });
983
- fetchList();
984
- return;
985
- }
986
- catch (_b) {
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, rowKey,
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,