优化过滤栏Link字段类型搜索方式

This commit is contained in:
jingrow 2025-11-01 18:11:02 +08:00
parent 87666e5945
commit bca0955225

View File

@ -95,6 +95,50 @@
/> />
</div> </div>
<!-- Link字段类型支持搜索和下拉选择 -->
<div v-else-if="field.fieldtype === 'Link' && field.options" class="filter-input link-filter-input">
<div class="link-filter-container" :ref="(el) => setLinkContainerRef(field.fieldname, el)">
<n-input
:value="getLinkDisplayValueSync(field)"
:placeholder="getFieldPlaceholder(field)"
clearable
size="small"
@update:value="(val) => handleLinkInputChange(field, val)"
@click="() => handleLinkInputClick(field)"
@focus="() => handleLinkInputClick(field)"
@clear="() => handleLinkClear(field)"
/>
<!-- 下拉选项 -->
<div
v-if="linkDropdownStates[field.fieldname]?.show"
class="link-dropdown-menu"
>
<div class="link-dropdown-content">
<div
v-for="option in linkDropdownStates[field.fieldname]?.options || []"
:key="option.value"
class="link-dropdown-item"
:class="{ 'create-item': option.type === 'create' }"
@click="() => handleLinkOptionSelect(field, option)"
>
<span v-if="option.type === 'create'" class="create-icon">
<i class="fa fa-plus"></i>
</span>
<span class="item-label">{{ option.label }}</span>
</div>
<div v-if="(linkDropdownStates[field.fieldname]?.options || []).length === 0 && !linkDropdownStates[field.fieldname]?.loading" class="link-empty-state">
{{ t('No Data') }}
</div>
<div v-if="linkDropdownStates[field.fieldname]?.loading" class="link-loading-state">
{{ t('Loading') }}
</div>
</div>
</div>
</div>
</div>
<!-- 其他字段类型使用文本输入 --> <!-- 其他字段类型使用文本输入 -->
<div v-else class="filter-input"> <div v-else class="filter-input">
<n-input <n-input
@ -165,9 +209,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { NInput, NSelect, NInputNumber, NDatePicker, NModal, NCard, NForm, NFormItem, NButton, useMessage } from 'naive-ui' import { NInput, NSelect, NInputNumber, NDatePicker, NModal, NCard, NForm, NFormItem, NButton, useMessage } from 'naive-ui'
import { t } from '@/shared/i18n' import { t } from '@/shared/i18n'
import axios from 'axios'
import { get_session_api_headers } from '@/shared/api/auth'
import { getRecords } from '@/shared/api/common'
interface FilterField { interface FilterField {
fieldname: string fieldname: string
@ -199,6 +246,19 @@ const saveFormRef = ref()
// //
const filters = ref<Record<string, any>>({ ...props.modelValue }) const filters = ref<Record<string, any>>({ ...props.modelValue })
// Link
interface LinkDropdownState {
show: boolean
options: Array<{ label: string; value: string; title?: string; type?: string }>
loading: boolean
searchQuery: string
displayValue: string
pageTypeConfig: any
}
const linkDropdownStates = ref<Record<string, LinkDropdownState>>({})
const linkContainerRefs = ref<Record<string, HTMLElement | null>>({})
// in_standard_filter // in_standard_filter
const filterableFields = computed(() => { const filterableFields = computed(() => {
const isFilterable = (f: FilterField) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype) const isFilterable = (f: FilterField) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype)
@ -288,10 +348,297 @@ function saveFilter() {
saveForm.value.name = '' saveForm.value.name = ''
} }
// Link
function initLinkDropdownState(fieldName: string) {
if (!linkDropdownStates.value[fieldName]) {
linkDropdownStates.value[fieldName] = {
show: false,
options: [],
loading: false,
searchQuery: '',
displayValue: '',
pageTypeConfig: {}
}
}
}
// Link
function setLinkContainerRef(fieldName: string, el: any) {
if (el) {
linkContainerRefs.value[fieldName] = el as HTMLElement
}
}
// Link
function getLinkDisplayValueSync(field: FilterField): string {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
const currentValue = filters.value[fieldName]
if (!currentValue) {
return linkDropdownStates.value[fieldName].searchQuery || ''
}
//
if (linkDropdownStates.value[fieldName].displayValue &&
linkDropdownStates.value[fieldName].displayValue !== currentValue) {
return linkDropdownStates.value[fieldName].displayValue
}
//
return currentValue
}
// Linktitle_field
async function loadLinkDisplayValue(field: FilterField) {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
const currentValue = filters.value[fieldName]
if (!currentValue || !field.options) {
return
}
//
if (linkDropdownStates.value[fieldName].displayValue &&
linkDropdownStates.value[fieldName].displayValue !== currentValue) {
return
}
try {
const config = await getLinkPageTypeConfig(field.options)
const titleField = config.title_field || 'name'
if (titleField === 'name') {
linkDropdownStates.value[fieldName].displayValue = currentValue
return
}
// title_field
const response = await axios.get(`/api/data/${encodeURIComponent(field.options)}/${encodeURIComponent(currentValue)}`, {
headers: get_session_api_headers(),
withCredentials: true
})
const recordData = response.data?.data || {}
const titleValue = recordData[titleField] || currentValue
linkDropdownStates.value[fieldName].displayValue = titleValue
} catch (error) {
//
console.warn('加载Link字段显示值失败:', error)
}
}
// Link
async function getLinkPageTypeConfig(pagetype: string) {
const fieldName = `_config_${pagetype}`
if (!linkDropdownStates.value[fieldName]) {
linkDropdownStates.value[fieldName] = {
show: false,
options: [],
loading: false,
searchQuery: '',
displayValue: '',
pageTypeConfig: {}
}
}
if (Object.keys(linkDropdownStates.value[fieldName].pageTypeConfig).length > 0) {
return linkDropdownStates.value[fieldName].pageTypeConfig
}
try {
const response = await axios.get(`/api/data/PageType/${encodeURIComponent(pagetype)}`, {
headers: get_session_api_headers(),
withCredentials: true
})
const config = response.data?.data || {}
linkDropdownStates.value[fieldName].pageTypeConfig = config
return config
} catch (error) {
console.error('获取页面类型配置失败:', error)
return {}
}
}
// Link
async function searchLinkOptions(field: FilterField, query: string = '') {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
if (!field.options) return
linkDropdownStates.value[fieldName].loading = true
try {
const config = await getLinkPageTypeConfig(field.options)
const titleField = config.title_field || 'name'
//
const fields = ['name']
if (titleField !== 'name') {
fields.push(titleField)
}
//
const searchFilters: any[] = []
if (query.trim()) {
//
if (titleField !== 'name') {
searchFilters.push([titleField, 'like', `%${query}%`])
} else {
searchFilters.push(['name', 'like', `%${query}%`])
}
}
// 使API
const result = await getRecords(field.options, searchFilters, fields, 'modified desc', 0, 20)
if (result.success && result.data) {
//
const processedRecords = result.data.map((item: any) => {
const title = titleField === 'name' ? item.name : (item[titleField] || item.name)
return {
label: title,
value: item.name,
title: title
}
})
linkDropdownStates.value[fieldName].options = processedRecords
} else {
linkDropdownStates.value[fieldName].options = []
}
} catch (error) {
console.error('搜索Link选项失败:', error)
linkDropdownStates.value[fieldName].options = []
} finally {
linkDropdownStates.value[fieldName].loading = false
}
}
// Link
function handleLinkInputChange(field: FilterField, value: string) {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
linkDropdownStates.value[fieldName].searchQuery = value || ''
//
if (linkDropdownStates.value[fieldName].show) {
searchLinkOptions(field, value)
}
//
if (!value) {
filters.value[fieldName] = ''
onFilterChange()
}
}
// Link/
async function handleLinkInputClick(field: FilterField) {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
//
if (linkDropdownStates.value[fieldName].show) return
//
await searchLinkOptions(field, linkDropdownStates.value[fieldName].searchQuery)
linkDropdownStates.value[fieldName].show = true
}
// Link
function handleLinkOptionSelect(field: FilterField, option: any) {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
if (option.type === 'create') {
//
linkDropdownStates.value[fieldName].show = false
return
}
filters.value[fieldName] = option.value
linkDropdownStates.value[fieldName].displayValue = option.title || option.label
linkDropdownStates.value[fieldName].searchQuery = ''
linkDropdownStates.value[fieldName].show = false
onFilterChange()
}
// Link
function handleLinkClear(field: FilterField) {
const fieldName = field.fieldname
initLinkDropdownState(fieldName)
filters.value[fieldName] = ''
linkDropdownStates.value[fieldName].displayValue = ''
linkDropdownStates.value[fieldName].searchQuery = ''
linkDropdownStates.value[fieldName].show = false
onFilterChange()
}
//
function handleClickOutside(event: Event) {
const target = event.target as HTMLElement
Object.keys(linkDropdownStates.value).forEach(fieldName => {
const container = linkContainerRefs.value[fieldName]
if (container && !container.contains(target)) {
linkDropdownStates.value[fieldName].show = false
}
})
}
//
onMounted(() => {
document.addEventListener('click', handleClickOutside)
// Link
props.fields.forEach(field => {
if (field.fieldtype === 'Link' && field.options) {
initLinkDropdownState(field.fieldname)
}
})
})
//
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// //
watch(() => props.modelValue, (newValue) => { watch(() => props.modelValue, (newValue) => {
filters.value = { ...newValue } filters.value = { ...newValue }
// Link
props.fields.forEach(field => {
if (field.fieldtype === 'Link' && field.options && newValue[field.fieldname]) {
initLinkDropdownState(field.fieldname)
//
linkDropdownStates.value[field.fieldname].displayValue = ''
// title_field
loadLinkDisplayValue(field)
} else if (field.fieldtype === 'Link' && field.options && !newValue[field.fieldname]) {
//
initLinkDropdownState(field.fieldname)
linkDropdownStates.value[field.fieldname].displayValue = ''
linkDropdownStates.value[field.fieldname].searchQuery = ''
}
})
}, { deep: true }) }, { deep: true })
// Link
watch(() => filterableFields.value, (fields) => {
fields.forEach(field => {
if (field.fieldtype === 'Link' && field.options && filters.value[field.fieldname]) {
loadLinkDisplayValue(field)
}
})
}, { immediate: true })
</script> </script>
<style scoped> <style scoped>
@ -441,4 +788,70 @@ watch(() => props.modelValue, (newValue) => {
.filter-input :deep(.n-input-number) { .filter-input :deep(.n-input-number) {
width: 100%; width: 100%;
} }
/* Link字段过滤样式 */
.link-filter-input {
position: relative;
}
.link-filter-container {
position: relative;
width: 100%;
}
.link-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
}
.link-dropdown-content {
background: white;
border: 1px solid #e0e0e6;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
}
.link-dropdown-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #f0f0f0;
}
.link-dropdown-item:last-child {
border-bottom: none;
}
.link-dropdown-item:hover {
background-color: #f5f5f5;
}
.link-dropdown-item.create-item {
color: #18a058;
font-weight: 500;
}
.create-icon {
color: #18a058;
}
.item-label {
flex: 1;
}
.link-empty-state,
.link-loading-state {
padding: 12px;
text-align: center;
color: #999;
font-size: 14px;
}
</style> </style>