imean-service-engine-htmx-plugin 2.2.0 → 2.3.0

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.mjs CHANGED
@@ -3,6 +3,7 @@ import { promises } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { jsx, jsxs, Fragment } from 'hono/jsx/jsx-runtime';
5
5
  import { getCookie } from 'hono/cookie';
6
+ import { html } from 'hono/html';
6
7
 
7
8
  var __defProp = Object.defineProperty;
8
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1757,18 +1758,21 @@ function Button(props) {
1757
1758
  disabled ? "opacity-50 cursor-not-allowed" : "",
1758
1759
  className
1759
1760
  ].filter(Boolean).join(" ");
1761
+ const isNewWindow = rest.target === "_blank";
1760
1762
  const htmxAttrs = {};
1761
- if (hxGet) htmxAttrs["hx-get"] = hxGet;
1762
- if (hxPost) htmxAttrs["hx-post"] = hxPost;
1763
- if (hxPut) htmxAttrs["hx-put"] = hxPut;
1764
- if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
1765
- if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
1766
- if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
1767
- if (hxPushUrl !== void 0)
1768
- htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
1769
- if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
1770
- if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
1771
- if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
1763
+ if (!isNewWindow) {
1764
+ if (hxGet) htmxAttrs["hx-get"] = hxGet;
1765
+ if (hxPost) htmxAttrs["hx-post"] = hxPost;
1766
+ if (hxPut) htmxAttrs["hx-put"] = hxPut;
1767
+ if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
1768
+ if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
1769
+ if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
1770
+ if (hxPushUrl !== void 0)
1771
+ htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
1772
+ if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
1773
+ if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
1774
+ if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
1775
+ }
1772
1776
  const href = rest.href ?? hxGet ?? "#";
1773
1777
  const { className: _, ...otherRest } = rest;
