imean-service-engine-htmx-plugin 2.10.1 → 2.11.1

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/dist/index.js CHANGED
@@ -878,6 +878,22 @@ function convertBoolean(value) {
878
878
  }
879
879
  return value;
880
880
  }
881
+ function convertDate(value) {
882
+ if (typeof value === "string") {
883
+ const trimmed = value.trim();
884
+ if (trimmed !== "") {
885
+ const dateValue = new Date(trimmed);
886
+ if (!isNaN(dateValue.getTime())) {
887
+ return dateValue;
888
+ }
889
+ }
890
+ return value;
891
+ }
892
+ if (value instanceof Date) {
893
+ return value;
894
+ }
895
+ return value;
896
+ }
881
897
  function processArrayElement(item, elementType) {
882
898
  if (typeof item === "object" && item !== null && !Array.isArray(item)) {
883
899
  const { typeName } = resolveZodType(elementType);
@@ -997,6 +1013,9 @@ function preprocessFormData(data, zodSchema) {
997
1013
  case "boolean":
998
1014
  processed[fieldName] = convertBoolean(value);
999
1015
  break;
1016
+ case "date":
1017
+ processed[fieldName] = convertDate(value);
1018
+ break;
1000
1019
  case "array":
1001
1020
  processed[fieldName] = processArrayType(value, actualSchema);
1002
1021
  break;
@@ -1019,6 +1038,8 @@ function convertValueByType(value, schema) {
1019
1038
  return convertNumber(value);
1020
1039
  case "boolean":
1021
1040
  return convertBoolean(value);
1041
+ case "date":
1042
+ return convertDate(value);
1022
1043
  default:
1023
1044
  return value;
1024
1045
  }
@@ -1244,6 +1265,11 @@ var BaseModelFeature = class extends BaseFeature {
1244
1265
  };
1245
1266
  var BaseFormFeature = class extends BaseModelFeature {
1246
1267
  groups;
1268
+ options;
1269
+ constructor(options) {
1270
+ super(options);
1271
+ this.options = options;
1272
+ }
1247
1273
  /**
1248
1274
  * 设置标题和描述到 context
1249
1275
  */
@@ -1316,13 +1342,13 @@ var BaseFormFeature = class extends BaseModelFeature {
1316
1342
  * 处理请求
1317
1343
  */
1318
1344
  async handler(context) {
1319
- context.actions = this.getDefaultActions(context);
1345
+ context.actions = [
1346
+ ...this.options.actions ?? [],
1347
+ ...this.getDefaultActions(context)
1348
+ ];
1320
1349
  if (context.ctx.req.method === "GET") {
1321
1350
  const initialData = await this.getInitialData(context);
1322
- await this.setTitleAndDescription(
1323
- context,
1324
- initialData
1325
- );
1351
+ await this.setTitleAndDescription(context, initialData);
1326
1352
  return this.renderForm(context, initialData);
1327
1353
  }
1328
1354
  const formData = preprocessFormData(context.body, this.schema);
@@ -1424,22 +1450,21 @@ var BaseFormFeature = class extends BaseModelFeature {
1424
1450
 
1425
1451
  // src/features/default-create-feature.tsx
1426
1452
  var DefaultCreateFeature = class extends BaseFormFeature {
1427
- constructor(options) {
1453
+ createItem;
1454
+ constructor(createOptions) {
1428
1455
  super({
1429
- ...options,
1456
+ ...createOptions,
1430
1457
  name: "create",
1431
1458
  type: "create",
1432
1459
  routes: [
1433
1460
  { method: "get", path: "/new" },
1434
1461
  { method: "post", path: "" }
1435
1462
  ],
1436
- permission: options.permission || "create"
1463
+ permission: createOptions.permission || "create"
1437
1464
  });
1438
- this.options = options;
1439
- this.createItem = options.createItem;
1440
- this.groups = options.groups;
1465
+ this.createItem = createOptions.createItem;
1466
+ this.groups = createOptions.groups;
1441
1467
  }
1442
- createItem;
1443
1468
  getFormAction() {
1444
1469
  return "create";
1445
1470
  }
@@ -1964,24 +1989,23 @@ var DefaultDetailFeature = class extends BaseModelFeature {
1964
1989
 
1965
1990
  // src/features/default-edit-feature.tsx
1966
1991
  var DefaultEditFeature = class extends BaseFormFeature {
1967
- constructor(options) {
1992
+ getItem;
1993
+ updateItem;
1994
+ constructor(editOptions) {
1968
1995
  super({
1969
- ...options,
1996
+ ...editOptions,
1970
1997
  name: "edit",
1971
1998
  type: "edit",
1972
1999
  routes: [
1973
2000
  { method: "get", path: "/edit/:id" },
1974
2001
  { method: "put", path: "/:id" }
1975
2002
  ],
1976
- permission: options.permission || "edit"
2003
+ permission: editOptions.permission || "edit"
1977
2004
  });
1978
- this.options = options;
1979
- this.getItem = options.getItem;
1980
- this.updateItem = options.updateItem;
1981
- this.groups = options.groups;
2005
+ this.getItem = editOptions.getItem;
2006
+ this.updateItem = editOptions.updateItem;
2007
+ this.groups = editOptions.groups;
1982
2008
  }
1983
- getItem;
1984
- updateItem;
1985
2009
  getFormAction() {
1986
2010
  return "edit";
1987
2011
  }
@@ -2023,154 +2047,315 @@ var DefaultEditFeature = class extends BaseFormFeature {
2023
2047
  return void 0;
2024
2048
  }
2025
2049
  };
2026
- function FilterForm(props) {
2027
- const {
2028
- fields,
2029
- listPath,
2030
- currentFilters = {}
2031
- } = props;
2032
- function getFieldValue(field) {
2033
- const value = currentFilters[field.name];
2034
- if (value === null || value === void 0 || value === "") {
2035
- return "";
2050
+ function SubmitButton() {
2051
+ return /* @__PURE__ */ jsxRuntime.jsx(
2052
+ "button",
2053
+ {
2054
+ type: "submit",
2055
+ className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm",
2056
+ children: "\u7B5B\u9009"
2036
2057
  }
2037
- return String(value);
2038
- }
2039
- function getCheckboxValue(field) {
2040
- const value = currentFilters[field.name];
2041
- if (value === "true" || value === true) {
2042
- return true;
2058
+ );
2059
+ }
2060
+ function ResetButton({ listPath }) {
2061
+ return /* @__PURE__ */ jsxRuntime.jsx(
2062
+ "a",
2063
+ {
2064
+ href: listPath,
2065
+ "hx-get": listPath,
2066
+ className: "px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors font-medium text-sm",
2067
+ children: "\u91CD\u7F6E"
2043
2068
  }
2044
- return false;
2045
- }
2069
+ );
2070
+ }
2071
+ function AddConditionButton() {
2072
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2073
+ "button",
2074
+ {
2075
+ type: "button",
2076
+ "x-show": "getAvailableFields().length > 0",
2077
+ className: "px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors font-medium text-sm flex items-center gap-2",
2078
+ "x-on:click": "showFieldSelector=!showFieldSelector",
2079
+ children: [
2080
+ /* @__PURE__ */ jsxRuntime.jsx(
2081
+ "svg",
2082
+ {
2083
+ className: "w-4 h-4",
2084
+ fill: "none",
2085
+ stroke: "currentColor",
2086
+ viewBox: "0 0 24 24",
2087
+ "aria-hidden": "true",
2088
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2089
+ "path",
2090
+ {
2091
+ strokeLinecap: "round",
2092
+ strokeLinejoin: "round",
2093
+ strokeWidth: 2,
2094
+ d: "M12 4v16m8-8H4"
2095
+ }
2096
+ )
2097
+ }
2098
+ ),
2099
+ "\u6DFB\u52A0\u7B5B\u9009\u6761\u4EF6"
2100
+ ]
2101
+ }
2102
+ );
2103
+ }
2104
+ function FieldInput() {
2105
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2106
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "getField(condition.fieldName)?.type === 'checkbox'", children: /* @__PURE__ */ jsxRuntime.jsxs(
2107
+ "select",
2108
+ {
2109
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm appearance-none",
2110
+ "x-bind:name": "condition.fieldName",
2111
+ "x-model": "condition.value",
2112
+ autocomplete: "off",
2113
+ children: [
2114
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "\u662F" }),
2115
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "\u5426" })
2116
+ ]
2117
+ }
2118
+ ) }),
2119
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "getField(condition.fieldName)?.type === 'select' && getField(condition.fieldName)?.options", children: /* @__PURE__ */ jsxRuntime.jsx(
2120
+ "select",
2121
+ {
2122
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm appearance-none",
2123
+ "x-bind:name": "condition.fieldName",
2124
+ "x-model": "condition.value",
2125
+ autocomplete: "off",
2126
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2127
+ "template",
2128
+ {
2129
+ "x-for": "option in getField(condition.fieldName)?.options",
2130
+ "x-bind:key": "option.value",
2131
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2132
+ "option",
2133
+ {
2134
+ "x-bind:value": "String(option.value)",
2135
+ "x-text": "option.label"
2136
+ }
2137
+ )
2138
+ }
2139
+ )
2140
+ }
2141
+ ) }),
2142
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "getField(condition.fieldName)?.type === 'date'", children: /* @__PURE__ */ jsxRuntime.jsx(
2143
+ "input",
2144
+ {
2145
+ type: "date",
2146
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm",
2147
+ "x-bind:name": "condition.fieldName",
2148
+ "x-model": "condition.value",
2149
+ autocomplete: "off"
2150
+ }
2151
+ ) }),
2152
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "getField(condition.fieldName)?.type === 'number'", children: /* @__PURE__ */ jsxRuntime.jsx(
2153
+ "input",
2154
+ {
2155
+ type: "number",
2156
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm",
2157
+ "x-bind:name": "condition.fieldName",
2158
+ "x-bind:placeholder": "getField(condition.fieldName)?.placeholder || '\u8BF7\u8F93\u5165' + getField(condition.fieldName)?.label",
2159
+ "x-bind:step": "getField(condition.fieldName)?.step",
2160
+ "x-model": "condition.value",
2161
+ autocomplete: "off"
2162
+ }
2163
+ ) }),
2164
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "getField(condition.fieldName)?.type === 'email'", children: /* @__PURE__ */ jsxRuntime.jsx(
2165
+ "input",
2166
+ {
2167
+ type: "email",
2168
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm",
2169
+ "x-bind:name": "condition.fieldName",
2170
+ "x-bind:placeholder": "getField(condition.fieldName)?.placeholder || '\u8BF7\u8F93\u5165' + getField(condition.fieldName)?.label",
2171
+ "x-model": "condition.value",
2172
+ autocomplete: "off"
2173
+ }
2174
+ ) }),
2175
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-if": "!['checkbox', 'select', 'date', 'number', 'email'].includes(getField(condition.fieldName)?.type || '')", children: /* @__PURE__ */ jsxRuntime.jsx(
2176
+ "input",
2177
+ {
2178
+ type: "text",
2179
+ className: "w-full h-full px-3 py-2 border-0 rounded-none focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-sm",
2180
+ "x-bind:name": "condition.fieldName",
2181
+ "x-bind:placeholder": "getField(condition.fieldName)?.placeholder || '\u8BF7\u8F93\u5165' + getField(condition.fieldName)?.label",
2182
+ "x-model": "condition.value",
2183
+ autocomplete: "off"
2184
+ }
2185
+ ) })
2186
+ ] });
2187
+ }
2188
+ function FieldSelectorDropdown({ fields }) {
2189
+ const transitionProps = {
2190
+ "x-show": "showFieldSelector",
2191
+ "x-transition:enter": "transition ease-out duration-200",
2192
+ "x-transition:enter-start": "opacity-0 scale-95",
2193
+ "x-transition:enter-end": "opacity-100 scale-100",
2194
+ "x-transition:leave": "transition ease-in duration-150",
2195
+ "x-transition:leave-start": "opacity-100 scale-100",
2196
+ "x-transition:leave-end": "opacity-0 scale-95"
2197
+ };
2198
+ return /* @__PURE__ */ jsxRuntime.jsx(
2199
+ "div",
2200
+ {
2201
+ className: "absolute top-full left-0 mt-2 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-10",
2202
+ ...transitionProps,
2203
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3", children: [
2204
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-xs font-semibold text-gray-600 mb-2", children: "\u9009\u62E9\u7B5B\u9009\u5B57\u6BB5" }),
2205
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 max-h-64 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
2206
+ "template",
2207
+ {
2208
+ "x-for": "field in getAvailableFields()",
2209
+ "x-bind:key": "field.name",
2210
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2211
+ "button",
2212
+ {
2213
+ type: "button",
2214
+ className: "w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-lg transition-colors",
2215
+ "x-on:click": "onFieldSelected(field.name)",
2216
+ "x-text": "field.label"
2217
+ }
2218
+ )
2219
+ }
2220
+ ) })
2221
+ ] })
2222
+ }
2223
+ );
2224
+ }
2225
+ function FilterConditionItem({ children }) {
2226
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[auto_1fr_auto] items-stretch h-10 bg-gray-50 rounded-lg border border-gray-200 overflow-hidden", children: [
2227
+ /* @__PURE__ */ jsxRuntime.jsx(
2228
+ "label",
2229
+ {
2230
+ className: "px-3 flex items-center text-xs font-semibold text-gray-600 whitespace-nowrap",
2231
+ "x-text": "getFieldLabel(condition.fieldName)"
2232
+ }
2233
+ ),
2234
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full border-l border-r border-gray-300", children }),
2235
+ /* @__PURE__ */ jsxRuntime.jsx(
2236
+ "button",
2237
+ {
2238
+ type: "button",
2239
+ className: "px-3 flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors",
2240
+ "x-on:click": "removeCondition(condition.id)",
2241
+ title: "\u5220\u9664\u6B64\u7B5B\u9009\u6761\u4EF6",
2242
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2243
+ "svg",
2244
+ {
2245
+ className: "w-4 h-4",
2246
+ fill: "none",
2247
+ stroke: "currentColor",
2248
+ viewBox: "0 0 24 24",
2249
+ "aria-hidden": "true",
2250
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2251
+ "path",
2252
+ {
2253
+ strokeLinecap: "round",
2254
+ strokeLinejoin: "round",
2255
+ strokeWidth: 2,
2256
+ d: "M6 18L18 6M6 6l12 12"
2257
+ }
2258
+ )
2259
+ }
2260
+ )
2261
+ }
2262
+ )
2263
+ ] });
2264
+ }
2265
+ function DynamicFilters(props) {
2266
+ const { fields, listPath, currentFilters = {} } = props;
2046
2267
  if (fields.length === 0) {
2047
2268
  return null;
2048
2269
  }
2049
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm p-4 mb-6", "data-testid": "filter-form-container", children: /* @__PURE__ */ jsxRuntime.jsxs(
2270
+ const systemParams = /* @__PURE__ */ new Set(["page", "pageSize", "sortBy", "sortOrder"]);
2271
+ const initialConditions = [];
2272
+ for (const field of fields) {
2273
+ if (currentFilters.hasOwnProperty(field.name) && !systemParams.has(field.name)) {
2274
+ const value = currentFilters[field.name];
2275
+ initialConditions.push({
2276
+ id: `condition-${Date.now()}-${Math.random()}`,
2277
+ fieldName: field.name,
2278
+ value: value === null || value === void 0 ? "" : String(value)
2279
+ });
2280
+ }
2281
+ }
2282
+ const availableFields = fields.map((field) => ({
2283
+ name: field.name,
2284
+ label: field.label,
2285
+ type: field.type,
2286
+ options: field.options,
2287
+ step: field.step,
2288
+ placeholder: field.placeholder
2289
+ }));
2290
+ const escapedListPath = listPath.replace(/'/g, "\\'");
2291
+ const alpineData = `{
2292
+ conditions: ${JSON.stringify(initialConditions)},
2293
+ availableFields: ${JSON.stringify(availableFields)},
2294
+ showFieldSelector: false,
2295
+ listPath: '${escapedListPath}',
2296
+
2297
+
2298
+ // \u83B7\u53D6\u5B57\u6BB5\u5B9A\u4E49
2299
+ getField(fieldName) {
2300
+ return this.availableFields.find(f => f.name === fieldName);
2301
+ },
2302
+
2303
+ // \u83B7\u53D6\u5B57\u6BB5\u6807\u7B7E
2304
+ getFieldLabel(fieldName) {
2305
+ const field = this.getField(fieldName);
2306
+ return field ? field.label : fieldName;
2307
+ },
2308
+
2309
+ // \u68C0\u67E5\u5B57\u6BB5\u662F\u5426\u5DF2\u88AB\u4F7F\u7528
2310
+ isFieldUsed(fieldName) {
2311
+ return this.conditions.some(c => c.fieldName === fieldName);
2312
+ },
2313
+
2314
+ // \u83B7\u53D6\u6240\u6709\u53EF\u7528\u5B57\u6BB5\uFF08\u672A\u4F7F\u7528\u7684\u5B57\u6BB5\uFF09
2315
+ getAvailableFields() {
2316
+ return this.availableFields.filter(f => !this.isFieldUsed(f.name));
2317
+ },
2318
+
2319
+ // \u5220\u9664\u6761\u4EF6
2320
+ removeCondition(id) {
2321
+ this.conditions = this.conditions.filter(c => c.id !== id);
2322
+ },
2323
+
2324
+ // \u5B57\u6BB5\u9009\u62E9\u56DE\u8C03\uFF08\u4ECE\u6D6E\u5C42\u9009\u62E9\u5B57\u6BB5\u65F6\u8C03\u7528\uFF09
2325
+ onFieldSelected(fieldName) {
2326
+ // \u68C0\u67E5\u5B57\u6BB5\u662F\u5426\u53EF\u7528
2327
+ if (!this.isFieldUsed(fieldName)) {
2328
+ const newCondition = {
2329
+ id: 'condition-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
2330
+ fieldName: fieldName,
2331
+ value: ''
2332
+ };
2333
+ setTimeout(() => {
2334
+ this.conditions.push(newCondition);
2335
+ }, 200);
2336
+ }
2337
+ this.showFieldSelector = false;
2338
+ }
2339
+ }`;
2340
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm p-4 mb-6", children: /* @__PURE__ */ jsxRuntime.jsx(
2050
2341
  "form",
2051
2342
  {
2052
2343
  method: "get",
2053
2344
  action: listPath,
2054
2345
  "hx-get": listPath,
2055
- className: "space-y-4",
2056
- "data-testid": "filter-form",
2057
- children: [
2058
- fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", "data-testid": `filter-field-${field.name}`, children: field.type === "checkbox" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 pt-7", children: [
2059
- /* @__PURE__ */ jsxRuntime.jsx(
2060
- "input",
2061
- {
2062
- id: `filter-${field.name}`,
2063
- name: field.name,
2064
- type: "checkbox",
2065
- value: "true",
2066
- checked: getCheckboxValue(field),
2067
- className: "w-4 h-4 text-blue-600 bg-white border-gray-300 rounded focus:ring-blue-500 focus:ring-2",
2068
- "data-testid": `filter-checkbox-${field.name}`
2069
- }
2070
- ),
2071
- /* @__PURE__ */ jsxRuntime.jsx(
2072
- "label",
2073
- {
2074
- htmlFor: `filter-${field.name}`,
2075
- className: "text-sm font-semibold text-gray-700 cursor-pointer",
2076
- "data-testid": `filter-label-${field.name}`,
2077
- children: field.label
2078
- }
2079
- )
2080
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2081
- /* @__PURE__ */ jsxRuntime.jsx(
2082
- "label",
2083
- {
2084
- htmlFor: `filter-${field.name}`,
2085
- className: "block text-sm font-semibold text-gray-700",
2086
- "data-testid": `filter-label-${field.name}`,
2087
- children: field.label
2088
- }
2089
- ),
2090
- field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
2091
- "select",
2092
- {
2093
- id: `filter-${field.name}`,
2094
- name: field.name,
2095
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
2096
- "data-testid": `filter-select-${field.name}`,
2097
- children: [
2098
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: getFieldValue(field) === "", children: "\u5168\u90E8" }),
2099
- field.options?.map((option) => {
2100
- const optionValue = String(option.value);
2101
- const isSelected = getFieldValue(field) === optionValue;
2102
- return /* @__PURE__ */ jsxRuntime.jsx("option", { value: optionValue, selected: isSelected, children: option.label }, optionValue);
2103
- })
2104
- ]
2105
- }
2106
- ) : field.type === "date" ? /* @__PURE__ */ jsxRuntime.jsx(
2107
- "input",
2108
- {
2109
- id: `filter-${field.name}`,
2110
- name: field.name,
2111
- type: "date",
2112
- value: getFieldValue(field),
2113
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
2114
- "data-testid": `filter-input-${field.name}`
2115
- }
2116
- ) : field.type === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
2117
- "input",
2118
- {
2119
- id: `filter-${field.name}`,
2120
- name: field.name,
2121
- type: "number",
2122
- value: getFieldValue(field),
2123
- placeholder: field.placeholder || `\u8BF7\u8F93\u5165${field.label}`,
2124
- step: field.step,
2125
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
2126
- "data-testid": `filter-input-${field.name}`
2127
- }
2128
- ) : field.type === "email" ? /* @__PURE__ */ jsxRuntime.jsx(
2129
- "input",
2130
- {
2131
- id: `filter-${field.name}`,
2132
- name: field.name,
2133
- type: "email",
2134
- value: getFieldValue(field),
2135
- placeholder: field.placeholder || `\u8BF7\u8F93\u5165${field.label}`,
2136
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
2137
- "data-testid": `filter-input-${field.name}`
2138
- }
2139
- ) : /* @__PURE__ */ jsxRuntime.jsx(
2140
- "input",
2141
- {
2142
- id: `filter-${field.name}`,
2143
- name: field.name,
2144
- type: "text",
2145
- value: getFieldValue(field),
2146
- placeholder: field.placeholder || `\u8BF7\u8F93\u5165${field.label}`,
2147
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
2148
- "data-testid": `filter-input-${field.name}`
2149
- }
2150
- )
2151
- ] }) }, field.name)) }),
2152
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 pt-2", children: [
2153
- /* @__PURE__ */ jsxRuntime.jsx(
2154
- "button",
2155
- {
2156
- type: "submit",
2157
- className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium",
2158
- "data-testid": "filter-submit-button",
2159
- children: "\u7B5B\u9009"
2160
- }
2161
- ),
2162
- /* @__PURE__ */ jsxRuntime.jsx(
2163
- "a",
2164
- {
2165
- href: listPath,
2166
- "hx-get": listPath,
2167
- className: "px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors font-medium",
2168
- "data-testid": "filter-reset-button",
2169
- children: "\u91CD\u7F6E"
2170
- }
2171
- )
2346
+ className: "space-y-4 mb-0",
2347
+ "x-data": alpineData,
2348
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2349
+ /* @__PURE__ */ jsxRuntime.jsx("template", { "x-for": "condition in conditions", "x-bind:key": "condition.id", children: /* @__PURE__ */ jsxRuntime.jsx(FilterConditionItem, { children: /* @__PURE__ */ jsxRuntime.jsx(FieldInput, {}) }) }),
2350
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
2352
+ /* @__PURE__ */ jsxRuntime.jsx(AddConditionButton, {}),
2353
+ /* @__PURE__ */ jsxRuntime.jsx(FieldSelectorDropdown, { fields })
2354
+ ] }),
2355
+ /* @__PURE__ */ jsxRuntime.jsx(SubmitButton, {}),
2356
+ /* @__PURE__ */ jsxRuntime.jsx(ResetButton, { listPath })
2172
2357
  ] })
