Skip to content
On this page

UI构建规范

本文档用于指导 AI、代码生成器或脚本生成器创建可被前端运行页识别的动态 UI。

目标:

  • 生成合法的 Lua UI 构建代码。
  • 保持旧 UI 兼容。
  • 正确使用布局组件。
  • 正确设置 name / key,保证参数可以被 UICurrentValueonUIChanged(values) 读取。

基础规则

动态 UI 最终由一个组件数组构成:

lua
local elements = {
    ui.createInput("name", "名称", "请输入名称", ""),
    ui.createSlider("speed", 50, "速度", 0, 100, 1)
}

ui.buildUI(elements, 1)
StartUI()

ui.buildUI(elements, updateMode) 的第二个参数:

  • 0:强制使用代码里的默认值覆盖当前 UI。
  • 1:更新 UI 结构,同时尽量保留用户已经修改过的值。推荐使用。

前端点击“保存配置”会写入 ui.json。正在运行的 Lua 进程不会因为保存按钮重新执行初始化代码;验证保存后的默认值时,需要停止脚本后重新运行。


节点结构

每个动态 UI 节点必须是“单组件键对象”:

lua
{
    Switch = {
        name = "enableFeature",
        title = "启用功能",
        isSelected = true
    }
}

对应 JSON 结构:

json
{
  "Switch": {
    "name": "enableFeature",
    "title": "启用功能",
    "isSelected": true
  }
}

AI 生成时必须遵守:

  • 一个节点只放一个组件类型。
  • 输入型组件必须设置取值名。
  • 容器/布局/展示组件不要设置业务取值名。
  • 制作任何动态 UI 时必须考虑大小屏幕适配。这是硬性要求,不是可选优化。
  • 默认按小屏优先生成布局,避免窄弹窗或侧栏中溢出。
  • 短输入组件推荐使用 layout = { span = 6, lg = 3 },实现小屏最多 2 列、大屏最多 4 列。
  • 长组件、说明文字、图片、Alert、Slider 和复杂分组默认使用 layout = { span = 12 }
  • 不要生成事件函数字符串,例如 onClick = "xxx"

支持组件

组件类型是否产生值取值字段
Input输入defaultValue
NumberInput输入defaultValue
Slider输入defaultValue
Select输入defaultSelectedKeys
Autocomplete输入defaultSelectedKey
Checkbox输入defaultSelected
Switch输入isSelected
CheckboxGroup输入defaultValue
RadioGroup输入defaultValue
Alert展示-
Chip展示-
Divider展示-
Image展示-
P展示-
Div布局-
Group布局-
Tabs容器-
Accordion容器-

取值规则

输入组件的取值名来自:

  • ui.createXxx(key, ...) 的第一个参数。
  • 手写节点中的 name 字段。

示例:

lua
local input = ui.createInput("username", "用户名", "请输入", "")

local enable = {
    Switch = {
        name = "enableFeature",
        title = "启用功能",
        isSelected = true
    }
}

取值:

lua
function onUIChanged(values)
    if values.username ~= nil then
        print(values.username)
    end

    if values.enableFeature ~= nil then
        print(values.enableFeature)
    end
end

function readCurrent()
    print(UICurrentValue.username)
    print(UICurrentValue.enableFeature)
end

AI 必须保证:

  • 所有需要取值的组件都有唯一的 key / name
  • 不同输入组件不要重复使用同一个 key / name
  • DivGroupPTabsAccordion 自身不作为业务参数读取;它们内部的输入组件仍按自己的 key / name 取值。
  • onUIChanged(values) 收到的是当前 UI 的完整参数表,可以读取任意输入组件当前值。
  • 回调外、主循环或其它函数中主动读取当前参数时,使用 UICurrentValue.xxx

各组件返回值类型

组件示例取值返回值
Inputvalues.username字符串
NumberInputvalues.count数字
Slider 单值values.speed数字
Slider 范围values.range数字数组
Select 单选values.mode字符串
Select 多选values.multiMode逗号分隔字符串
Autocompletevalues.project字符串或 nil
Checkboxvalues.enableLog布尔值
Switchvalues.enableFeature布尔值
CheckboxGroupvalues.fruits字符串数组
RadioGroupvalues.priority字符串