1774
1778
  return /* @__PURE__ */ jsx(
@@ -1966,20 +1970,24 @@ function ActionLink(props) {
1966
1970
  const { action, item } = props;
1967
1971
  const hrefValue = action.href(item);
1968
1972
  const isDelete = action.method === "delete";
1973
+ const isNewWindow = action.target === "_blank";
1969
1974
  const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
1970
- return /* @__PURE__ */ jsx(
1971
- "a",
1972
- {
1973
- href: hrefValue,
1974
- className,
1975
- "hx-get": !isDelete ? hrefValue : void 0,
1976
- "hx-delete": isDelete ? hrefValue : void 0,
1977
- "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
1978
- "data-testid": `table-action-${action.label}`,
1979
- "aria-label": `${action.label}\uFF1A${item.id || item}`,
1980
- children: action.label
1981
- }
1982
- );
1975
+ const linkProps = {
1976
+ href: hrefValue,
1977
+ className,
1978
+ "data-testid": `table-action-${action.label}`,
1979
+ "aria-label": `${action.label}\uFF1A${item.id || item}`
1980
+ };
1981
+ if (isNewWindow) {
1982
+ linkProps.target = "_blank";
1983
+ linkProps.rel = "noopener noreferrer";
1984
+ } else if (isDelete) {
1985
+ linkProps["hx-delete"] = hrefValue;
1986
+ linkProps["hx-confirm"] = "\u786E\u5B9A\u5220\u9664\u5417\uFF1F";
1987
+ } else {
1988
+ linkProps["hx-get"] = hrefValue;
1989
+ }
1990
+ return /* @__PURE__ */ jsx("a", { ...linkProps, children: action.label });
1983
1991
  }
1984
1992
  function ActionCell(props) {
1985
1993
  const { actions, item, actionStyle } = props;
@@ -2000,15 +2008,18 @@ function ActionCell(props) {
2000
2008
  ) : /* @__PURE__ */ jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
2001
2009
  const hrefValue = action.href(item);
2002
2010
  const isDelete = action.method === "delete";
2011
+ const isNewWindow = action.target === "_blank";
2003
2012
  return /* @__PURE__ */ jsx(
2004
2013
  Button,
2005
2014
  {
2006
2015
  variant: isDelete ? "danger" : "secondary",
2007
2016
  size: "sm",
2008
2017
  href: hrefValue,
2009
- "hx-get": !isDelete ? hrefValue : void 0,
2018
+ "hx-get": !isDelete && !isNewWindow ? hrefValue : void 0,
2010
2019
  "hx-delete": isDelete ? hrefValue : void 0,
2011
2020
  "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
2021
+ target: isNewWindow ? "_blank" : void 0,
2022
+ rel: isNewWindow ? "noopener noreferrer" : void 0,
2012
2023
  className: action.class,
2013
2024
  "data-testid": `table-action-${action.label}`,
2014
2025
  "aria-label": `${action.label}\uFF1A${item.id || item}`,
@@ -2105,19 +2116,24 @@ function ListPage(props) {
2105
2116
  editPath,
2106
2117
  deletePath,
2107
2118
  listPath,
2108
- filterFields
2119
+ filterFields,
2120
+ openMode
2109
2121
  } = props;
2110
2122
  const actions = [];
2111
2123
  if (detailPath) {
2124
+ const mode = openMode?.detail || "dialog";
2112
2125
  actions.push({
2113
2126
  label: "\u67E5\u770B",
2114
- href: detailPath
2127
+ href: detailPath,
2128
+ target: mode === "newWindow" ? "_blank" : void 0
2115
2129
  });
2116
2130
  }
2117
2131
  if (editPath) {
2132
+ const mode = openMode?.edit || "dialog";
2118
2133
  actions.push({
2119
2134
  label: "\u7F16\u8F91",
2120
- href: editPath
2135
+ href: editPath,
2136
+ target: mode === "newWindow" ? "_blank" : void 0
2121
2137
  });
2122
2138
  }
2123
2139
  if (deletePath) {
@@ -2162,7 +2178,8 @@ function ListPage(props) {
2162
2178
  actions: actions.length > 0 ? actions.map((action) => ({
2163
2179
  label: action.label,
2164
2180
  href: (item) => action.href(item),
2165
- method: action.method
2181
+ method: action.method,
2182
+ target: action.target
2166
2183
  })) : void 0,
2167
2184
  pagination: {
2168
2185
  page: result.page,
@@ -2203,6 +2220,7 @@ var DefaultListFeature = class extends BaseFeature {
2203
2220
  listFieldNames;
2204
2221
  filterSchema;
2205
2222
  columnRenderers;
2223
+ openMode;
2206
2224
  constructor(options) {
2207
2225
  super({
2208
2226
  name: "list",
@@ -2218,6 +2236,7 @@ var DefaultListFeature = class extends BaseFeature {
2218
2236
  this.listFieldNames = options.listFieldNames;
2219
2237
  this.filterSchema = options.filterSchema;
2220
2238
  this.columnRenderers = options.columnRenderers;
2239
+ this.openMode = options.openMode;
2221
2240
  }
2222
2241
  getRoutes() {
2223
2242
  return [{ method: "get", path: "/list" }];
@@ -2238,9 +2257,12 @@ var DefaultListFeature = class extends BaseFeature {
2238
2257
  const prefix = context.prefix || "";
2239
2258
  const basePath = `${prefix}/${model.modelName}`;
2240
2259
  const listPath = `${basePath}/list`;
2241
- const createPath = model.features.get("create") ? `${basePath}/new?dialog=true` : void 0;
2242
- const detailPath = model.features.get("detail") ? (item) => `${basePath}/detail/${item.id}?dialog=true` : void 0;
2243
- const editPath = model.features.get("edit") ? (item) => `${basePath}/edit/${item.id}?dialog=true` : void 0;
2260
+ const createMode = this.openMode?.create || "dialog";
2261
+ const detailMode = this.openMode?.detail || "dialog";
2262
+ const editMode = this.openMode?.edit || "dialog";
2263
+ const createPath = model.features.get("create") ? createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new` : void 0;
2264
+ const detailPath = model.features.get("detail") ? (item) => detailMode === "dialog" ? `${basePath}/detail/${item.id}?dialog=true` : `${basePath}/detail/${item.id}` : void 0;
2265
+ const editPath = model.features.get("edit") ? (item) => editMode === "dialog" ? `${basePath}/edit/${item.id}?dialog=true` : `${basePath}/edit/${item.id}` : void 0;
2244
2266
  const deletePath = this.deleteItem && model.features.get("delete") ? (item) => `${basePath}/${item.id}` : void 0;
2245
2267
  return /* @__PURE__ */ jsx(
2246
2268
  ListPage,
@@ -2253,7 +2275,8 @@ var DefaultListFeature = class extends BaseFeature {
2253
2275
  editPath,
2254
2276
  deletePath,
2255
2277
  listPath,
2256
- filterFields
2278
+ filterFields,
2279
+ openMode: this.openMode
2257
2280
  }
2258
2281
  );
2259
2282
  }
@@ -2264,11 +2287,22 @@ var DefaultListFeature = class extends BaseFeature {
2264
2287
  const hasCreate = model.features.get("create") !== void 0;
2265
2288
  const actions = [];
2266
2289
  if (hasCreate) {
2267
- actions.push({
2268
- label: "\u65B0\u5EFA",
2269
- hxGet: `${basePath}/new?dialog=true`,
2270
- variant: "primary"
2271
- });
2290
+ const createMode = this.openMode?.create || "dialog";
2291
+ const createUrl = createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new`;
2292
+ if (createMode === "newWindow") {
2293
+ actions.push({
2294
+ label: "\u65B0\u5EFA",
2295
+ href: createUrl,
2296
+ variant: "primary",
2297
+ target: "_blank"
2298
+ });
2299
+ } else {
2300
+ actions.push({
2301
+ label: "\u65B0\u5EFA",
2302
+ hxGet: createUrl,
2303
+ variant: "primary"
2304
+ });
2305
+ }
2272
2306
  }
2273
2307
  return actions;
2274
2308
  }
@@ -3124,7 +3158,8 @@ function renderActionButton2(action, index) {
3124
3158
  submit,
3125
3159
  formId,
3126
3160
  onClick,
3127
- className = ""
3161
+ className = "",
3162
+ target
3128
3163
  } = action;
3129
3164
  if (submit && formId) {
3130
3165
  const variantStyles = {
@@ -3176,18 +3211,21 @@ function renderActionButton2(action, index) {
3176
3211
  } else if (label === "\u53D6\u6D88") {
3177
3212
  testId = "cancel-button";
3178
3213
  }
3214
+ const isNewWindow = target === "_blank";
3179
3215
  return /* @__PURE__ */ jsx(
3180
3216
  Button,
3181
3217
  {
3182
3218
  variant,
3183
3219
  href,
3184
- hxGet,
3185
- hxPost,
3186
- hxPut,
3187
- hxDelete,
3188
- hxConfirm: confirm,
3220
+ hxGet: !isNewWindow ? hxGet : void 0,
3221
+ hxPost: !isNewWindow ? hxPost : void 0,
3222
+ hxPut: !isNewWindow ? hxPut : void 0,
3223
+ hxDelete: !isNewWindow ? hxDelete : void 0,
3224
+ hxConfirm: !isNewWindow ? confirm : void 0,
3189
3225
  className,
3190
3226
  "data-testid": testId,
3227
+ target,
3228
+ rel: target === "_blank" ? "noopener noreferrer" : void 0,
3191
3229
  children: label
3192
3230
  },
3193
3231
  index
@@ -3979,80 +4017,433 @@ var HtmxAdminPlugin = class {
3979
4017
  registerHomeRedirect(this.pages, routeOptions);
3980
4018
  }
3981
4019
  };
3982
- function createFormFieldXData(options) {
3983
- const {
3984
- fieldName,
3985
- dataKey,
3986
- defaultValue = [],
3987
- customData = {},
3988
- customMethods = {}
3989
- } = options;
3990
- const dataEntries = [];
3991
- dataEntries.push(`${dataKey}: ${JSON.stringify(defaultValue)}`);
3992
- for (const [key, value] of Object.entries(customData)) {
3993
- dataEntries.push(`${key}: ${JSON.stringify(value)}`);
3994
- }
3995
- const methodEntries = [];
3996
- methodEntries.push(`init() {
3997
- const dataAttr = this.$el.getAttribute('data-initial-value');
3998
- if (dataAttr) {
3999
- try {
4000
- this.${dataKey} = JSON.parse(dataAttr);
4001
- } catch (e) {
4002
- console.error('Failed to parse initial value:', e);
4003
- this.${dataKey} = ${JSON.stringify(defaultValue)};
4020
+ function ObjectEditor(props) {
4021
+ const { value, fieldName, objectSchema } = props;
4022
+ if (!objectSchema) {
4023
+ return /* @__PURE__ */ jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
4024
+ }
4025
+ const fields = parseSchemaToFields(objectSchema);
4026
+ const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
4027
+ fields.forEach((field) => {
4028
+ if (!(field.name in initialObject)) {
4029
+ if (field.type === "number") {
4030
+ initialObject[field.name] = field.required ? 0 : void 0;
4031
+ } else if (field.type === "checkbox") {
4032
+ initialObject[field.name] = field.required ? false : void 0;
4033
+ } else {
4034
+ initialObject[field.name] = field.required ? "" : void 0;
4035
+ }
4036
+ }
4037
+ });
4038
+ const fieldNames = fields.map((f) => f.name);
4039
+ const fieldNamesJson = JSON.stringify(fieldNames);
4040
+ const initialValueJson = JSON.stringify(initialObject);
4041
+ const xDataContent = `{
4042
+ obj: {},
4043
+ init() {
4044
+ const dataAttr = this.$el.getAttribute('data-initial-value');
4045
+ if (dataAttr) {
4046
+ try {
4047
+ this.obj = JSON.parse(dataAttr);
4048
+ } catch (e) {
4049
+ console.error('Failed to parse initial value:', e);
4050
+ this.obj = {};
4051
+ }
4052
+ }
4053
+ const fieldNames = ${fieldNamesJson};
4054
+ fieldNames.forEach(fieldName => {
4055
+ if (!(fieldName in this.obj)) {
4056
+ this.obj[fieldName] = undefined;
4057
+ }
4058
+ });
4059
+ this.updateHiddenField();
4060
+ },
4061
+ updateHiddenField() {
4062
+ const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4063
+ if (hiddenInput) {
4064
+ hiddenInput.value = JSON.stringify(this.obj);
4004
4065
  }
4066
+ },
4067
+ updateField(fieldName, value, fieldType, required) {
4068
+ let convertedValue = value;
4069
+ if (fieldType === 'number') {
4070
+ convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
4071
+ if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
4072
+ } else if (fieldType === 'checkbox') {
4073
+ convertedValue = value === 'true' || value === true || value === '1' || value === 1;
4074
+ } else {
4075
+ convertedValue = value || (required ? '' : undefined);
4076
+ }
4077
+ if (convertedValue === undefined && !required) {
4078
+ delete this.obj[fieldName];
4079
+ } else {
4080
+ this.obj[fieldName] = convertedValue;
4081
+ }
4082
+ this.updateHiddenField();
4005
4083
  }
4006
- this.updateHiddenField();
4007
- }`);
4008
- methodEntries.push(`updateHiddenField() {
4009
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4010
- if (hiddenInput) {
4011
- hiddenInput.value = JSON.stringify(this.${dataKey});
4084
+ }`;
4085
+ const generateField = (field) => {
4086
+ const fieldId = `${fieldName}-${field.name}`;
4087
+ const fieldNameVar = `obj.${field.name}`;
4088
+ const requiredValue = field.required ? "true" : "false";
4089
+ const fieldNameForJs = JSON.stringify(field.name);
4090
+ let inputElement;
4091
+ if (field.type === "text") {
4092
+ inputElement = html`
4093
+ <input
4094
+ type="text"
4095
+ id="${fieldId}"
4096
+ x-bind:value="${fieldNameVar} || ''"
4097
+ x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4098
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4099
+ data-testid="${fieldName}-input-${field.name}"
4100
+ ${field.required ? "required" : ""}
4101
+ />
4102
+ `;
4103
+ } else if (field.type === "textarea") {
4104
+ inputElement = html`
4105
+ <textarea
4106
+ id="${fieldId}"
4107
+ x-bind:value="${fieldNameVar} || ''"
4108
+ x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4109
+ rows="4"
4110
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
4111
+ data-testid="${fieldName}-input-${field.name}"
4112
+ ${field.required ? "required" : ""}
4113
+ ></textarea>
4114
+ `;
4115
+ } else if (field.type === "number") {
4116
+ const step = field.step || (field.step === void 0 ? "1" : "any");
4117
+ inputElement = html`
4118
+ <input
4119
+ type="number"
4120
+ id="${fieldId}"
4121
+ x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
4122
+ x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
4123
+ step="${step}"
4124
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4125
+ data-testid="${fieldName}-input-${field.name}"
4126
+ ${field.required ? "required" : ""}
4127
+ />
4128
+ `;
4129
+ } else if (field.type === "date") {
4130
+ inputElement = html`
4131
+ <input
4132
+ type="date"
4133
+ id="${fieldId}"
4134
+ x-bind:value="${fieldNameVar} || ''"
4135
+ x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
4136
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4137
+ data-testid="${fieldName}-input-${field.name}"
4138
+ ${field.required ? "required" : ""}
4139
+ />
4140
+ `;
4141
+ } else if (field.type === "email") {
4142
+ inputElement = html`
4143
+ <input
4144
+ type="email"
4145
+ id="${fieldId}"
4146
+ x-bind:value="${fieldNameVar} || ''"
4147
+ x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4148
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4149
+ data-testid="${fieldName}-input-${field.name}"
4150
+ ${field.required ? "required" : ""}
4151
+ />
4152
+ `;
4153
+ } else if (field.type === "select" && field.options) {
4154
+ inputElement = html`
4155
+ <select
4156
+ id="${fieldId}"
4157
+ x-bind:value="${fieldNameVar} || ''"
4158
+ x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4159
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
4160
+ data-testid="${fieldName}-select-${field.name}"
4161
+ ${field.required ? "required" : ""}
4162
+ >
4163
+ ${!field.required ? html`<option value="">请选择</option>` : ""}
4164
+ ${field.options.map(
4165
+ (option) => html`
4166
+ <option value="${String(option.value)}">${option.label}</option>
4167
+ `
4168
+ )}
4169
+ </select>
4170
+ `;
4171
+ } else if (field.type === "checkbox") {
4172
+ inputElement = html`
4173
+ <div class="flex items-center">
4174
+ <input
4175
+ type="checkbox"
4176
+ id="${fieldId}"
4177
+ x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
4178
+ x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
4179
+ class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
4180
+ data-testid="${fieldName}-checkbox-${field.name}"
4181
+ />
4182
+ <label for="${fieldId}" class="ml-2 text-sm text-gray-700">
4183
+ ${field.label}
4184
+ </label>
4185
+ </div>
4186
+ `;
4012
4187
  }
4013
- }`);
4014
- for (const [methodName, methodBody] of Object.entries(customMethods)) {
4015
- methodEntries.push(`${methodName}${methodBody}`);
4016
- }
4017
- const dataStr = dataEntries.join(",\n ");
4018
- const methodsStr = methodEntries.join(",\n ");
4019
- return `{
4020
- ${dataStr},
4021
- ${methodsStr}
4022
- }`;
4188
+ return html`
4189
+ <div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
4190
+ ${field.type !== "checkbox" ? html`
4191
+ <label
4192
+ for="${fieldId}"
4193
+ class="block text-sm font-semibold text-gray-700"
4194
+ data-testid="${fieldName}-label-${field.name}"
4195
+ >
4196
+ ${field.label}
4197
+ ${field.required ? html`<span class="text-red-500 ml-1">*</span>` : ""}
4198
+ </label>
4199
+ ` : ""}
4200
+ ${inputElement}
4201
+ </div>
4202
+ `;
4203
+ };
4204
+ return html`
4205
+ <div
4206
+ x-data="${xDataContent}"
4207
+ data-initial-value="${initialValueJson}"
4208
+ x-init="init()"
4209
+ class="space-y-4"
4210
+ >
4211
+ <input
4212
+ type="hidden"
4213
+ name="${fieldName}"
4214
+ value=""
4215
+ data-testid="hidden-${fieldName}"
4216
+ />
4217
+ <div class="space-y-4">
4218
+ ${fields.map((field) => generateField(field))}
4219
+ </div>
4220
+ </div>
4221
+ `;
4023
4222
  }
4024
- function FormFieldWrapper(props) {
4223
+ function StringArrayEditor(props) {
4025
4224
  const {
4225
+ value,
4026
4226
  fieldName,
4027
- initialValue,
4028
- xData,
4029
- autoSync = false,
4030
- children,
4031
- className = "space-y-4"
4227
+ placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
4228
+ allowEmpty = false
4032
4229
  } = props;
4033
- const initialValueJson = JSON.stringify(initialValue);
4034
- return /* @__PURE__ */ jsxs(
4035
- "div",
4036
- {
4037
- "x-data": xData,
4038
- "data-initial-value": initialValueJson,
4039
- "x-init": "init()",
4040
- ...autoSync ? { "x-effect": "updateHiddenField()" } : {},
4041
- className,
4042
- children: [
4043
- /* @__PURE__ */ jsx(
4044
- "input",
4045
- {
4046
- type: "hidden",
4047
- name: fieldName,
4048
- value: "",
4049
- "data-testid": `hidden-${fieldName}`
4230
+ const initialItems = value || [];
4231
+ const initialValueJson = JSON.stringify(initialItems);
4232
+ const xDataContent = `{
4233
+ items: ${initialValueJson},
4234
+ draggedIndex: null,
4235
+ draggedOverIndex: null,
4236
+ fieldName: ${JSON.stringify(fieldName)},
4237
+ placeholder: ${JSON.stringify(placeholder)},
4238
+ allowEmpty: ${allowEmpty},
4239
+ init() {
4240
+ const dataAttr = this.$el.getAttribute('data-initial-value');
4241
+ if (dataAttr) {
4242
+ try {
4243
+ const parsed = JSON.parse(dataAttr);
4244
+ if (Array.isArray(parsed)) {
4245
+ this.items = parsed;
4246
+ } else {
4247
+ this.items = [];
4050
4248
  }
4051
- ),
4052
- children
4053
- ]
4249
+ } catch (e) {
4250
+ console.error('Failed to parse initial value:', e);
4251
+ this.items = [];
4252
+ }
4253
+ }
4254
+ this.updateHiddenField();
4255
+ },
4256
+ updateHiddenField() {
4257
+ const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4258
+ if (hiddenInput) {
4259
+ hiddenInput.value = JSON.stringify(this.items);
4260
+ }
4261
+ },
4262
+ addItem() {
4263
+ this.items.push('');
4264
+ this.updateHiddenField();
4265
+ this.$nextTick(() => {
4266
+ const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
4267
+ if (keyInputs.length > 0) {
4268
+ const lastInput = keyInputs[keyInputs.length - 1];
4269
+ if (lastInput && lastInput.focus) {
4270
+ lastInput.focus();
4271
+ }
4272
+ }
4273
+ });
4274
+ },
4275
+ removeItem(index) {
4276
+ this.items.splice(index, 1);
4277
+ this.updateHiddenField();
4278
+ },
4279
+ updateItem(index, value) {
4280
+ this.items[index] = value;
4281
+ this.updateHiddenField();
4282
+ },
4283
+ handleDragStart(index, event) {
4284
+ this.draggedIndex = index;
4285
+ event.dataTransfer.effectAllowed = 'move';
4286
+ event.dataTransfer.setData('text/plain', index.toString());
4287
+ const target = event.currentTarget || event.target.closest('[draggable="true"]');
4288
+ if (target) {
4289
+ target.style.opacity = '0.5';
4290
+ }
4291
+ },
4292
+ handleDragEnd(event) {
4293
+ const target = event.currentTarget || event.target.closest('[draggable="true"]');
4294
+ if (target) {
4295
+ target.style.opacity = '';
4296
+ }
4297
+ this.draggedIndex = null;
4298
+ this.draggedOverIndex = null;
4299
+ },
4300
+ handleDragOver(index, event) {
4301
+ event.preventDefault();
4302
+ event.dataTransfer.dropEffect = 'move';
4303
+ this.draggedOverIndex = index;
4304
+ },
4305
+ handleDragLeave() {
4306
+ this.draggedOverIndex = null;
4307
+ },
4308
+ handleDrop(index, event) {
4309
+ event.preventDefault();
4310
+ if (this.draggedIndex !== null && this.draggedIndex !== index) {
4311
+ const draggedItem = this.items[this.draggedIndex];
4312
+ this.items.splice(this.draggedIndex, 1);
4313
+ this.items.splice(index, 0, draggedItem);
4314
+ this.updateHiddenField();
4315
+ }
4316
+ this.draggedIndex = null;
4317
+ this.draggedOverIndex = null;
4054
4318
  }
4055
- );
4319
+ }`;
4320
+ return html`
4321
+ <div
4322
+ x-data="${xDataContent}"
4323
+ data-initial-value="${initialValueJson}"
4324
+ x-init="init()"
4325
+ class="space-y-3"
4326
+ >
4327
+ <input
4328
+ type="hidden"
4329
+ name="${fieldName}"
4330
+ value=""
4331
+ data-testid="hidden-${fieldName}"
4332
+ />
4333
+ <div class="space-y-3">
4334
+ <!-- 头部:显示数量和添加按钮 -->
4335
+ <div class="flex items-center justify-between">
4336
+ <span class="text-sm text-gray-600">
4337
+ 共 <span x-text="items.length">0</span> 项
4338
+ </span>
4339
+ <button
4340
+ type="button"
4341
+ x-on:click="addItem()"
4342
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
4343
+ data-testid="${fieldName}-add-button"
4344
+ >
4345
+ <svg
4346
+ class="w-4 h-4"
4347
+ fill="none"
4348
+ stroke="currentColor"
4349
+ viewBox="0 0 24 24"
4350
+ >
4351
+ <path
4352
+ stroke-linecap="round"
4353
+ stroke-linejoin="round"
4354
+ stroke-width="2"
4355
+ d="M12 4v16m8-8H4"
4356
+ />
4357
+ </svg>
4358
+ 添加项
4359
+ </button>
4360
+ </div>
4361
+
4362
+ <!-- 列表项 -->
4363
+ <div class="space-y-2" x-show="items.length > 0">
4364
+ <template x-for="(item, index) in items" x-bind:key="index">
4365
+ <div
4366
+ class="flex items-center gap-2 group"
4367
+ x-bind:class="{
4368
+ 'opacity-50': draggedIndex === index,
4369
+ 'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
4370
+ }"
4371
+ draggable="true"
4372
+ x-on:dragstart="handleDragStart(index, $event)"
4373
+ x-on:dragend="handleDragEnd($event)"
4374
+ x-on:dragover="handleDragOver(index, $event)"
4375
+ x-on:dragleave="handleDragLeave()"
4376
+ x-on:drop="handleDrop(index, $event)"
4377
+ x-bind:data-testid="fieldName + '-item-' + index"
4378
+ >
4379
+ <!-- 拖拽手柄 -->
4380
+ <div
4381
+ class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
4382
+ x-bind:data-testid="fieldName + '-drag-handle-' + index"
4383
+ title="拖拽排序"
4384
+ >
4385
+ <svg
4386
+ class="w-5 h-5"
4387
+ fill="none"
4388
+ stroke="currentColor"
4389
+ viewBox="0 0 24 24"
4390
+ >
4391
+ <path
4392
+ stroke-linecap="round"
4393
+ stroke-linejoin="round"
4394
+ stroke-width="2"
4395
+ d="M4 8h16M4 16h16"
4396
+ />
4397
+ </svg>
4398
+ </div>
4399
+
4400
+ <!-- 输入框 -->
4401
+ <input
4402
+ type="text"
4403
+ x-bind:value="items[index] || ''"
4404
+ x-on:input="updateItem(index, $event.target.value)"
4405
+ x-bind:placeholder="placeholder + ' ' + (index + 1)"
4406
+ class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4407
+ x-bind:data-testid="fieldName + '-input-' + index"
4408
+ x-bind:required="!allowEmpty"
4409
+ />
4410
+
4411
+ <!-- 删除按钮 -->
4412
+ <button
4413
+ type="button"
4414
+ x-on:click="removeItem(index)"
4415
+ class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
4416
+ x-bind:data-testid="fieldName + '-remove-button-' + index"
4417
+ title="删除此项"
4418
+ >
4419
+ <svg
4420
+ class="w-5 h-5"
4421
+ fill="none"
4422
+ stroke="currentColor"
4423
+ viewBox="0 0 24 24"
4424
+ >
4425
+ <path
4426
+ stroke-linecap="round"
4427
+ stroke-linejoin="round"
4428
+ stroke-width="2"
4429
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
4430
+ />
4431
+ </svg>
4432
+ </button>
4433
+ </div>
4434
+ </template>
4435
+ </div>
4436
+
4437
+ <!-- 空状态提示 -->
4438
+ <div
4439
+ x-show="items.length === 0"
4440
+ class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
4441
+ >
4442
+ 暂无项,点击"添加项"按钮添加
4443
+ </div>
4444
+ </div>
4445
+ </div>
4446
+ `;
4056
4447
  }
4057
4448
 
4058
- export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, FormFieldWrapper, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, createFormFieldXData, getUserInfo, modelNameToPath, parseListParams };
4449
+ export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, ObjectEditor, PageModel, StringArrayEditor, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };