优化过滤栏Link字段类型搜索方式
This commit is contained in:
parent
87666e5945
commit
bca0955225
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步加载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) => {
|
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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user