mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
fix(web): create form
This commit is contained in:
@@ -67,7 +67,6 @@
|
||||
|
||||
<!-- Display Name -->
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
@@ -79,7 +78,8 @@
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('models.displayNamePlaceholder')"
|
||||
v-bind="componentField"
|
||||
:model-value="form.values.name ?? ''"
|
||||
@input="onNameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@@ -130,7 +130,7 @@
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="!form.meta.value.valid"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
<Spinner v-if="isLoading" />
|
||||
{{ title === 'edit' ? $t('common.save') : $t('models.addModel') }}
|
||||
@@ -169,11 +169,11 @@ import {
|
||||
Spinner,
|
||||
} from '@memoh/ui'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { inject, computed, watch, type Ref, ref } from 'vue'
|
||||
import { inject, computed, watch, nextTick, type Ref, ref } from 'vue'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { type ModelInfo } from '@memoh/shared'
|
||||
import { useCreateModel } from '@/composables/api/useModels'
|
||||
import { useCreateModel, useUpdateModel } from '@/composables/api/useModels'
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
type: z.string().min(1),
|
||||
@@ -187,53 +187,117 @@ const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
})
|
||||
|
||||
const selectedType = computed(() => form.values.type)
|
||||
|
||||
const { id } = defineProps<{ id: string }>()
|
||||
|
||||
const { mutate: createModel, isLoading } = useCreateModel()
|
||||
|
||||
const addModel = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
type: values.type,
|
||||
model_id: values.model_id,
|
||||
llm_provider_id: id,
|
||||
}
|
||||
|
||||
if (values.name) {
|
||||
payload.name = values.name
|
||||
}
|
||||
|
||||
if (values.type === 'embedding' && values.dimensions) {
|
||||
payload.dimensions = values.dimensions
|
||||
}
|
||||
|
||||
if (values.type === 'chat') {
|
||||
payload.is_multimodal = values.is_multimodal ?? false
|
||||
}
|
||||
|
||||
await createModel(payload as any)
|
||||
open.value = false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
const selectedType = computed(() => form.values.type || editInfo?.value?.type)
|
||||
|
||||
const open = inject<Ref<boolean>>('openModel', ref(false))
|
||||
const title = inject<Ref<'edit' | 'title'>>('openModelTitle', ref('title'))
|
||||
const editInfo = inject<Ref<ModelInfo | null>>('openModelState', ref(null))
|
||||
|
||||
watch(open, () => {
|
||||
if (open.value && editInfo?.value) {
|
||||
form.setValues(editInfo.value)
|
||||
} else {
|
||||
form.resetForm()
|
||||
// 保存按钮:编辑模式直接可提交(表单已预填充,handleSubmit 内部会校验)
|
||||
// 新建模式需要必填字段有值
|
||||
const canSubmit = computed(() => {
|
||||
if (title.value === 'edit') return true
|
||||
const { type, model_id } = form.values
|
||||
return !!type && !!model_id
|
||||
})
|
||||
|
||||
// 新建时的空值
|
||||
const emptyValues = {
|
||||
type: '' as string,
|
||||
model_id: '' as string,
|
||||
name: '' as string,
|
||||
dimensions: undefined as number | undefined,
|
||||
is_multimodal: undefined as boolean | undefined,
|
||||
}
|
||||
|
||||
// Display Name 自动跟随 Model ID,除非用户主动修改过
|
||||
const userEditedName = ref(false)
|
||||
|
||||
watch(
|
||||
() => form.values.model_id,
|
||||
(newModelId) => {
|
||||
if (!userEditedName.value && newModelId !== undefined) {
|
||||
form.setFieldValue('name', newModelId)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function onNameInput(e: Event) {
|
||||
userEditedName.value = true
|
||||
form.setFieldValue('name', (e.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const { id } = defineProps<{ id: string }>()
|
||||
|
||||
const { mutateAsync: createModel, isLoading: createLoading } = useCreateModel()
|
||||
const { mutateAsync: updateModel, isLoading: updateLoading } = useUpdateModel()
|
||||
const isLoading = computed(() => createLoading.value || updateLoading.value)
|
||||
|
||||
async function addModel(e: Event) {
|
||||
e.preventDefault()
|
||||
|
||||
const isEdit = title.value === 'edit' && !!editInfo?.value
|
||||
const fallback = editInfo?.value
|
||||
|
||||
// 从 form.values 读取,编辑模式用 editInfo 兜底
|
||||
// (Dialog 异步渲染可能导致 vee-validate 内部状态未同步)
|
||||
const type = form.values.type || (isEdit ? fallback!.type : '')
|
||||
const model_id = form.values.model_id || (isEdit ? fallback!.model_id : '')
|
||||
const name = form.values.name ?? (isEdit ? fallback!.name : '')
|
||||
const dimensions = form.values.dimensions ?? (isEdit ? fallback!.dimensions : undefined)
|
||||
const is_multimodal = form.values.is_multimodal ?? (isEdit ? fallback!.is_multimodal : undefined)
|
||||
|
||||
if (!type || !model_id) return
|
||||
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
type,
|
||||
model_id,
|
||||
llm_provider_id: id,
|
||||
}
|
||||
|
||||
if (name) {
|
||||
payload.name = name
|
||||
}
|
||||
|
||||
if (type === 'embedding' && dimensions) {
|
||||
payload.dimensions = dimensions
|
||||
}
|
||||
|
||||
if (type === 'chat') {
|
||||
payload.is_multimodal = is_multimodal ?? false
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
await updateModel({ modelId: fallback!.model_id, data: payload as any })
|
||||
} else {
|
||||
await createModel(payload as any)
|
||||
}
|
||||
open.value = false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, async () => {
|
||||
if (!open.value) {
|
||||
title.value = 'title'
|
||||
editInfo.value = null
|
||||
return
|
||||
}
|
||||
|
||||
// 等待 Dialog 内容和 FormField 组件挂载完成
|
||||
await nextTick()
|
||||
|
||||
if (editInfo?.value) {
|
||||
const { type, model_id, name, dimensions, is_multimodal } = editInfo.value
|
||||
form.resetForm({ values: { type, model_id, name, dimensions, is_multimodal } })
|
||||
// 编辑时,如果已有 name 且与 model_id 不同,视为用户自定义
|
||||
userEditedName.value = !!(name && name !== model_id)
|
||||
} else {
|
||||
// 新建模式:显式传空值,避免复用上次编辑数据
|
||||
form.resetForm({ values: { ...emptyValues } })
|
||||
userEditedName.value = false
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
|
||||
@@ -55,6 +55,18 @@ export function useCreateModel() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateModel() {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: ({ modelId, data }: { modelId: string; data: Partial<CreateModelRequest> }) =>
|
||||
fetchApi(`/models/model/${modelId}`, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
}),
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['model'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteModel() {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
|
||||
@@ -67,14 +67,23 @@
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
{{ $t('bots.type') }}
|
||||
<span class="text-muted-foreground text-xs ml-1">({{ $t('common.optional') }})</span>
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('bots.typePlaceholder')"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('bots.typePlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="personal">
|
||||
Personal
|
||||
</SelectItem>
|
||||
<SelectItem value="public">
|
||||
Public
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
@@ -129,6 +138,12 @@ import {
|
||||
FormField,
|
||||
FormControl,
|
||||
FormItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
Label,
|
||||
Spinner,
|
||||
@@ -148,12 +163,15 @@ const isEdit = computed(() => !!editBot.value)
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
display_name: z.string().min(1),
|
||||
avatar_url: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
type: z.string().min(1),
|
||||
is_active: z.coerce.boolean().optional(),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
type: 'personal',
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: createBot, isLoading: createLoading } = useCreateBot()
|
||||
@@ -172,7 +190,7 @@ watch(open, (val) => {
|
||||
},
|
||||
})
|
||||
} else if (!val) {
|
||||
form.resetForm()
|
||||
form.resetForm({ values: { display_name: '', avatar_url: '', type: 'personal' } })
|
||||
editBot.value = null
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user