Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ declare module 'vue' {
MkInfiniteScroll: typeof import('./components/global/mk-infinite-scroll/index.vue')['default']
MkListItem: typeof import('./components/global/mk-list-item/index.vue')['default']
MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
MkSlider: typeof import('./components/global/mk-slider/index.vue')['default']
MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default']
MkTable: typeof import('./components/global/mk-table/index.vue')['default']
MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default']
Expand Down Expand Up @@ -66,6 +67,7 @@ declare global {
const MkInfiniteScroll: typeof import('./components/global/mk-infinite-scroll/index.vue')['default']
const MkListItem: typeof import('./components/global/mk-list-item/index.vue')['default']
const MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
const MkSlider: typeof import('./components/global/mk-slider/index.vue')['default']
const MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default']
const MkTable: typeof import('./components/global/mk-table/index.vue')['default']
const MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default']
Expand Down
32 changes: 32 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ src/components/
│ │ └── index.vue # 带默认搜索图标的输入框
│ ├── mk-status-label/
│ │ └── index.vue # 布尔状态图标和文案
│ ├── mk-slider/
│ │ └── index.vue # 滑块与右置控制按钮的数值输入框
│ ├── mk-table/
│ │ ├── index.vue # 表格、分页、列宽拖拽和批量操作
│ │ ├── mk-table-filter.vue # 表头多选筛选器
Expand Down Expand Up @@ -563,6 +565,22 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载
<MkSearchInput v-model="searchKeyword" placeholder="搜索工作空间" />
```

### MkSlider

全局滑块组件,组合 `el-slider` 和 `el-input-number`,默认显示数值输入框,控制按钮固定在
右侧,输入内容左对齐。输入框的 `controls` 固定为 `true`,始终显示加减按钮;
`show-input-controls` 不作为组件配置使用,调用方无需传入,传入也不会改变按钮显隐。
其余 Element Plus Slider Props 和 `update:modelValue`、`input`、`change` 事件保持可用;
`class`、`style` 作用于外层布局。通过 `:show-input="false"` 隐藏输入框。
`range` 或 `step="mark"` 模式不显示
单值输入框;垂直模式下输入框位于滑块下方。输入框与滑块共用范围、步长、禁用状态,
输入框清空并提交后回退到最小值,表单变更校验由滑块统一触发。

```vue
<MkSlider v-model="temperature" :min="0" :max="1" :step="0.1" />
<MkSlider v-model="score" :show-input="false" />
```

### MkStatusLabel

用于展示布尔状态。`active` 控制启用或禁用,默认文案为“已启用”和“已禁用”;通过
Expand Down Expand Up @@ -1008,6 +1026,20 @@ const formValue = ref<Dict<DynamicFormValue>>({})
字段配置中的动态校验器和表格行表达式属于受信任的服务端协议,只允许加载可信配置;普通业务
输入不得作为脚本传入。

单选、多选和卡片单选配置器的自定义选项中,标签和选项值的必填校验跟随字段的“是否必填”;每行通过
`option_list.<index>.label`、`option_list.<index>.value` 参与配置表单校验。编辑中的不完整行
保留在表单内,仅两项都填写且非纯空白的选项进入默认值选择区和 `getData()` 返回的 `option_list`。
初始化、空配置回填和切回自定义赋值时保留一行空白选项。卡片单选仅在存在完整选项时显示默认值卡片区。
引用变量模式继续保留变量路径,不应用自定义选项过滤。

动态表单的 Model 字段使用 `ModelSelect`,将 `attrs.provider_list` 中的模型快照映射为
扁平 `ModelItem[]`,由选择器统一分组和展示供应商。旧快照未保存状态时保留可选行为,已保存的
状态按 `ModelSelect` 的可用性规则处理。切换模型一次性回写 `model_id` 和深拷贝后的
`model_params_setting`;清空时回写空 ID 和空参数,避免修改原配置中的参数对象。

Model 配置器的默认模型也使用 `ModelSelect`,仅展示已选的可选模型,不开启参数设置入口。
选择时将模型 ID 转换为包含已配置参数的 `default_value` 对象,清空时重置为空对象。

## 跨页面业务组件

业务组件可以调用其固定业务 API,但应将通用展示部分拆成内部纯 UI 组件;页面继续负责资源列表
Expand Down
57 changes: 57 additions & 0 deletions ui/src/components/global/mk-slider/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed } from 'vue'
import { sliderEmits, sliderProps } from 'element-plus'

defineOptions({ name: 'MkSlider' })

/* input-number 的controls始终是true,el-slider上的show-input-controls不需要*/
const props = defineProps({
...sliderProps,
showInput: { type: Boolean, default: true },
})
const emit = defineEmits(sliderEmits)

// 区间和标记步长模式沿用原生滑块,不显示单值输入框。
const showNumberInput = computed(() => props.showInput && !props.range && props.step !== 'mark')

function updateInputValue(value: number | undefined) {
if (value === undefined) return
emit('update:modelValue', value)
emit('input', value)
}

function changeInputValue(value: number | undefined) {
if (value !== undefined) emit('change', value)
}
</script>

<template>
<div class="flex w-full items-center gap-4" :class="{ 'flex-col': props.vertical }">
<el-slider
v-bind="props"
:show-input="false"
:class="{ 'min-w-0 flex-1': !props.vertical }"
@update:model-value="emit('update:modelValue', $event)"
@input="emit('input', $event)"
@change="emit('change', $event)"
/>
<el-input-number
v-if="showNumberInput"
:model-value="typeof props.modelValue === 'number' ? props.modelValue : props.min"
:min="props.min"
:max="props.max"
:step="typeof props.step === 'number' ? props.step : 1"
:disabled="props.disabled"
:size="props.inputSize || props.size"
:controls="true"
:aria-label="props.ariaLabel"
:validate-event="false"
value-on-clear="min"
controls-position="right"
align="left"
class="w-30! shrink-0"
@update:model-value="updateInputValue"
@change="changeInputValue"
/>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
<script setup lang="ts">
import type { DynamicFormValue } from '../../type'
import { computed, inject, ref } from 'vue'
import { groupBy } from 'lodash'
import ModelSelect from '@/components/business/model-select/index.vue'
import { providerList as providerOptions } from '../../items/model/provider-data'
import { relatedObject } from '@/utils/common'
const modelTypeList = [
{ text: 'LLM', value: 'LLM' },
{ text: 'EMBEDDING', value: 'EMBEDDING' },
{ text: 'RERANKER', value: 'RERANKER' },
{ text: 'STT', value: 'STT' },
{ text: 'TTS', value: 'TTS' },
{ text: 'IMAGE', value: 'IMAGE' },
{ text: 'TTI', value: 'TTI' },
{ text: 'ITV', value: 'ITV' },
{ text: 'TTV', value: 'TTV' },
]
import { MODEL_TYPE_LABELS } from '@/constants/model'
const getSelectModelList = inject<(params: { model_type: string }) => Promise<DynamicFormValue>>('getSelectModelList')
const getModelParamsForm = inject<(modelId: string) => Promise<DynamicFormValue>>('getModelParamsForm')

Expand Down Expand Up @@ -58,8 +46,7 @@ const selectedIds = computed({

const selectedModelsOptions = computed(() => {
const ids = (formValue.value.provider_list || []).map((p: DynamicFormValue) => p.model_id)
const filtered = rawModelOptions.value.filter((m: DynamicFormValue) => ids.includes(m.id))
return groupBy(filtered, 'provider')
return rawModelOptions.value.filter((model: DynamicFormValue) => ids.includes(model.id))
})

function fetchDefaultParams(modelId: string) {
Expand All @@ -84,7 +71,6 @@ function fetchDefaultParams(modelId: string) {
})
}
const rawModelOptions = ref<DynamicFormValue[]>([])
const groupedModelOptions = ref<Record<string, any[]>>({})

const fetchModelByType = (type: string) => {
if (!type || !getSelectModelList) return
Expand All @@ -101,7 +87,6 @@ const handleModelTypeChange = (val: string) => {
fetchModelByType(val)
} else {
rawModelOptions.value = []
groupedModelOptions.value = {}
}
}

Expand All @@ -120,6 +105,13 @@ const getProviderItem = (modelId: string) => {
return { model_id: modelId, model_params_setting: {} }
}

const defaultModelId = computed({
get: () => formValue.value.default_value?.model_id ?? '',
set: (modelId: string) => {
formValue.value.default_value = modelId ? getProviderItem(modelId) : {}
},
})

const getData = () => {
const providerList = (formValue.value.provider_list || []).map((p: DynamicFormValue) => {
const modelInfo = getModelInfo(p.model_id)
Expand All @@ -131,7 +123,12 @@ const getData = () => {
model_form_field: p.model_form_field || [],
}
})
return { input_type: 'Model', model_type: formValue.value.model_type, default_value: formValue.value.default_value, attrs: { provider_list: providerList } }
return {
input_type: 'Model',
model_type: formValue.value.model_type,
default_value: formValue.value.default_value,
attrs: { provider_list: providerList },
}
}

const render = (formData: DynamicFormValue) => {
Expand All @@ -150,15 +147,21 @@ defineExpose({ getData, render })
<template>
<el-form-item label="模型类型" required prop="model_type" :rules="[{ required: true, message: '请选择模型类型' }]">
<el-select v-model="formValue.model_type" placeholder="请选择模型类型" @change="handleModelTypeChange">
<el-option v-for="item in modelTypeList" :key="item.value" :label="item.text" :value="item.value" />
<el-option v-for="(label, value) in MODEL_TYPE_LABELS" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>

<el-form-item label="可选模型" required prop="provider_list" :rules="[{ required: true, message: '请选择模型', type: 'array' }]">
<div class="flex-between w-full">
<ModelSelect multiple v-model="selectedIds" placeholder="请选择模型" :options="rawModelOptions" :provider-options="providerOptions" :model-type="formValue.model_type">
</ModelSelect>
</div>
<!-- // TODO -->
<ModelSelect
multiple
v-model="selectedIds"
placeholder="请选择模型"
:options="rawModelOptions"
:provider-options="providerOptions"
:model-type="formValue.model_type"
>
</ModelSelect>
</el-form-item>
<el-form-item
label="默认模型"
Expand All @@ -167,29 +170,7 @@ defineExpose({ getData, render })
:rules="formValue.required ? [{ required: true, message: '请选择模型' }] : []"
v-if="formValue.provider_list && formValue.provider_list.length > 0"
>
<div class="flex-between w-full">
<el-select v-model="formValue.default_value" value-key="model_id" placeholder="请选择模型">
<el-option-group
v-for="(modelList, providerName) in selectedModelsOptions"
:key="providerName"
:label="relatedObject(providerOptions, String(providerName), 'provider')?.name"
>
<el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="getProviderItem(item.id)">
<el-space :size="8">
<span :innerHTML="String(relatedObject(providerOptions, String(providerName), 'provider')?.icon ?? '')" class="select-model-icon" style="margin-top: -7px"></span>
<span>{{ item.name }}</span>
</el-space>
</el-option>
</el-option-group>
<template #label="{ label, value }">
<el-space :size="8" v-if="value?.model_id">
<span class="select-model-icon" :innerHTML="String(relatedObject(providerOptions, getModelInfo(value.model_id)?.provider, 'provider')?.icon ?? '')"> </span>
<span>
<span>{{ label }}</span>
</span>
</el-space>
</template>
</el-select>
</div>
<!-- // TODO -->
<ModelSelect v-model="defaultModelId" placeholder="请选择模型" :options="selectedModelsOptions" :provider-options="providerOptions" />
</el-form-item>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,25 @@ const addOption = () => {
formValue.value.option_list.push({ value: '', label: '' })
}

const resetOptions = () => {
formValue.value.option_list = []
if (formValue.value.assignment_method === 'custom') addOption()
}

const handleOptionRemove = (option: DynamicFormConstructorOption) => {
if (Array.isArray(formValue.value.default_value)) {
formValue.value.default_value = formValue.value.default_value.filter((value: DynamicFormValue) => value !== option.value)
}
}

// 编辑中的空行保留在表单内,仅完整选项参与默认值选择和配置输出。
const completeOptions = computed<DynamicFormConstructorOption[]>(() => {
if (formValue.value.assignment_method === 'ref_variables') return []
return (formValue.value.option_list || []).filter(
(option: DynamicFormConstructorOption) => String(option.label ?? '').trim() && String(option.value ?? '').trim(),
)
})

const getData = () => {
return {
input_type: 'MultiSelect',
Expand All @@ -60,14 +73,16 @@ const getData = () => {
show_default_value: formValue.value.show_default_value,
text_field: 'label',
value_field: 'value',
option_list: formValue.value.option_list,
option_list: formValue.value.assignment_method === 'ref_variables' ? formValue.value.option_list : completeOptions.value,
assignment_method: formValue.value.assignment_method || 'custom',
}
}
const render = (formData: DynamicFormValue) => {
formValue.value.option_list = formData.option_list || []
formValue.value.default_value = formData.default_value
formValue.value.assignment_method = formData.assignment_method || 'custom'
// 空配置回填后仍保留一行,供用户填写首个选项。
if (formValue.value.assignment_method === 'custom' && !formValue.value.option_list.length) addOption()
}

defineExpose({ getData, render })
Expand All @@ -85,7 +100,7 @@ onMounted(() => {
<template>
<el-form-item v-if="getModel" label="赋值方式">
<!-- // TODO 赋值方式待调整 -->
<el-radio-group @change="formValue.option_list = []" v-model="formValue.assignment_method">
<el-radio-group @change="resetOptions" v-model="formValue.assignment_method">
<el-radio :value="item.value" v-for="(item, index) in assignmentMethodOptions" :key="index">
<span class="flex align-center">
{{ item.label }}
Expand Down Expand Up @@ -122,13 +137,23 @@ onMounted(() => {
</el-button>
</div>

<div class="w-full mk-gray-card">
<div class="w-full mk-gray-card py-3!">
<MkFormList v-model="formValue.option_list" :default-item="{ label: '', value: '' }" :show-add-button="false" @remove="handleOptionRemove">
<template #default="{ index, item: option }">
<el-form-item :label="index === 0 ? '标签' : ''" class="flex-1">
<el-input v-model="option.label" placeholder="请输入选项标签" />
<el-form-item
:label="index === 0 ? '标签' : ''"
:prop="`option_list.${index}.label`"
:rules="[{ required: formValue.required, message: '请输入标签', trigger: ['blur', 'change'] }]"
class="flex-1 small"
>
<el-input v-model="option.label" placeholder="请输入标签" />
</el-form-item>
<el-form-item :label="index === 0 ? '选项值' : ''" class="flex-1">
<el-form-item
:label="index === 0 ? '选项值' : ''"
:prop="`option_list.${index}.value`"
:rules="[{ required: formValue.required, message: '请输入选项值', trigger: ['blur', 'change'] }]"
class="flex-1 small"
>
<el-input v-model="option.value" placeholder="请输入选项值" />
</el-form-item>
</template>
Expand All @@ -149,7 +174,7 @@ onMounted(() => {
</div>
</template>
<el-select multiple collapse-tags filterable clearable :reserve-keyword="false" v-model="formValue.default_value">
<el-option v-for="(option, index) in formValue.option_list" :key="index" :label="option.label" :value="option.value" />
<el-option v-for="(option, index) in completeOptions" :key="index" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,24 @@ onMounted(() => {
</script>

<template>
<el-form-item label="文本长度" required>
<div class="flex w-full items-start gap-2">
<el-form-item class="min-w-0 flex-1" :rules="[{ required: true, message: '请输入最小长度', trigger: 'change' }]" prop="minlength">
<el-input-number v-model="formValue.minlength" class="w-full!" :min="1" :step="1" controls-position="right" align="left" step-strictly />
</el-form-item>
<span class="flex-center shrink-0">-</span>
<el-form-item class="min-w-0 flex-1" :rules="[{ required: true, message: '请输入最大长度', trigger: 'change' }]" prop="maxlength">
<el-input-number
v-model="formValue.maxlength"
class="w-full!"
:min="formValue.minlength > formValue.maxlength ? formValue.minlength : 1"
:step="1"
controls-position="right"
align="left"
step-strictly
/>
</el-form-item>
</div>
</el-form-item>
<div class="mk-required mb-2">文本长度</div>
<div class="flex w-full items-start gap-2">
<el-form-item class="min-w-0 flex-1" :rules="[{ required: true, message: '请输入最小长度', trigger: 'change' }]" prop="minlength">
<el-input-number v-model="formValue.minlength" class="w-full!" :min="1" :step="1" controls-position="right" align="left" step-strictly />
</el-form-item>
<span class="flex-center shrink-0 h-8">-</span>
<el-form-item class="min-w-0 flex-1" :rules="[{ required: true, message: '请输入最大长度', trigger: 'change' }]" prop="maxlength">
<el-input-number
v-model="formValue.maxlength"
class="w-full!"
:min="formValue.minlength > formValue.maxlength ? formValue.minlength : 1"
:step="1"
controls-position="right"
align="left"
step-strictly
/>
</el-form-item>
</div>

<el-form-item
class="mk-hide-asterisk"
Expand Down
Loading
Loading