布局规则

旧 UI 默认垂直排列。不要为了旧 UI 主动添加布局字段。

需要横向或栅格布局时使用:

lua
component.ComponentName.layout = { span = 6 }

常用 span

  • 12:占满一行。
  • 6:半行,两个组件一行。
  • 4:三分之一行,三个组件一行。
  • 3:四分之一行,四个组件一行。

响应式宽度:

  • span:默认宽度,也是小屏宽度。
  • sm:容器宽度大于等于 480px 时使用。
  • md:容器宽度大于等于 768px 时使用。
  • lg:容器宽度大于等于 1024px 时使用。
  • xl:容器宽度大于等于 1280px 时使用。

推荐写法:小屏最多 2 列,大屏最多 4 列。

lua
nameInput.Input.layout = {
    span = 6, -- 小屏:半行,一行最多 2 个
    lg = 3,   -- 大屏:四分之一行,一行最多 4 个
}

长输入、说明文字、Slider 通常保持整行:

lua
speedSlider.Slider.layout = { span = 12 }
description.P.layout = { span = 12 }

示例:

lua
local nameInput = ui.createInput("name", "名称", "请输入", "")
nameInput.Input.layout = { span = 6 }

local countInput = ui.createNumberInput("count", 1, "数量", 0, 100, 1, "1~100")
countInput.NumberInput.layout = { span = 6 }

条件显示和禁用

输入组件和布局组件可以使用条件字段,让一个组件根据另一个组件的值改变状态。

支持字段:

  • visibleWhen:条件满足时显示,否则隐藏。
  • hiddenWhen:条件满足时隐藏。
  • disabledWhen:条件满足时禁用。用于 InputNumberInputSelectAutocompleteSliderCheckboxSwitchCheckboxGroupRadioGroup 等输入组件。

条件写法:

lua
component.Input.visibleWhen = {
    field = "enableAdvanced",
    equals = true,
}

可用判断:

  • equals / value:等于指定值。
  • notEquals:不等于指定值。
  • in:当前值在数组内。
  • notIn:当前值不在数组内。
  • truthy = true:值为真。
  • falsy = true:值为空、false 或 nil。
  • exists = true:字段存在且不为 nil。

示例:开关控制高级参数禁用。

lua
local enableAdvanced = {
    Switch = {
        name = "enableAdvanced",
        title = "启用高级设置",
        isSelected = false,
        layout = { span = 6, lg = 3 },
    }
}

local threshold = ui.createSlider("threshold", 75, "识别阈值", 0, 100, 1)
threshold.Slider.layout = { span = 12, lg = 6 }
threshold.Slider.disabledWhen = {
    field = "enableAdvanced",
    equals = false,
}

示例:调试模式开启后才显示备注输入框。

lua
local debugText = ui.createInput("debugText", "调试备注", "请输入备注", "")
debugText.Input.layout = { span = 12 }
debugText.Input.visibleWhen = {
    field = "debugMode",
    equals = true,
}

示例:下拉框选择不同模式时显示不同配置区。

lua
local modeItems = {
    { key = "normal", label = "普通模式" },
    { key = "advanced", label = "高级模式" },
    { key = "custom", label = "自定义模式" },
}

local modeSelect = ui.createSelect("mode", "normal", "运行模式", modeItems)
modeSelect.Select.layout = { span = 6, lg = 3 }

local advancedGroup = {
    Group = {
        title = "高级参数",
        description = "只有运行模式为 advanced 时显示。",
        direction = "grid",
        columns = 12,
        gap = 4,
        visibleWhen = {
            field = "mode",
            equals = "advanced",
        },
        item = {
            { ui.createSlider("advancedSpeed", 50, "高级速度", 0, 100, 1) },
        },
        layout = { span = 12 },
    }
}

local customInput = ui.createInput("customValue", "自定义参数", "请输入", "")
customInput.Input.layout = { span = 12 }
customInput.Input.visibleWhen = {
    field = "mode",
    equals = "custom",
}

