diff --git a/packages/ui/src/components/ui/form-controls.test.ts b/packages/ui/src/components/ui/form-controls.test.ts
index f8a7681..ea3b6f4 100644
--- a/packages/ui/src/components/ui/form-controls.test.ts
+++ b/packages/ui/src/components/ui/form-controls.test.ts
@@ -40,6 +40,33 @@ describe("composed form controls", () => {
expect(html).toContain('aria-invalid="true"');
expect(html).toContain('aria-required="true"');
expect(html).toContain('role="alert"');
+ expect(html.indexOf("账号")).toBeLessThan(html.indexOf(" {
+ const html = renderToStaticMarkup(
+ React.createElement(
+ FormField,
+ {
+ id: "exclude-regex",
+ label: "排除正则",
+ description: "每行一条,按节点导入时的原始名称匹配。",
+ descriptionPlacement: "before-control",
+ },
+ React.createElement(Input)
+ )
+ );
+ const labelIndex = html.indexOf("排除正则");
+ const descriptionIndex = html.indexOf(
+ "每行一条,按节点导入时的原始名称匹配。"
+ );
+ const controlIndex = html.indexOf(" {
diff --git a/packages/ui/src/components/ui/form-field.tsx b/packages/ui/src/components/ui/form-field.tsx
index 9cfc3b9..96f458a 100644
--- a/packages/ui/src/components/ui/form-field.tsx
+++ b/packages/ui/src/components/ui/form-field.tsx
@@ -15,6 +15,7 @@ export interface FormFieldProps {
id?: string;
label: React.ReactNode;
description?: React.ReactNode;
+ descriptionPlacement?: "before-control" | "after-control";
error?: React.ReactNode;
required?: boolean;
className?: string;
@@ -29,6 +30,7 @@ function FormField({
id,
label,
description,
+ descriptionPlacement = "after-control",
error,
required = false,
className,
@@ -51,19 +53,30 @@ function FormField({
"aria-invalid": error ? true : controlChild.props["aria-invalid"],
"aria-required": required || controlChild.props["aria-required"] || undefined,
});
+ const labelElement = (
+
+ );
+ const descriptionElement = description ? (
+
-
+ {descriptionPlacement === "before-control" && descriptionElement ? (
+
+ {labelElement}
+ {descriptionElement}
+
+ ) : (
+ labelElement
+ )}
{control}
- {description ? (
-
- {description}
-
- ) : null}
+ {descriptionPlacement === "after-control" ? descriptionElement : null}
{error ? (
{error}
diff --git a/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.test.ts
index 8912c76..5d25b11 100644
--- a/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.test.ts
+++ b/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.test.ts
@@ -6,6 +6,7 @@ import type { ParsedNode } from "@subboost/core/types/node";
const mocks = vi.hoisted(() => ({
buttons: [] as Array>,
dialog: null as Record | null,
+ formField: null as Record | null,
switchField: null as Record | null,
textarea: null as Record | null,
}));
@@ -28,15 +29,23 @@ vi.mock("@subboost/ui/components/ui/dialog", () => ({
DialogTitle: (props: any) => React.createElement("h2", null, props.children),
}));
vi.mock("@subboost/ui/components/ui/form-field", () => ({
- FormField: (props: any) =>
- React.createElement(
+ FormField: (props: any) => {
+ mocks.formField = props;
+ const label = React.createElement("label", null, props.label);
+ const description = props.description
+ ? React.createElement("p", null, props.description)
+ : null;
+ return React.createElement(
"div",
null,
- React.createElement("label", null, props.label),
+ props.descriptionPlacement === "before-control"
+ ? React.createElement("div", null, label, description)
+ : label,
props.children,
- props.description ? React.createElement("p", null, props.description) : null,
+ props.descriptionPlacement === "before-control" ? null : description,
props.error ? React.createElement("p", { role: "alert" }, props.error) : null
- ),
+ );
+ },
}));
vi.mock("@subboost/ui/components/ui/switch-field", () => ({
SwitchField: (props: any) => {
@@ -111,22 +120,36 @@ describe("NodeManagementAutoProcessingDialog", () => {
beforeEach(() => {
mocks.buttons = [];
mocks.dialog = null;
+ mocks.formField = null;
mocks.switchField = null;
mocks.textarea = null;
});
it("renders the compact wording, counts, and matching display/origin names", () => {
const { html } = renderDialog();
+ const helperText =
+ "每行一条,按节点导入时的原始名称匹配;命中节点会在生成配置时全局排除,关闭后恢复。";
expect(html).toContain("自动处理");
expect(html).toContain("启用");
expect(html).toContain("排除正则");
- expect(html).toContain("每行一条,匹配导入名称。");
+ expect(html).toContain(helperText);
expect(html).toContain("剩余流量|套餐到期|注意事项");
+ expect(html.indexOf("排除正则")).toBeLessThan(html.indexOf(helperText));
+ expect(html.indexOf(helperText)).toBeLessThan(
+ html.indexOf("剩余流量|套餐到期|注意事项")
+ );
expect(html).toContain("导入 2 · 排除 1 · 保留 1");
expect(html).toContain("[HK] Alpha");
expect(html).toContain("原名:Alpha");
expect(html).not.toContain("原名:Beta");
+ expect(mocks.formField).toEqual(
+ expect.objectContaining({
+ description: helperText,
+ descriptionPlacement: "before-control",
+ label: "排除正则",
+ })
+ );
expect(mocks.switchField).toEqual(
expect.objectContaining({ checked: true, label: "启用" })
);
diff --git a/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.tsx b/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.tsx
index 6842b1f..6334a34 100644
--- a/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.tsx
+++ b/packages/ui/src/product/converter/advanced-mode/sections/node-management/auto-processing-dialog.tsx
@@ -118,7 +118,8 @@ export function NodeManagementAutoProcessingDialog({