2173
- ]
2358
+ ] })
2174
2359
  }
2175
2360
  ) });
2176
2361
  }
@@ -2224,15 +2409,18 @@ function Pagination(props) {
2224
2409
  pageSize,
2225
2410
  total,
2226
2411
  totalPages,
2412
+ nextPageState,
2413
+ itemCount,
2227
2414
  baseUrl,
2228
2415
  currentParams = {}
2229
2416
  } = props;
2230
- if (totalPages <= 1) {
2417
+ const isCursorPagination = totalPages === void 0 && total === void 0;
2418
+ if (!isCursorPagination && (totalPages ?? 0) <= 1) {
2231
2419
  return null;
2232
2420
  }
2233
- const start = (page - 1) * pageSize + 1;
2234
- const end = Math.min(page * pageSize, total);
2235
- const buildUrl = (targetPage) => {
2421
+ const start = page && !isCursorPagination ? (page - 1) * pageSize + 1 : void 0;
2422
+ const end = page && total && !isCursorPagination ? Math.min(page * pageSize, total) : void 0;
2423
+ const buildPageUrl = (targetPage) => {
2236
2424
  const url = new URL(baseUrl, "http://localhost");
2237
2425
  for (const [key, value] of Object.entries(currentParams)) {
2238
2426
  if (value !== void 0 && value !== null && value !== "") {
@@ -2240,47 +2428,89 @@ function Pagination(props) {
2240
2428
  }
2241
2429
  }
2242
2430
  url.searchParams.set("page", String(targetPage));
2431
+ url.searchParams.delete("pageState");
2243
2432
  return url.pathname + url.search;
2244
2433
  };
2245
- return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "px-6 py-4 flex items-center justify-between border-t border-gray-200 bg-gray-50", "data-testid": "pagination", "aria-label": "\u5206\u9875\u5BFC\u822A", children: [
2246
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-700", "data-testid": "pagination-info", children: [
2247
- "\u663E\u793A ",
2248
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-start", children: start }),
2249
- " \u5230",
2250
- " ",
2251
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-end", children: end }),
2252
- " \u6761\uFF0C\u5171",
2253
- " ",
2254
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-total", children: total }),
2255
- " \u6761"
2256
- ] }),
2257
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", "data-testid": "pagination-controls", children: [
2258
- page > 1 && /* @__PURE__ */ jsxRuntime.jsx(
2259
- Button,
2260
- {
2261
- variant: "secondary",
2262
- size: "sm",
2263
- href: buildUrl(page - 1),
2264
- "hx-get": buildUrl(page - 1),
2265
- "data-testid": "pagination-prev",
2266
- "aria-label": "\u4E0A\u4E00\u9875",
2267
- children: "\u4E0A\u4E00\u9875"
2268
- }
2269
- ),
2270
- page < totalPages && /* @__PURE__ */ jsxRuntime.jsx(
2271
- Button,
2272
- {
2273
- variant: "secondary",
2274
- size: "sm",
2275
- href: buildUrl(page + 1),
2276
- "hx-get": buildUrl(page + 1),
2277
- "data-testid": "pagination-next",
2278
- "aria-label": "\u4E0B\u4E00\u9875",
2279
- children: "\u4E0B\u4E00\u9875"
2280
- }
2281
- )
2282
- ] })
2283
- ] });
2434
+ const buildCursorUrl = (pageState) => {
2435
+ const url = new URL(baseUrl, "http://localhost");
2436
+ for (const [key, value] of Object.entries(currentParams)) {
2437
+ if (value !== void 0 && value !== null && value !== "") {
2438
+ url.searchParams.set(key, String(value));
2439
+ }
2440
+ }
2441
+ url.searchParams.set("pageState", pageState);
2442
+ url.searchParams.delete("page");
2443
+ return url.pathname + url.search;
2444
+ };
2445
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2446
+ "nav",
2447
+ {
2448
+ className: "px-6 py-4 flex items-center justify-between border-t border-gray-200 bg-gray-50",
2449
+ "data-testid": "pagination",
2450
+ "aria-label": "\u5206\u9875\u5BFC\u822A",
2451
+ children: [
2452
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-700", "data-testid": "pagination-info", children: isCursorPagination ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2453
+ "\u672C\u9875",
2454
+ " ",
2455
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-count", children: itemCount ?? 0 }),
2456
+ " ",
2457
+ "\u6761"
2458
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2459
+ "\u663E\u793A",
2460
+ " ",
2461
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-start", children: start }),
2462
+ " ",
2463
+ "\u5230",
2464
+ " ",
2465
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-end", children: end }),
2466
+ " ",
2467
+ "\u6761\uFF0C\u5171",
2468
+ " ",
2469
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", "data-testid": "pagination-total", children: total }),
2470
+ " ",
2471
+ "\u6761"
2472
+ ] }) }),
2473
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", "data-testid": "pagination-controls", children: [
2474
+ !isCursorPagination && page && totalPages && page > 1 && /* @__PURE__ */ jsxRuntime.jsx(
2475
+ Button,
2476
+ {
2477
+ variant: "secondary",
2478
+ size: "sm",
2479
+ href: buildPageUrl(page - 1),
2480
+ "hx-get": buildPageUrl(page - 1),
2481
+ "data-testid": "pagination-prev",
2482
+ "aria-label": "\u4E0A\u4E00\u9875",
2483
+ children: "\u4E0A\u4E00\u9875"
2484
+ }
2485
+ ),
2486
+ !isCursorPagination && page && totalPages && page < totalPages && /* @__PURE__ */ jsxRuntime.jsx(
2487
+ Button,
2488
+ {
2489
+ variant: "secondary",
2490
+ size: "sm",
2491
+ href: buildPageUrl(page + 1),
2492
+ "hx-get": buildPageUrl(page + 1),
2493
+ "data-testid": "pagination-next",
2494
+ "aria-label": "\u4E0B\u4E00\u9875",
2495
+ children: "\u4E0B\u4E00\u9875"
2496
+ }
2497
+ ),
2498
+ isCursorPagination && nextPageState ? /* @__PURE__ */ jsxRuntime.jsx(
2499
+ Button,
2500
+ {
2501
+ variant: "secondary",
2502
+ size: "sm",
2503
+ href: buildCursorUrl(nextPageState),
2504
+ "hx-get": buildCursorUrl(nextPageState),
2505
+ "data-testid": "pagination-next",
2506
+ "aria-label": "\u4E0B\u4E00\u9875",
2507
+ children: "\u4E0B\u4E00\u9875"
2508
+ }
2509
+ ) : null
2510
+ ] })
2511
+ ]
2512
+ }
2513
+ );
2284
2514
  }
