自动完成控件优化

This commit is contained in:
Gary Fu
2024-01-06 11:09:43 +08:00
parent 6d1abed421
commit 1a885d942c
9 changed files with 184 additions and 133 deletions

View File

@@ -2,8 +2,9 @@ body, #app .el-header{
padding: 0; padding: 0;
} }
body { html, body {
margin: 0; margin: 0;
--el-border-radius-large: 6px;
} }
html, body, #app, .index-container { html, body, #app, .index-container {
@@ -58,6 +59,10 @@ html, body, #app, .index-container {
text-align: right; text-align: right;
} }
.el-dialog {
--el-dialog-border-radius: var(--el-border-radius-large);
}
.icon-dialog .el-dialog__body { .icon-dialog .el-dialog__body {
padding: 10px; padding: 10px;
} }
@@ -102,7 +107,7 @@ html, body, #app, .index-container {
.common-autocomplete .el-popover__title{ .common-autocomplete .el-popover__title{
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 600;
} }
.common-autocomplete .autocomplete-table .el-table__cell{ .common-autocomplete .autocomplete-table .el-table__cell{

View File

@@ -23,11 +23,11 @@
* @property {boolean} disabled 是否禁用 * @property {boolean} disabled 是否禁用
* @property {boolean} emptySearchEnabled 没有关键字时是否点击就开始搜索 * @property {boolean} emptySearchEnabled 没有关键字时是否点击就开始搜索
* @property {string} title popover标题 * @property {string} title popover标题
* @property {string} autocompleteLabel 自动完成label * @property {string} placeholder placeholder占位符
* @property {string} defaultLabel 自动完成默认label
* @property {string} idKey id字段名 * @property {string} idKey id字段名
* @property {string} labelKey label字段名 * @property {string} labelKey label字段名
* @property {number} debounceTime 防抖时间 * @property {number} debounceTime 防抖时间
* @property {CommonPage} page 分页数据
* @property {string} autocompleteWidth 宽度 * @property {string} autocompleteWidth 宽度
* @property {CommonSelectPageOption} selectPageConfig 分页 * @property {CommonSelectPageOption} selectPageConfig 分页
* @property {Number} colSize 显示几列 * @property {Number} colSize 显示几列
@@ -56,7 +56,11 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
autocompleteLabel: { placeholder: {
type: String,
default: ''
},
defaultLabel: {
type: String, type: String,
default: '' default: ''
}, },
@@ -76,10 +80,6 @@ const props = defineProps({
type: String, type: String,
default: '500px' default: '500px'
}, },
page: {
type: Object,
default: null
},
autocompleteConfig: { autocompleteConfig: {
type: Object, type: Object,
required: true required: true
@@ -122,11 +122,11 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['update:modelValue', 'onSelectData', 'update:page', 'update:autocompleteLabel']) const emit = defineEmits(['update:modelValue', 'onSelectData', 'update:defaultLabel'])
// 关键字搜索 // 关键字搜索
const keywords = ref(props.autocompleteLabel) const keywords = ref(props.defaultLabel)
// 上次搜索记录 // 上次搜索记录
const lastAutocompleteLabel = ref(props.autocompleteLabel) const lastAutocompleteLabel = ref(props.defaultLabel)
// 分页条 // 分页条
const pageAttrs = { layout: 'total, prev, pager, next', small: true, background: true } const pageAttrs = { layout: 'total, prev, pager, next', small: true, background: true }
const selectPageAttrs = { layout: 'prev, pager, next', small: true, background: true } const selectPageAttrs = { layout: 'prev, pager, next', small: true, background: true }
@@ -137,9 +137,15 @@ const selectPageData = ref({})
const selectPageTab = ref(null) const selectPageTab = ref(null)
const popoverVisible = ref(false) const popoverVisible = ref(false)
const autocompletePopover = ref() const autocompletePopover = ref()
const pageConfig = useVModel(props, 'page', emit) const autoPage = ref({
pageSize: 8,
pageNumber: 1
})
const loadingData = ref(false) const loadingData = ref(false)
const idProp = computed(() => props.autocompleteConfig?.idKey || props.idKey)
const labelProp = computed(() => props.autocompleteConfig?.labelKey || props.labelKey)
const showSelectPage = computed(() => { const showSelectPage = computed(() => {
return props.selectPageConfig && (!keywords.value || lastAutocompleteLabel.value === keywords.value) return props.selectPageConfig && (!keywords.value || lastAutocompleteLabel.value === keywords.value)
}) })
@@ -148,10 +154,10 @@ const loadAutoDataList = (val) => {
if (val || props.emptySearchEnabled) { if (val || props.emptySearchEnabled) {
popoverVisible.value = true popoverVisible.value = true
loadingData.value = true loadingData.value = true
props.autocompleteConfig.searchMethod(val, (result) => { props.autocompleteConfig.searchMethod({ query: val, page: autoPage.value }, (result) => {
dataList.value = result.items || [] dataList.value = result.items || []
if (props.page) { if (result.page) {
pageConfig.value = { ...result.page } autoPage.value = { ...result.page }
} }
loadingData.value = false loadingData.value = false
}) })
@@ -180,8 +186,8 @@ const onInputKeywords = debounce((input) => {
popoverVisible.value = true popoverVisible.value = true
loadSelectData() loadSelectData()
} else { } else {
if (input && pageConfig.value) { if (input && autoPage.value) {
pageConfig.value = { ...pageConfig.value, pageNumber: 1 } autoPage.value = { ...autoPage.value, pageNumber: 1 }
} }
loadAutoDataList(val) loadAutoDataList(val)
} }
@@ -211,7 +217,7 @@ watch(() => popoverVisible.value, (val) => {
//* ********************数据选择********************* //* ********************数据选择*********************
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
const vAutocompleteLabel = useVModel(props, 'autocompleteLabel', emit) const vAutocompleteLabel = useVModel(props, 'defaultLabel', emit)
const onSelectData = (row) => { const onSelectData = (row) => {
popoverVisible.value = false popoverVisible.value = false
@@ -222,8 +228,8 @@ const onSelectData = (row) => {
let label = '' let label = ''
let value = null let value = null
if (row) { if (row) {
label = row[props.labelKey] label = row[labelProp.value]
value = props.useIdModel ? row[props.idKey] : row value = props.useIdModel ? row[idProp.value] : row
} }
keywords.value = label keywords.value = label
vAutocompleteLabel.value = label vAutocompleteLabel.value = label
@@ -317,13 +323,13 @@ const selectPagePaginationChange = (tab, pageNumber) => {
popper-class="common-autocomplete" popper-class="common-autocomplete"
placement="bottom-start" placement="bottom-start"
:width="autocompleteWidth" :width="autocompleteWidth"
:title="title" :title="title||placeholder"
> >
<template #reference> <template #reference>
<el-input <el-input
v-model="keywords" v-model="keywords"
:clearable="clearable" :clearable="clearable"
:placeholder="title" :placeholder="placeholder||title"
:disabled="disabled" :disabled="disabled"
:readonly="readonly" :readonly="readonly"
v-bind="inputAttrs" v-bind="inputAttrs"
@@ -369,7 +375,7 @@ const selectPagePaginationChange = (tab, pageNumber) => {
class="common-select-page-btn is-text" class="common-select-page-btn is-text"
@click="onSelectData(colData)" @click="onSelectData(colData)"
> >
{{ colData[labelKey] }} {{ colData[labelProp] }}
</el-button> </el-button>
</el-col> </el-col>
</el-row> </el-row>
@@ -392,7 +398,7 @@ const selectPagePaginationChange = (tab, pageNumber) => {
<common-table <common-table
v-else v-else
ref="tableRef" ref="tableRef"
v-model:page="pageConfig" v-model:page="autoPage"
:loading="loadingData" :loading="loadingData"
:loading-text="loadingText" :loading-text="loadingText"
class="autocomplete-table" class="autocomplete-table"

View File

@@ -42,8 +42,8 @@ const props = defineProps({
}) })
const inputType = computed(() => { const inputType = computed(() => {
if (props.option.common) { if (props.option.type && props.option.type.startsWith('common-')) {
return `common-${props.option.type}` return `${props.option.type}` // 自定义控件
} }
return `el-${props.option.type || 'input'}` return `el-${props.option.type || 'input'}`
}) })

View File

@@ -1,7 +1,8 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { ref, watch } from 'vue'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import { $i18nBundle } from '@/messages' import { $i18nBundle } from '@/messages'
import { useVModel } from '@vueuse/core'
/** /**
* @typedef {FormProps} CommonFormProps * @typedef {FormProps} CommonFormProps
@@ -64,7 +65,7 @@ const props = defineProps({
} }
}) })
const rules = computed(() => { const initRules = () => {
const ruleResult = {} const ruleResult = {}
props.options.forEach(option => { props.options.forEach(option => {
if (option.prop) { if (option.prop) {
@@ -88,11 +89,26 @@ const rules = computed(() => {
} }
} }
}) })
console.info('==============rules', ruleResult)
return ruleResult return ruleResult
}) }
defineEmits(['submitForm']) const rules = ref({})
const emit = defineEmits(['submitForm', 'update:model'])
const formModel = useVModel(props, 'model', emit)
watch(() => props.options, (options) => {
console.info('=================options', options)
options.forEach(option => {
if (formModel.value && option.value) {
formModel.value[option.prop] = option.value
}
})
rules.value = initRules()
}, { deep: true })
//= ============form暴露============//
const form = ref() const form = ref()
@@ -105,7 +121,7 @@ defineExpose({
<template> <template>
<el-form <el-form
ref="form" ref="form"
:model="model" :model="formModel"
:rules="rules" :rules="rules"
:label-width="labelWidth" :label-width="labelWidth"
v-bind="$attrs" v-bind="$attrs"
@@ -113,7 +129,7 @@ defineExpose({
<common-form-control <common-form-control
v-for="(option,index) in options" v-for="(option,index) in options"
:key="index" :key="index"
:model="model" :model="formModel"
:option="option" :option="option"
/> />
<el-form-item v-if="showButtons"> <el-form-item v-if="showButtons">

View File

@@ -63,16 +63,16 @@ const selectIcon = icon => {
<template> <template>
<label class="el-radio"> <label class="el-radio">
<common-icon <common-icon
v-if="modelValue" v-if="vModel"
:icon="modelValue" :icon="vModel"
class="el-radio__input" class="el-radio__input"
/> />
<span <span
v-if="modelValue" v-if="vModel"
class="el-radio__label" class="el-radio__label"
>{{ modelValue }}</span> >{{ vModel }}</span>
<el-button <el-button
class="icon-select-button" :class="{'icon-select-button': !!vModel}"
type="primary" type="primary"
:disabled="disabled||readonly" :disabled="disabled||readonly"
size="small" size="small"

View File

@@ -1,4 +1,5 @@
import { $httpPost } from '@/vendors/axios' import { $httpPost } from '@/vendors/axios'
import { $i18nMsg } from '@/messages'
/** /**
* @typedef {Object} CityDto * @typedef {Object} CityDto
@@ -20,3 +21,65 @@ export const loadSelectCities = (query, config) => {
export const loadAutoCities = (data, config) => { export const loadAutoCities = (data, config) => {
return $httpPost('/city/autoCities', data, config) return $httpPost('/city/autoCities', data, config)
} }
/**
* 城市自动完成配置
*/
export const useCityAutocompleteConfig = () => {
return {
idKey: 'code',
labelKey: $i18nMsg('nameCn', 'nameEn'),
columns: [{
label: $i18nMsg('代码', 'Code'),
property: 'code'
}, {
label: $i18nMsg('中文名', 'CN Name'),
property: 'nameCn'
}, {
label: $i18nMsg('英文名', 'EN Name'),
property: 'nameEn'
}],
searchMethod ({ query, page }, cb) {
loadAutoCities({ page }) // {query, page}
.then(result => {
const data = {
page: result.resultData.page,
items: result.resultData.cityList
}
cb(data)
})
}
}
}
/**
* 程序可选项配置
*/
export const useCitySelectPageConfig = () => {
return {
tabs: [{
label: $i18nMsg('热门', 'Hot'),
id: '0'
}, {
label: 'A-F',
id: 'A-F'
}, {
label: 'G-J',
id: 'G-J'
}, {
label: 'K-N',
id: 'K-N'
}, {
label: 'P-W',
id: 'P-W'
}, {
label: 'X-Z',
id: 'X-Z'
}],
searchMethod (id, cb) {
loadSelectCities({ id })
.then(result => {
console.info('================selectCities', result)
cb(result.resultData.cityList)
})
}
}
}

View File

@@ -8,6 +8,7 @@
* @property {Date} birthday * @property {Date} birthday
*/ */
import { $httpGet, $httpPost } from '@/vendors/axios' import { $httpGet, $httpPost } from '@/vendors/axios'
import { $i18nMsg } from '@/messages'
/** /**
* 加载用户数据 * 加载用户数据
@@ -74,3 +75,33 @@ export const useUserFormOptions = () => {
} }
}] }]
} }
/**
* 用户自动完成配置
*/
export const useUserAutocompleteConfig = () => {
return {
columns: [{
label: $i18nMsg('姓名', 'Name'),
property: $i18nMsg('nameCn', 'nameEn')
}, {
label: $i18nMsg('性别', 'Gender'),
property: 'gender',
slot: 'gender'
}, {
label: $i18nMsg('地址', 'Address'),
property: 'address',
width: '300px'
}],
searchMethod ({ query, page }, cb) {
loadUsersResult({ page })
.then(result => {
const data = {
page: result.resultData.page,
items: result.resultData.userList
}
cb(data)
})
}
}
}

View File

@@ -1,10 +1,10 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/city/CityService'
/** /**
* @type {[CommonFormOption]} * @type {[CommonFormOption]}
*/ */
const formOptions = [{ const formOptions = ref([{
label: '用户名', label: '用户名',
prop: 'userName', prop: 'userName',
value: '', value: '',
@@ -95,12 +95,23 @@ const formOptions = [{
} }
] ]
}, { }, {
label: '图标测试', label: '图标',
prop: 'icon', prop: 'icon',
value: '', value: '',
type: 'icon-select', type: 'common-icon-select',
required: true
}, {
label: '城市',
prop: 'city',
value: 'SHA',
type: 'common-autocomplete',
required: true, required: true,
common: true placeholder: '请选择城市',
attrs: {
defaultLabel: '上海市',
autocompleteConfig: useCityAutocompleteConfig(),
selectPageConfig: useCitySelectPageConfig()
}
}, { }, {
label: '地址', label: '地址',
prop: 'address', prop: 'address',
@@ -108,7 +119,7 @@ const formOptions = [{
attrs: { attrs: {
type: 'textarea' type: 'textarea'
} }
}] }])
const userDto = ref({ const userDto = ref({
userName: '', userName: '',
userPassword: '' userPassword: ''

View File

@@ -1,96 +1,17 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useDefaultPage } from '@/config' import { useUserAutocompleteConfig } from '@/services/user/UserService'
import { loadUsersResult } from '@/services/user/UserService'
import { $i18nMsg } from '@/messages' import { $i18nMsg } from '@/messages'
import { loadAutoCities, loadSelectCities } from '@/services/city/CityService' import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/city/CityService'
const modelIcon = ref('Apple') const modelIcon = ref('Apple')
const modelAuto = ref('99999') const modelAuto = ref('99999')
const modelAutoLabel = ref('Gary Fu') const modelAutoLabel = ref('Gary Fu')
const autoPage = ref(useDefaultPage(8)) const userAutocompleteConfig = computed(() => useUserAutocompleteConfig())
// 城市控件
const cityModel = ref('') const cityModel = ref('')
const autoCityPage = ref(useDefaultPage(8)) const cityAutocompleteConfig = computed(() => useCityAutocompleteConfig())
const userAutocompleteConfig = computed(() => { const citySelectPageConfig = computed(() => useCitySelectPageConfig())
return {
columns: [{
label: $i18nMsg('姓名', 'Name'),
property: $i18nMsg('nameCn', 'nameEn')
}, {
label: $i18nMsg('性别', 'Gender'),
property: 'gender',
slot: 'gender'
}, {
label: $i18nMsg('地址', 'Address'),
property: 'address',
width: '300px'
}],
searchMethod (query, cb) {
loadUsersResult({ page: autoPage.value })
.then(result => {
const data = {
page: result.resultData.page,
items: result.resultData.userList
}
cb(data)
})
}
}
})
const cityAutocompleteConfig = computed(() => {
return {
columns: [{
label: $i18nMsg('代码', 'Code'),
property: 'code'
}, {
label: $i18nMsg('中文名', 'CN Name'),
property: 'nameCn'
}, {
label: $i18nMsg('英文名', 'EN Name'),
property: 'nameEn'
}],
searchMethod (query, cb) {
loadAutoCities({ page: autoCityPage.value })
.then(result => {
const data = {
page: result.resultData.page,
items: result.resultData.cityList
}
cb(data)
})
}
}
})
const citySelectPageConfig = computed(() => {
return {
tabs: [{
label: $i18nMsg('热门', 'Hot'),
id: '0'
}, {
label: 'A-F',
id: 'A-F'
}, {
label: 'G-J',
id: 'G-J'
}, {
label: 'K-N',
id: 'K-N'
}, {
label: 'P-W',
id: 'P-W'
}, {
label: 'X-Z',
id: 'X-Z'
}],
searchMethod (id, cb) {
loadSelectCities({ id })
.then(result => {
console.info('================selectCities', result)
cb(result.resultData.cityList)
})
}
}
})
</script> </script>
<template> <template>
@@ -103,8 +24,7 @@ const citySelectPageConfig = computed(() => {
<el-form-item label="用户"> <el-form-item label="用户">
<common-autocomplete <common-autocomplete
v-model="modelAuto" v-model="modelAuto"
v-model:autocomplete-label="modelAutoLabel" v-model:default-label="modelAutoLabel"
v-model:page="autoPage"
id-key="id" id-key="id"
:label-key="$i18nMsg('nameCn', 'nameEn')" :label-key="$i18nMsg('nameCn', 'nameEn')"
:empty-search-enabled="false" :empty-search-enabled="false"
@@ -123,7 +43,6 @@ const citySelectPageConfig = computed(() => {
<el-form-item label="出发城市"> <el-form-item label="出发城市">
<common-autocomplete <common-autocomplete
v-model="cityModel" v-model="cityModel"
v-model:page="autoCityPage"
id-key="code" id-key="code"
:label-key="$i18nMsg('nameCn', 'nameEn')" :label-key="$i18nMsg('nameCn', 'nameEn')"
:empty-search-enabled="false" :empty-search-enabled="false"