优化过滤栏Link字段类型搜索方式
This commit is contained in:
parent
87666e5945
commit
bca0955225
@ -95,6 +95,50 @@
|
||||
/>
|
||||
</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">
|
||||
<n-input
|
||||
@ -165,9 +209,12 @@
|
||||
</template>
|
||||
|
||||
<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 { 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 {
|
||||
fieldname: string
|
||||
@ -199,6 +246,19 @@ const saveFormRef = ref()
|
||||
// 过滤条件
|
||||
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 的字段
|
||||
const filterableFields = computed(() => {
|
||||
const isFilterable = (f: FilterField) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype)
|
||||
@ -288,10 +348,297 @@ function saveFilter() {
|
||||
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
|
||||
}
|
||||
|
||||
// 异步加载Link字段的显示值(title_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) => {
|
||||
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 })
|
||||
|
||||
// 组件挂载后,加载已有Link字段的显示值
|
||||
watch(() => filterableFields.value, (fields) => {
|
||||
fields.forEach(field => {
|
||||
if (field.fieldtype === 'Link' && field.options && filters.value[field.fieldname]) {
|
||||
loadLinkDisplayValue(field)
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -441,4 +788,70 @@ watch(() => props.modelValue, (newValue) => {
|
||||
.filter-input :deep(.n-input-number) {
|
||||
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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user