optimize ParamBuilderMobile components

This commit is contained in:
GeekMaster
2025-09-15 22:18:46 +08:00
parent 5979451ea6
commit 49254b2a32
10 changed files with 404 additions and 39 deletions

View File

@@ -148,7 +148,7 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1.5px solid transparent; border: 1.5px solid transparent;
border-radius: 12px; border-radius: 12px;
background: #fff; // background: #fff;
position: relative; position: relative;
z-index: 1; z-index: 1;

View File

@@ -26,6 +26,7 @@
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active: rgba(255, 255, 255, 0.1); --border-active: rgba(255, 255, 255, 0.1);
--card-bg: #252d58; --card-bg: #252d58;
--card-bg-secondary: #313a6b;
--chat-bg: #1f243f; --chat-bg: #1f243f;
--chat-wel-bg: #2d2f38; --chat-wel-bg: #2d2f38;
--card-bg-table: rgba(17, 28, 68, 1); --card-bg-table: rgba(17, 28, 68, 1);

View File

@@ -24,7 +24,8 @@
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active: rgba(134, 140, 255, 1); --border-active: rgba(134, 140, 255, 1);
--code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--card-bg: #fff; --card-bg: #f5f5f5;
--card-bg-secondary: #e5e5e5;
--chat-bg: #fff; --chat-bg: #fff;
--theme-bg: linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff); --theme-bg: linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
--theme-bg-all: #f5f7fd; --theme-bg-all: #f5f7fd;

View File

@@ -4,7 +4,11 @@
@click="showPicker = true" @click="showPicker = true"
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors" class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors"
> >
<span>{{ selectedLabel || placeholder || '请选择' }}</span> <span>
<slot name="label">
{{ selectedLabel || placeholder || '请选择' }}
</slot>
</span>
<i class="iconfont icon-arrow-down text-gray-400"></i> <i class="iconfont icon-arrow-down text-gray-400"></i>
</button> </button>

View File

