| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- <!--
- 搜索表单组件
- 基于 AiForm 封装,专门用于列表页搜索场景
- -->
- <template>
- <div class="ai-search-box">
- <AiForm
- ref="formRef"
- v-model:value="formData"
- :schema="schema"
- :grid-cols="gridCols"
- :label-placement="labelPlacement"
- :label-width="labelWidth"
- :size="size"
- :enable-collapse="enableCollapse"
- :max-visible-fields="maxVisibleFields"
- :show-submit="false"
- :show-reset="false"
- :context="formContext"
- :show-feedback="false"
- :y-gap="yGap"
- >
- <!-- 透传所有插槽 -->
- <template v-for="slotName in Object.keys($slots)" #[slotName]="slotProps">
- <slot :name="slotName" v-bind="slotProps" />
- </template>
- <!-- 搜索操作按钮 -->
- <template #formAction="{ formData: data }">
- <n-space>
- <n-button
- type="primary"
- size="small"
- :loading="searchLoading"
- :disabled="searchLoading"
- @click="handleSearch"
- >
- <template #icon>
- <n-icon><SearchOutline /></n-icon>
- </template>
- {{ searchText }}
- </n-button>
- <n-button
- size="small"
- strong
- secondary
- :loading="resetLoading"
- :disabled="searchLoading || resetLoading"
- @click="handleReset"
- >
- <template #icon>
- <n-icon><RefreshOutline /></n-icon>
- </template>
- {{ resetText }}
- </n-button>
- <!-- 额外操作按钮插槽 -->
- <slot name="extra-actions" :form-data="data" />
- </n-space>
- </template>
- </AiForm>
- </div>
- </template>
- <script setup>
- import { RefreshOutline, SearchOutline } from '@vicons/ionicons5'
- import { computed, ref, watch } from 'vue'
- import AiForm from './AiForm.vue'
- const props = defineProps({
- // 表单配置(兼容 options 命名)
- schema: {
- type: Array,
- default: () => [],
- },
- options: {
- type: Array,
- default: () => [],
- },
- // 初始值
- modelValue: {
- type: Object,
- default: () => ({}),
- },
- // 栅格列数
- gridCols: {
- type: Number,
- default: 4,
- },
- // 标签位置
- labelPlacement: {
- type: String,
- default: 'left',
- },
- // 标签宽度
- labelWidth: {
- type: [String, Number],
- default: 'auto',
- },
- // 尺寸
- size: {
- type: String,
- default: 'medium',
- },
- // 是否启用折叠
- enableCollapse: {
- type: Boolean,
- default: true,
- },
- // 最大显示字段数
- maxVisibleFields: {
- type: Number,
- default: 3,
- },
- // 搜索按钮文本
- searchText: {
- type: String,
- default: '搜索',
- },
- // 重置按钮文本
- resetText: {
- type: String,
- default: '重置',
- },
- // 上下文对象
- context: {
- type: Object,
- default: () => ({}),
- },
- // 重置前的钩子函数
- beforeReset: {
- type: Function,
- default: null,
- },
- // 表单项间距
- yGap: {
- type: Number,
- default: 16,
- },
- })
- const emit = defineEmits(['search', 'reset', 'update:modelValue'])
- const formRef = ref(null)
- const formData = ref({ ...props.modelValue })
- const searchLoading = ref(false)
- const resetLoading = ref(false)
- // 同步父组件 modelValue 变化到表单(用于设置初始默认值、重置等场景)
- let isInternalUpdate = false
- watch(() => props.modelValue, (newVal) => {
- if (isInternalUpdate) {
- isInternalUpdate = false
- return
- }
- // 深比较避免无意义更新
- const currentStr = JSON.stringify(formData.value)
- const newStr = JSON.stringify(newVal || {})
- if (currentStr !== newStr) {
- formData.value = { ...newVal }
- }
- }, { deep: true })
- // 兼容 options 和 schema 两种命名
- const schema = computed(() => props.schema.length > 0 ? props.schema : props.options)
- const formContext = computed(() => ({
- ...props.context,
- isSearch: true,
- }))
- /**
- * 搜索
- */
- async function handleSearch() {
- // 防止重复点击
- if (searchLoading.value || resetLoading.value) {
- return
- }
- try {
- searchLoading.value = true
- await formRef.value?.validate()
- isInternalUpdate = true
- emit('search', { ...formData.value })
- emit('update:modelValue', { ...formData.value })
- }
- catch (error) {
- console.warn('表单验证失败:', error)
- }
- finally {
- // 延迟 300ms 再关闭 loading,防止连续点击
- setTimeout(() => {
- searchLoading.value = false
- }, 300)
- }
- }
- /**
- * 重置
- */
- async function handleReset() {
- // 防止重复点击
- if (searchLoading.value || resetLoading.value) {
- return
- }
- try {
- resetLoading.value = true
- // 执行重置前的钩子
- if (props.beforeReset && typeof props.beforeReset === 'function') {
- const result = props.beforeReset(formData.value)
- // 如果是 Promise,等待执行完成
- if (result instanceof Promise) {
- await result
- }
- }
- // 重置表单
- formRef.value?.reset()
- formData.value = {}
- isInternalUpdate = true
- emit('reset')
- emit('update:modelValue', {})
- // 重置后自动搜索
- await handleSearch()
- }
- catch (error) {
- console.error('重置失败:', error)
- }
- finally {
- setTimeout(() => {
- resetLoading.value = false
- }, 300)
- }
- }
- /**
- * 更新单个字段值
- */
- function updateField(name, value) {
- formData.value[name] = value
- }
- /**
- * 获取表单数据
- */
- function getFormData() {
- return { ...formData.value }
- }
- /**
- * 设置表单数据
- */
- function setFormData(data) {
- formData.value = { ...data }
- }
- // 暴露方法
- defineExpose({
- handleSearch,
- handleReset,
- updateField,
- getFormData,
- setFormData,
- validate: () => formRef.value?.validate(),
- reset: () => formRef.value?.reset(),
- })
- </script>
- <style scoped>
- .ai-search-box {
- padding: 8px 12px 0;
- background: var(--bg-primary);
- border-bottom: 1px solid var(--border-light);
- }
- </style>
|