2285
2515
  var STYLES = {
2286
2516
  header: {
@@ -2574,6 +2804,7 @@ function ListPage(props) {
2574
2804
  pageSize: params.pageSize,
2575
2805
  sortBy: params.sortBy,
2576
2806
  sortOrder: params.sortOrder,
2807
+ pageState: params.pageState,
2577
2808
  ...params.filters
2578
2809
  // 包含所有筛选条件
2579
2810
  };
@@ -2596,7 +2827,7 @@ function ListPage(props) {
2596
2827
  ];
2597
2828
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
2598
2829
  filterFields && filterFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2599
- FilterForm,
2830
+ DynamicFilters,
2600
2831
  {
2601
2832
  fields: filterFields,
2602
2833
  listPath,
@@ -2618,14 +2849,16 @@ function ListPage(props) {
2618
2849
  method: action.method,
2619
2850
  target: action.target
2620
2851
  })) : void 0,
2621
- pagination: {
2852
+ pagination: result.totalPages !== void 0 || result.pageState !== void 0 ? {
2622
2853
  page: result.page,
2623
2854
  pageSize: result.pageSize,
2624
2855
  total: result.total,
2625
2856
  totalPages: result.totalPages,
2857
+ nextPageState: result.pageState,
2858
+ itemCount: result.items.length,
2626
2859
  baseUrl: listPath,
2627
2860
  currentParams
2628
- },
2861
+ } : void 0,
2629
2862
  tableActions,
2630
2863
  actionStyle: "link"
2631
2864
  }