@@ -1,19 +1,4 @@
<template> <template>
<!--
CustomSelectOption 组件
Props:
- option: 选项对象必需包含 label/desc/value 等属性
- selected: 是否为当前选中项
Emits:
- select(option): 选中该项时触发
Slots:
- 默认插槽default用于自定义 option 内容slotProps: { option, selected }
示例
<template #option="{ option, selected }">
<div>{{ option.label }}</div>
<div v-if="selected"></div>
</template>
-->
<div <div
class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors border-b last:border-b-0" class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors border-b last:border-b-0"
@click="$emit('select', option)" @click="$emit('select', option)"
@@ -23,15 +8,15 @@
<span class="text-gray-900 font-medium">{{ option.label }}</span> <span class="text-gray-900 font-medium">{{ option.label }}</span>
<p v-if="option.desc" class="text-sm text-gray-500 mt-1">{{ option.desc }}</p> <p v-if="option.desc" class="text-sm text-gray-500 mt-1">{{ option.desc }}</p>
</div> </div>
<div v-if="selected" class="text-blue-600">
<i class="iconfont icon-success"></i>
</div>
</slot> </slot>
<div v-if="selected" class="text-green-500">
<i class="iconfont icon-success"></i>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits } from 'vue' import { defineEmits, defineProps } from 'vue'
const props = defineProps({ const props = defineProps({
option: { option: {

View File

@@ -0,0 +1,371 @@
<template>
<div class="param-builder-mobile">
<ParamEmpty
v-if="items.length === 0"
:progress="progress"
:title="title"
:status-text="statusText"
:description="description"
/>
<div v-else class="flex flex-col w-full space-y-5">
<!-- 模型选择移动端样式使用 CustomSelect -->
<div class="bg-white rounded-xl shadow-sm w-full p-4">
<label class="block text-gray-700 mb-2 font-semibold">选择模型</label>
<CustomSelect v-model="selectedModelKey" :options="modelOptions" title="选择模型">
<template #label>
<div class="flex items-center w-full">
<i class="iconfont icon-model !text-xl mr-2"></i>
<span class="text-gray-700 font-semibold">{{
selectedModel.name || '请选择模型'
}}</span>
</div>
</template>
<template #option="{ option, selected }">
<div class="flex items-center w-full">
<span
class="flex items-center justify-center text-white model-version mr-2 w-[40px] h-[40px] rounded-lg"
:class="option.iconSize ? option.iconSize : '!text-xl'"
>{{ option.iconText }}</span
>
<div class="flex !items-start flex-col py-1">
<span
class="font-semibold text-gray-900"
:class="{ '!text-purple-600': selected }"
>{{ option.label }}</span
>
<span
class="text-xs text-gray-500 line-clamp-1 max-w-[250px]"
:title="option.subLabel"
>{{ option.subLabel }}</span
>
</div>
</div>
</template>
</CustomSelect>
</div>
<!-- 参数渲染移动端卡片样式 -->
<template v-for="param in selectedModel.params">
<div
class="bg-white rounded-xl shadow-sm w-full p-4"
:key="param.name"
v-if="param.type !== 'hidden'"
>
<!-- switch 类型单独处理 -->
<div class="w-full flex flex-col !items-start space-y-2" v-if="param.type === 'switch'">
<div class="w-full flex justify-between items-center">
<label class="text-gray-700 font-semibold">{{ param.label }}</label>
<van-switch v-model="modelValue[param.name]" size="default" />
</div>
<p v-if="param.info" class="text-xs text-gray-500">{{ param.info }}</p>
</div>
<div class="w-full flex flex-col !items-start space-y-2" v-else>
<label class="text-gray-700 font-semibold">
{{ param.label }}
<span v-if="param.required" class="text-red-500 ml-1">*</span>
</label>
<p v-if="param.info" class="text-xs text-gray-500">{{ param.info }}</p>
<div class="flex w-full">
<el-input
v-if="param.type === 'text'"
v-model="modelValue[param.name]"
:placeholder="param.placeholder"
/>
<el-input-number
v-if="param.type === 'number'"
v-model="modelValue[param.name]"
class="!w-full"
:placeholder="param.placeholder"
:min="param.min"
:max="param.max"
:step="param.step"
/>
<el-slider
v-if="param.type === 'slider'"
v-model="modelValue[param.name]"
:min="param.min"
:max="param.max"
:step="param.step"
/>
<el-date-picker
v-if="param.type === 'date'"
v-model="modelValue[param.name]"
:placeholder="param.placeholder"
/>
<el-time-picker
v-if="param.type === 'time'"
v-model="modelValue[param.name]"
:placeholder="param.placeholder"
/>
<!-- 使用 CustomSelect 替换 el-select -->
<CustomSelect
v-if="param.type === 'select'"
v-model="modelValue[param.name]"
:options="formatParamOptions(param)"
:title="param.placeholder || '请选择' + param.label"
class="w-full"
>
<template #label>
<div class="flex items-center w-full">
<i class="iconfont !text-xl mr-2" :class="param.prefix ? param.prefix : ''"></i>
<span class="text-gray-700 font-semibold">{{
param.placeholder || '请选择' + param.label
}}</span>
</div>
</template>
<template v-if="hasImageOption(param)" #option="{ option, selected }">
<div class="flex items-center w-full">
<el-image
v-if="option.image"
:src="option.image"
fit="cover"
class="w-10 h-10 rounded-lg mr-2"
/>
<div class="flex !items-start flex-col py-1">
<span
class="font-bold text-gray-900 mr-2"
:class="{ '!text-purple-600': selected }"
>{{ option.label }}</span
>
<span
class="text-xs text-gray-500 line-clamp-1 max-w-[200px]"
:title="option.value"
>{{ option.value }}</span
>
</div>
</div>
</template>
<template #option="{ option, selected }" v-else>
<div class="flex items-center w-full">
<span class="mr-2" :class="{ 'font-bold !text-purple-600': selected }">{{
option.label
}}</span>
</div>
</template>
</CustomSelect>
<el-input
type="textarea"
v-if="param.type === 'textarea'"
v-model="modelValue[param.name]"
:autosize="param.autosize || { minRows: 3, maxRows: 6 }"
:maxlength="param.maxlength"
:show-word-limit="param.showWordLimit"
:placeholder="param.placeholder"
/>
<ImageUpload
v-if="param.type === 'image'"
v-model="modelValue[param.name]"
:max-count="param.maxCount"
:multiple="param.multiple"
:max-size="param.maxSize"
:accept="param.accept"
/>
<FileUpload
v-if="param.type === 'file'"
v-model="modelValue[param.name]"
:max-count="param.maxCount"
:multiple="param.multiple"
:max-size="param.maxSize"
:accept="param.accept"
/>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup>
import FileUpload from '@/components/FileUpload.vue'
import ImageUpload from '@/components/ImageUpload.vue'
import CustomSelect from '@/components/mobile/CustomSelect.vue'
import ParamEmpty from '@/components/ui/ParamEmpty.vue'
import { computed, onMounted, ref, watch } from 'vue'
const title = ref('参数构建器')
const statusText = ref('功能正在开发中')
const description = ref('我们正在努力完善当前功能,敬请期待!')
const props = defineProps({
modelValue: {
type: Object,
required: true,
},
requiredKeys: {
type: Object,
default: {},
required: false,
},
items: {
type: Array,
required: true,
},
progress: {
type: Number,
default: 65,
validator: (value) => value >= 0 && value <= 100,
},
})
const selectedModel = ref(props.items[0])
const selectedModelKey = ref(props.items[0] ? props.items[0].key : '')
const requiredKeys = ref(props.requiredKeys)
const emit = defineEmits(['update:modelValue', 'update:requiredKeys'])
const initModelValue = (model) => {
if (!props.items || props.items.length === 0) {
return {}
}
const defaultValues = {}
requiredKeys.value = {}
if (model && model.params) {
model.params.forEach((param) => {
if (param.required) {
requiredKeys.value[param.name] = { required: true, label: param.label }
}
switch (param.type) {
case 'text':
case 'textarea':
defaultValues[param.name] = param.value || ''
break
case 'number':
defaultValues[param.name] = param.value || 0
break
case 'slider':
defaultValues[param.name] = param.value || param.min || 0
break
case 'select':
defaultValues[param.name] =
param.value || (param.options && param.options[0] ? param.options[0].value : '')
break
case 'checkbox':
case 'switch':
defaultValues[param.name] = param.value || false
break
case 'date':
case 'time':
defaultValues[param.name] = param.value || null
break
case 'image':
defaultValues[param.name] = param.value || []
break
default:
defaultValues[param.name] = param.value || ''
}
})
}
defaultValues.req_key = selectedModel.value.key
defaultValues.action = selectedModel.value.action
? selectedModel.value.action
: 'CVSync2AsyncSubmitTask'
return defaultValues
}
const modelValue = ref(initModelValue(selectedModel.value))
const modelOptions = computed(() => {
return (props.items || []).map((m) => ({
label: m.name,
value: m.key,
subLabel: m.label,
iconText: m.icon?.text || '',
iconSize: m.icon?.size || '!text-xl',
}))
})
watch(
modelValue,
(newValue) => {
emit('update:modelValue', newValue)
},
{ deep: true }
)
watch(
requiredKeys,
(newValue) => {
emit('update:requiredKeys', newValue)
},
{ deep: true }
)
watch(
() => props.items,
(newValue) => {
selectedModel.value = newValue[0]
selectedModelKey.value = newValue[0]?.key || ''
modelValue.value = initModelValue(selectedModel.value)
},
{ deep: true }
)
watch(selectedModelKey, (key) => {
if (!key) return
const found = (props.items || []).find((m) => m.key === key)
if (found) {
selectedModel.value = found
modelValue.value = initModelValue(found)
}
})
onMounted(() => {
if (props.modelValue && Object.keys(props.modelValue).length > 0) {
modelValue.value = { ...props.modelValue }
} else {
modelValue.value = initModelValue(selectedModel.value)
}
})
const hasImageOption = (param) => {
return Array.isArray(param.options) && param.options.some((o) => !!o.image)
}
const formatParamOptions = (param) => {
return (param.options || []).map((o) => ({
label: o.label,
value: o.value,
image: o.image,
}))
}
</script>
<style lang="scss" scoped>
@use '@/assets/css/mobile/jimeng.scss';
.param-builder-mobile {
.model-version {
background: url('@/assets/img/model-version.png') no-repeat center center;
background-size: cover;
}
}
/* 采用 JimengCreate.vue 的卡片/表单视觉(该文件已引入 mobile/jimeng.scss */
:deep(.custom-select) {
.select-trigger {
background-color: rgb(31, 41, 55);
border-color: rgb(75, 85, 99);
color: rgb(209, 213, 219);
}
.select-dropdown {
background-color: rgb(55, 65, 81);
border-color: rgb(75, 85, 99);
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8);
}
.select-option {
color: rgb(209, 213, 219);
&:hover {
background-color: rgb(75, 85, 99);
}
&.selected {
background-color: rgb(139, 92, 246);
color: rgb(255, 255, 255);
}
}
}
</style>

View File

@@ -172,10 +172,11 @@
<template #option="{ option, selected }"> <template #option="{ option, selected }">
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<el-image :src="option.preview" fit="cover" class="w-10 h-10 rounded-lg mr-2" /> <el-image :src="option.preview" fit="cover" class="w-10 h-10 rounded-lg mr-2" />
<span class="font-bold text-blue-600 mr-2">{{ option.label }}</span> <span
<span v-if="selected" class="ml-auto text-green-500" class="font-bold text-gray-900 mr-2"
><i class="iconfont icon-success"></i :class="{ '!text-purple-600': selected }"
></span> >{{ option.label }}</span
>
</div> </div>
</template> </template>
</CustomSelect> </CustomSelect>

View File

@@ -40,11 +40,12 @@
> >
<template #option="{ option, selected }"> <template #option="{ option, selected }">
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<span class="font-bold text-blue-600 mr-2">{{ option.label }}</span> <span
class="font-bold text-gray-900 mr-2"
:class="{ '!text-purple-600': selected }"
>{{ option.label }}</span
>
<span class="text-xs text-gray-400">({{ option.value }})</span> <span class="text-xs text-gray-400">({{ option.value }})</span>
<span v-if="selected" class="ml-auto text-green-500"
><i class="iconfont icon-success"></i
></span>
</div> </div>
</template> </template>
</CustomSelect> </CustomSelect>
@@ -498,10 +499,10 @@
<script setup> <script setup>
import '@/assets/css/mobile/suno.scss' import '@/assets/css/mobile/suno.scss'
import CustomSelect from '@/components/mobile/CustomSelect.vue' import CustomSelect from '@/components/mobile/CustomSelect.vue'
import { checkSession } from '@/store/cache'
import { useSunoStore } from '@/store/mobile/suno' import { useSunoStore } from '@/store/mobile/suno'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { checkSession } from '@/store/cache'
const router = useRouter() const router = useRouter()
const suno = useSunoStore() const suno = useSunoStore()

View File

@@ -221,11 +221,12 @@
> >
<template #option="{ option, selected }"> <template #option="{ option, selected }">
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<span class="font-bold text-blue-600 mr-2">{{ option.label }}</span> <span
class="font-bold text-gray-900 mr-2"
:class="{ '!text-purple-600': selected }"
>{{ option.label }}</span
>
<span class="text-xs text-gray-400">({{ option.value }})</span> <span class="text-xs text-gray-400">({{ option.value }})</span>
<span v-if="selected" class="ml-auto text-green-500"
><i class="iconfont icon-success"></i
></span>
</div> </div>
</template> </template>
</CustomSelect> </CustomSelect>
@@ -613,11 +614,11 @@
<script setup> <script setup>
import '@/assets/css/mobile/video.scss' import '@/assets/css/mobile/video.scss'
import CustomSelect from '@/components/mobile/CustomSelect.vue' import CustomSelect from '@/components/mobile/CustomSelect.vue'
import { checkSession } from '@/store/cache'
import { useVideoStore } from '@/store/mobile/video' import { useVideoStore } from '@/store/mobile/video'
import { showConfirmDialog } from 'vant' import { showConfirmDialog } from 'vant'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { checkSession } from '@/store/cache'
const router = useRouter() const router = useRouter()
const video = useVideoStore() const video = useVideoStore()

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="audio-chat-page"> <div class="audio-chat-page bg-gray-50">
<div class="flex m-3"> <div class="flex m-3">
<el-select v-model="currentFunction" placeholder="请选择功能" popper-class="custom-select"> <el-select v-model="currentFunction" placeholder="请选择功能" popper-class="custom-select">
<template #prefix> <template #prefix>
@@ -18,7 +18,7 @@
</div> </div>
<div class="p-3"> <div class="p-3">
<param-builder <param-builder-mobile
v-model="formData" v-model="formData"
:items="params[currentFunction]" :items="params[currentFunction]"
:progress="progress[currentFunction]" :progress="progress[currentFunction]"
@@ -40,7 +40,7 @@
</template> </template>
<script setup> <script setup>
import ParamBuilder from '@/components/ParamBuilder.vue' import ParamBuilderMobile from '@/components/mobile/ParamBuilderMobile.vue'
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_params' import { JimengFunctions, JimengParams } from '@/store/data/jimeng_params'
import { ref } from 'vue' import { ref } from 'vue'