示例:勾选某个多选项后显示提示。

lua
local featureItems = {
    { key = "log", label = "日志" },
    { key = "notify", label = "通知" },
    { key = "safe", label = "安全模式" },
}

local features = ui.createCheckboxGroup("features", {"log"}, "功能选项", featureItems)
features.CheckboxGroup.orientation = "horizontal"
features.CheckboxGroup.layout = { span = 12 }

local safeTip = {
    Alert = {
        title = "安全模式已启用",
        description = "启用后部分高风险参数会被限制。",
        color = "warning",
        visibleWhen = {
            field = "features",
            in = {"safe"},
        },
        layout = { span = 12 },
    }
}

示例:开启锁定后禁用整个分组。

lua
local lockSwitch = {
    Switch = {
        name = "lockConfig",
        title = "锁定配置",
        isSelected = false,
        layout = { span = 6, lg = 3 },
    }
}

local configGroup = {
    Group = {
        title = "可编辑配置",
        direction = "grid",
        columns = 12,
        gap = 4,
        disabledWhen = {
            field = "lockConfig",
            equals = true,
        },
        item = {
            { ui.createInput("name", "名称", "请输入", "") },
            { ui.createNumberInput("count", 1, "数量", 0, 100, 1, "1~100") },
        },
        layout = { span = 12 },
    }
}

示例:值不存在时显示初始化提示,存在后隐藏。

lua
local initTip = {
    Alert = {
        title = "请先选择项目",
        description = "选择项目后会显示后续配置。",
        color = "primary",
        visibleWhen = {
            field = "project",
            falsy = true,
        },
        layout = { span = 12 },
    }
}

local projectConfig = {
    Group = {
        title = "项目配置",
        visibleWhen = {
            field = "project",
            exists = true,
        },
        item = {
            { ui.createInput("projectName", "项目名称", "请输入", "") },
        },
        layout = { span = 12 },
    }
}

注意:隐藏或禁用只影响界面显示和交互,不要把 DivGroupP 当作业务参数读取。真正取值仍然读取内部输入组件自己的 name


Group 布局

Group 是纯布局组件,不产生值。

推荐用于明确控制一组组件的排列方式。

lua
local group = {
    Group = {
        title = "基础参数",
        description = "这里的 Group 只负责布局,不参与取值。",
        direction = "grid",
        columns = 12,
        gap = 4,
        item = {
            { nameInput, countInput }
        }
    }
}

direction 可选值:

  • "vertical":从上到下排列。
  • "horizontal":横向排列,空间不足自动换行。
  • "grid":栅格布局,配合 layout.span

AI 推荐:

  • 表单区域使用 direction = "grid"
  • 开关、标签、短选项使用 direction = "horizontal"
  • 普通说明区使用 direction = "vertical"

Div 和 P

Div 是通用布局容器,不产生值。

lua
local card = {
    Div = {
        title = "配置区域",
        description = "这个容器只负责外观。",
        className = "rounded-small border border-default-200 p-4",
        item = {
            { nameInput },
            { countInput }
        },
        layout = { span = 12 }
    }
}

P 是段落文本,不产生值。

lua
local tip = {
    P = {
        text = "请先填写参数,再点击运行。",
        className = "text-sm text-default-500",
        layout = { span = 12 }
    }
}

常用 className

  • "text-sm text-default-500":普通说明文字。
  • "text-sm text-warning font-medium":警告文字。
  • "rounded-small border border-default-200 p-4":普通卡片。
  • "rounded-small bg-default-50 p-4":浅色区域。

Tabs 和 Accordion

TabsAccordion 是容器组件,不产生业务值。

内部输入组件正常取值。

lua
local tabs = ui.createTabs({
    ui.createTab("基础设置", {
        { nameInput, countInput }
    }),
    ui.createTab("高级设置", {
        { speedSlider }
    })
}, "top", true)

左侧导航式 Tabs:

lua
local tabs = ui.createTabs({
    ui.createTab("总览", {
        { ui.createAlert("提示", "总览内容", "info") }
    }),
    ui.createTab("参数", {
        { nameInput, countInput }
    })
}, "start", false)