@@ -2634,21 +2867,49 @@ function ListPage(props) {
2634
2867
  }
2635
2868
 
2636
2869
  // src/utils/params.ts
2637
- function parseListParams(ctx) {
2870
+ function parseListParams(ctx, filterSchema) {
2638
2871
  const url = new URL(ctx.req.url);
2639
- const systemParams = /* @__PURE__ */ new Set(["page", "pageSize", "sortBy", "sortOrder"]);
2872
+ const systemParams = /* @__PURE__ */ new Set([
2873
+ "page",
2874
+ "pageSize",
2875
+ "sortBy",
2876
+ "sortOrder",
2877
+ "pageState"
2878
+ ]);
2640
2879
  const filters = {};
2641
2880
  for (const [key, value] of url.searchParams.entries()) {
2642
- if (!systemParams.has(key) && value !== "") {
2881
+ if (!systemParams.has(key)) {
2643
2882
  filters[key] = value;
2644
2883
  }
2645
2884
  }
2885
+ let processedFilters = void 0;
2886
+ if (filterSchema && Object.keys(filters).length > 0) {
2887
+ const emptyStringFields = /* @__PURE__ */ new Set();
2888
+ for (const [key, value] of Object.entries(filters)) {
2889
+ if (value === "") {
2890
+ emptyStringFields.add(key);
2891
+ }
2892
+ }
2893
+ const preprocessed = preprocessFormData(filters, filterSchema);
2894
+ const cleanedFilters = {};
2895
+ for (const [key, value] of Object.entries(preprocessed)) {
2896
+ if (emptyStringFields.has(key)) {
2897
+ cleanedFilters[key] = "";
2898
+ } else if (value !== void 0 && value !== null) {
2899
+ cleanedFilters[key] = value;
2900
+ }
2901
+ }
2902
+ processedFilters = Object.keys(cleanedFilters).length > 0 ? cleanedFilters : void 0;
2903
+ } else if (Object.keys(filters).length > 0) {
2904
+ processedFilters = filters;
2905
+ }
2646
2906
  return {
2647
2907
  page: parseInt(url.searchParams.get("page") || "1", 10),
2648
2908
  pageSize: parseInt(url.searchParams.get("pageSize") || "10", 10),
2649
2909
  sortBy: url.searchParams.get("sortBy") || void 0,
2650
2910
  sortOrder: url.searchParams.get("sortOrder") || void 0,
2651
- filters: Object.keys(filters).length > 0 ? filters : void 0
2911
+ pageState: url.searchParams.get("pageState") || void 0,
2912
+ filters: processedFilters
2652
2913
  };
2653
2914
  }
2654
2915
  var DefaultListFeature = class extends BaseModelFeature {
@@ -2677,7 +2938,7 @@ var DefaultListFeature = class extends BaseModelFeature {
2677
2938
  const prefix = context.prefix || "";
2678
2939
  const basePath = `${prefix}/${model.modelName}`;
2679
2940
  const hasCreate = model.features.get("create") !== void 0;
2680
- const actions = [];
2941
+ const actions = [...this.options.actions ?? []];
2681
2942
  if (hasCreate) {
2682
2943
  const createMode = this.openMode?.create || "dialog";
2683
2944
  const createUrl = createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new`;
@@ -2699,8 +2960,10 @@ var DefaultListFeature = class extends BaseModelFeature {
2699
2960
  return actions;
2700
2961
  }
2701
2962
  async handler(context) {
2702
- const params = parseListParams(context.ctx);
2703
- const result = await this.getList(params);
2963
+ const params = this.filterSchema ? parseListParams(context.ctx, this.filterSchema) : parseListParams(context.ctx);
2964
+ const result = await this.getList(
2965
+ params
2966
+ );
2704
2967
  context.actions = this.getDefaultActions(context);
2705
2968
  const title = this.options?.title;
2706
2969
  if (typeof title === "function") {
@@ -5431,9 +5694,9 @@ exports.DefaultEditFeature = DefaultEditFeature;
5431
5694
  exports.DefaultListFeature = DefaultListFeature;
5432
5695
  exports.DetailFieldRenderer = DetailFieldRenderer;
5433
5696
  exports.Dialog = Dialog;
5697
+ exports.DynamicFilters = DynamicFilters;
5434
5698
  exports.EmptyState = EmptyState;
5435
5699
  exports.ErrorAlert = ErrorAlert;
5436
- exports.FilterForm = FilterForm;
5437
5700
  exports.FragmentFeature = FragmentFeature;
5438
5701
  exports.Header = Header;
5439
5702
  exports.HtmxAdminPlugin = HtmxAdminPlugin;