tabs.Tabs.variant = "solid"
tabs.Tabs.color = "primary"

Accordion:

lua
local panel = ui.createAccordion({
    ui.createTab("基础设置", {
        { nameInput }
    }),
    ui.createTab("高级设置", {
        { speedSlider }
    })
})

panel.Accordion.variant = "splitted"
panel.Accordion.selectionMode = "multiple"
panel.Accordion.defaultSelectedKeys = {"基础设置"}

选项结构

SelectAutocompleteCheckboxGroupRadioGroup 使用相同选项格式:

lua
local items = {
    { label = "自动", key = "auto", description = "按默认策略运行" },
    { label = "手动", key = "manual" },
    { label = "调试", key = "debug", isDisabled = true },
}

字段说明:

  • label:显示文字。
  • key:实际取值。
  • description:说明文字,可选。
  • isDisabled:是否禁用,可选。

推荐完整模板

lua
local ui = require("ui_builder")

local modeItems = {
    { label = "自动", key = "auto", description = "按默认策略运行" },
    { label = "手动", key = "manual" },
}

local nameInput = ui.createInput("name", "名称", "请输入名称", "")
nameInput.Input.variant = "bordered"
nameInput.Input.layout = { span = 6 }

local countInput = ui.createNumberInput("count", 1, "数量", 0, 100, 1, "1~100")
countInput.NumberInput.variant = "bordered"
countInput.NumberInput.layout = { span = 6 }

local modeSelect = ui.createSelect("mode", "auto", "模式", modeItems)
modeSelect.Select.variant = "bordered"
modeSelect.Select.layout = { span = 12 }

local enableSwitch = {
    Switch = {
        name = "enableFeature",
        title = "启用功能",
        isSelected = true,
        color = "primary",
        layout = { span = 12 }
    }
}

local page = {
    Div = {
        className = "rounded-small border border-default-200 p-4",
        item = {
            {
                {
                    P = {
                        text = "这是 AI 生成的动态 UI 示例。",
                        className = "text-sm text-default-500",
                        layout = { span = 12 }
                    }
                }
            },
            {
                {
                    Group = {
                        title = "基础参数",
                        direction = "grid",
                        columns = 12,
                        gap = 4,
                        item = {
                            { nameInput, countInput },
                            { modeSelect },
                            { enableSwitch }
                        }
                    }
                }
            }
        }
    }
}

ui.buildUI({ page }, 1)
StartUI()

function onUIChanged(values)
    if values.name ~= nil then
        print("名称变化:", values.name)
    end

    if values.count ~= nil then
        print("数量变化:", values.count)
    end

    if values.mode ~= nil then
        print("模式变化:", values.mode)
    end

    if values.enableFeature ~= nil then
        print("启用功能变化:", values.enableFeature)
    end
end

function readCurrentValues()
    print(UICurrentValue.name)
    print(UICurrentValue.count)
    print(UICurrentValue.mode)
    print(UICurrentValue.enableFeature)
end

禁止事项

AI 生成时不要做这些事:

  • 不要给 DivGroupPTabsAccordion 生成业务 name
  • 不要从 values.Divvalues.Groupvalues.P 取值。
  • 不要把 onUIChanged(values) 当成增量变化表;它是全量当前值表。
  • 不要重复使用相同的输入组件 key / name
  • 不要生成事件字符串,例如 onClick = "run()"
  • 不要把多个组件类型写进同一个节点。
  • 不要假设 Select 多选返回数组;当前返回逗号字符串。

取值检查清单

生成 UI 后,AI 应检查:

  • 每个需要读取的输入组件都有唯一 key / name
  • onUIChanged(values) 中可以读取全量当前值;读取可选字段时仍建议用 ~= nil 判断。
  • UICurrentValue.xxx 中的 xxx 对应真实输入组件。
  • 布局组件只负责排版,不参与取值。
  • 嵌套在 TabsAccordionGroupDiv 内的输入组件仍按自己的 key / name 取值。