pagetype列表页增加过滤栏
This commit is contained in:
parent
33dd3ffc6e
commit
31430cf6b5
6
.gitignore
vendored
6
.gitignore
vendored
@ -11,17 +11,14 @@ dump.rdb
|
|||||||
*.rdb
|
*.rdb
|
||||||
redis.conf.bak
|
redis.conf.bak
|
||||||
|
|
||||||
# Jingrow Local 前端
|
# Jingrow 前端
|
||||||
node_modules
|
node_modules
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/.env.local
|
frontend/.env.local
|
||||||
frontend/.env.test
|
frontend/.env.test
|
||||||
frontend/.env.production
|
frontend/.env.production
|
||||||
frontend/public/files/
|
|
||||||
|
|
||||||
# Jingrow Framework 前端
|
|
||||||
apps/jingrow/frontend/public/files/
|
|
||||||
|
|
||||||
# 忽略名为 test 的文件夹
|
# 忽略名为 test 的文件夹
|
||||||
test/
|
test/
|
||||||
@ -29,6 +26,7 @@ test/
|
|||||||
|
|
||||||
|
|
||||||
# 忽略所有 文件夹
|
# 忽略所有 文件夹
|
||||||
|
**/frontend/public/files
|
||||||
**/jfile/files/
|
**/jfile/files/
|
||||||
**/output/
|
**/output/
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
|
|||||||
429
apps/jingrow/frontend/src/core/components/FilterBar.vue
Normal file
429
apps/jingrow/frontend/src/core/components/FilterBar.vue
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<div class="elegant-filter-bar">
|
||||||
|
<!-- 过滤条件内容 -->
|
||||||
|
<div class="filter-content">
|
||||||
|
<div v-if="filterableFields.length === 0" class="empty-state">
|
||||||
|
<i class="fa fa-filter"></i>
|
||||||
|
<span>{{ t('暂无可过滤的字段') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="filter-row">
|
||||||
|
<div
|
||||||
|
v-for="field in filterableFields"
|
||||||
|
:key="field.fieldname"
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ 'has-value': filters[field.fieldname] }"
|
||||||
|
>
|
||||||
|
<!-- 文本输入框 -->
|
||||||
|
<div v-if="isTextField(field.fieldtype)" class="filter-input">
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@input="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选择框 -->
|
||||||
|
<div v-else-if="field.fieldtype === 'Select'" class="filter-input">
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:options="getSelectOptions(field)"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@update:value="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 多选框 -->
|
||||||
|
<div v-else-if="isMultiSelectField(field.fieldtype)" class="filter-input">
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:options="getSelectOptions(field)"
|
||||||
|
multiple
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@update:value="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复选框 -->
|
||||||
|
<div v-else-if="field.fieldtype === 'Check'" class="filter-input">
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:options="[
|
||||||
|
{ label: t('是'), value: 1 },
|
||||||
|
{ label: t('否'), value: 0 }
|
||||||
|
]"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@update:value="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数字输入框 -->
|
||||||
|
<div v-else-if="isNumberField(field.fieldtype)" class="filter-input">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@update:value="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<div v-else-if="isDateField(field.fieldtype)" class="filter-input">
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@update:value="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他字段类型使用文本输入 -->
|
||||||
|
<div v-else class="filter-input">
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters[field.fieldname]"
|
||||||
|
:placeholder="getFieldPlaceholder(field)"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
@input="onFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
class="action-btn clear-btn"
|
||||||
|
@click="clearAllFilters"
|
||||||
|
:title="t('清除所有过滤条件')"
|
||||||
|
>
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-btn save-btn"
|
||||||
|
@click="showSaveDialog = true"
|
||||||
|
:title="t('保存当前过滤条件')"
|
||||||
|
>
|
||||||
|
<i class="fa fa-bookmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存过滤条件对话框 -->
|
||||||
|
<n-modal v-model:show="showSaveDialog">
|
||||||
|
<n-card
|
||||||
|
style="width: 400px"
|
||||||
|
:title="t('保存筛选条件')"
|
||||||
|
:bordered="false"
|
||||||
|
size="huge"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<n-form :model="saveForm" ref="saveFormRef">
|
||||||
|
<n-form-item :label="t('过滤器名称')" path="name">
|
||||||
|
<n-input
|
||||||
|
v-model:value="saveForm.name"
|
||||||
|
:placeholder="t('请输入过滤器名称')"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<n-button @click="showSaveDialog = false" size="small">
|
||||||
|
{{ t('取消') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button type="primary" @click="saveFilter" size="small">
|
||||||
|
{{ t('保存') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { NInput, NSelect, NInputNumber, NDatePicker, NModal, NCard, NForm, NFormItem, NButton, useMessage } from 'naive-ui'
|
||||||
|
import { t } from '@/shared/i18n'
|
||||||
|
|
||||||
|
interface FilterField {
|
||||||
|
fieldname: string
|
||||||
|
label: string
|
||||||
|
fieldtype: string
|
||||||
|
options?: string
|
||||||
|
in_list_view?: boolean
|
||||||
|
in_standard_filter?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fields: FilterField[]
|
||||||
|
modelValue: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: Record<string, any>): void
|
||||||
|
(e: 'filter-change', filters: Record<string, any>): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const showSaveDialog = ref(false)
|
||||||
|
const saveForm = ref({ name: '' })
|
||||||
|
const saveFormRef = ref()
|
||||||
|
|
||||||
|
// 过滤条件
|
||||||
|
const filters = ref<Record<string, any>>({ ...props.modelValue })
|
||||||
|
|
||||||
|
// 可过滤的字段:排除分隔符字段,只显示 in_standard_filter 的字段
|
||||||
|
const filterableFields = computed(() => {
|
||||||
|
const isFilterable = (f: FilterField) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype)
|
||||||
|
const filterable = props.fields.filter(isFilterable)
|
||||||
|
|
||||||
|
// 只显示 in_standard_filter 的字段
|
||||||
|
const standardFilterFields = filterable.filter(f => f.in_standard_filter)
|
||||||
|
|
||||||
|
return standardFilterFields.slice(0, 8) // 限制最多8个过滤字段
|
||||||
|
})
|
||||||
|
|
||||||
|
// 活跃过滤条件数量
|
||||||
|
const activeFilterCount = computed(() => {
|
||||||
|
return Object.values(filters.value).filter(value =>
|
||||||
|
value !== null && value !== undefined && value !== '' &&
|
||||||
|
!(Array.isArray(value) && value.length === 0)
|
||||||
|
).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否有活跃的过滤条件
|
||||||
|
const hasActiveFilters = computed(() => activeFilterCount.value > 0)
|
||||||
|
|
||||||
|
// 字段类型判断函数
|
||||||
|
function isTextField(fieldtype: string): boolean {
|
||||||
|
return ['Data', 'Text', 'Long Text', 'Comment'].includes(fieldtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiSelectField(fieldtype: string): boolean {
|
||||||
|
return ['MultiSelect', 'MultiSelect Pills', 'MultiSelect List'].includes(fieldtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumberField(fieldtype: string): boolean {
|
||||||
|
return ['Int', 'Float', 'Currency', 'Percent'].includes(fieldtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDateField(fieldtype: string): boolean {
|
||||||
|
return ['Date', 'Datetime'].includes(fieldtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段占位符文本
|
||||||
|
function getFieldPlaceholder(field: FilterField): string {
|
||||||
|
const label = field.label || field.fieldname
|
||||||
|
return t(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选择框选项
|
||||||
|
function getSelectOptions(field: FilterField) {
|
||||||
|
if (!field.options) return []
|
||||||
|
const options = field.options.split('\n').filter((s: string) => s.trim() !== '')
|
||||||
|
return options.map((opt: string) => ({ label: t(opt), value: opt }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤条件变化
|
||||||
|
function onFilterChange() {
|
||||||
|
emit('update:modelValue', { ...filters.value })
|
||||||
|
emit('filter-change', { ...filters.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有过滤条件
|
||||||
|
function clearAllFilters() {
|
||||||
|
filters.value = {}
|
||||||
|
onFilterChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存过滤条件
|
||||||
|
function saveFilter() {
|
||||||
|
if (!saveForm.value.name.trim()) {
|
||||||
|
message.warning(t('请输入过滤器名称'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以保存到本地存储或发送到服务器
|
||||||
|
const savedFilters = JSON.parse(localStorage.getItem('savedFilters') || '{}')
|
||||||
|
savedFilters[saveForm.value.name] = { ...filters.value }
|
||||||
|
localStorage.setItem('savedFilters', JSON.stringify(savedFilters))
|
||||||
|
|
||||||
|
message.success(t('过滤条件已保存'))
|
||||||
|
showSaveDialog.value = false
|
||||||
|
saveForm.value.name = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听外部传入的过滤条件变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
filters.value = { ...newValue }
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.elegant-filter-bar {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item.has-value {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input :deep(.n-input),
|
||||||
|
.filter-input :deep(.n-select),
|
||||||
|
.filter-input :deep(.n-date-picker),
|
||||||
|
.filter-input :deep(.n-input-number) {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input :deep(.n-input:focus),
|
||||||
|
.filter-input :deep(.n-select:focus),
|
||||||
|
.filter-input :deep(.n-date-picker:focus),
|
||||||
|
.filter-input :deep(.n-input-number:focus) {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #fca5a5;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-left: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保输入框完全显示 */
|
||||||
|
.filter-input :deep(.n-input),
|
||||||
|
.filter-input :deep(.n-select),
|
||||||
|
.filter-input :deep(.n-date-picker),
|
||||||
|
.filter-input :deep(.n-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -11,6 +11,21 @@
|
|||||||
<div class="filters">
|
<div class="filters">
|
||||||
<n-input v-model:value="searchQuery" :placeholder="t('Search')" clearable style="width: 200px" />
|
<n-input v-model:value="searchQuery" :placeholder="t('Search')" clearable style="width: 200px" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 活跃过滤条件标签 -->
|
||||||
|
<div v-if="activeFilterTags.length > 0" class="active-filters">
|
||||||
|
<div class="filter-tags">
|
||||||
|
<span v-for="tag in activeFilterTags" :key="tag.field" class="filter-tag">
|
||||||
|
<span class="tag-content">
|
||||||
|
<span class="tag-label">{{ tag.label }}</span>
|
||||||
|
<span class="tag-separator">:</span>
|
||||||
|
<span class="tag-value">{{ tag.value }}</span>
|
||||||
|
</span>
|
||||||
|
<button @click="removeFilter(tag.field)" class="remove-filter-btn">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button
|
<button
|
||||||
class="toggle-btn"
|
class="toggle-btn"
|
||||||
@ -54,6 +69,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
<!-- 过滤栏 -->
|
||||||
|
<FilterBar
|
||||||
|
v-if="!isSinglePage && metaFields.length > 0"
|
||||||
|
:fields="metaFields"
|
||||||
|
v-model="filters"
|
||||||
|
@filter-change="onFilterChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="loading" class="loading">
|
<div v-if="loading" class="loading">
|
||||||
<i class="fa fa-spinner fa-spin"></i> {{ t('Loading...') }}
|
<i class="fa fa-spinner fa-spin"></i> {{ t('Loading...') }}
|
||||||
</div>
|
</div>
|
||||||
@ -174,6 +197,7 @@ import { get_session_api_headers } from '@/shared/api/auth'
|
|||||||
import { usePageTypeSlug } from '@/shared/utils/slug'
|
import { usePageTypeSlug } from '@/shared/utils/slug'
|
||||||
import { isSinglePageType } from '@/shared/utils/pagetype'
|
import { isSinglePageType } from '@/shared/utils/pagetype'
|
||||||
import SinglePageDetail from './SinglePageDetail.vue'
|
import SinglePageDetail from './SinglePageDetail.vue'
|
||||||
|
import FilterBar from '@/core/components/FilterBar.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -201,6 +225,49 @@ const viewMode = ref<'card' | 'list'>(
|
|||||||
(localStorage.getItem(`genericListViewMode:${entity.value}`) as 'card' | 'list') || 'list'
|
(localStorage.getItem(`genericListViewMode:${entity.value}`) as 'card' | 'list') || 'list'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 过滤条件
|
||||||
|
const filters = ref<Record<string, any>>({})
|
||||||
|
|
||||||
|
// 活跃过滤条件标签
|
||||||
|
const activeFilterTags = computed(() => {
|
||||||
|
const tags: Array<{field: string, label: string, value: string}> = []
|
||||||
|
Object.entries(filters.value).forEach(([fieldName, value]) => {
|
||||||
|
if (value !== null && value !== undefined && value !== '' &&
|
||||||
|
!(Array.isArray(value) && value.length === 0)) {
|
||||||
|
const field = metaFields.value.find(f => f.fieldname === fieldName)
|
||||||
|
const label = field?.label || fieldName
|
||||||
|
let displayValue = value
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
displayValue = value.join(', ')
|
||||||
|
} else if (field?.fieldtype === 'Check') {
|
||||||
|
displayValue = value === 1 ? t('是') : t('否')
|
||||||
|
} else if (field?.options) {
|
||||||
|
// 对于有选项的字段,显示选项标签而不是值
|
||||||
|
const options = field.options.split('\n').filter((s: string) => s.trim() !== '')
|
||||||
|
if (options.includes(value)) {
|
||||||
|
displayValue = t(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.push({ field: fieldName, label, value: displayValue })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tags
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移除单个过滤条件
|
||||||
|
function removeFilter(fieldName: string) {
|
||||||
|
filters.value[fieldName] = null
|
||||||
|
onFilterChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听过滤条件变化
|
||||||
|
function onFilterChange() {
|
||||||
|
page.value = 1 // 重置到第一页
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
const displayColumns = computed(() => {
|
const displayColumns = computed(() => {
|
||||||
// 生成列表列:优先 in_list_view,并排除分隔类
|
// 生成列表列:优先 in_list_view,并排除分隔类
|
||||||
const isDisplayable = (f: any) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype)
|
const isDisplayable = (f: any) => !['Section Break', 'Column Break', 'Tab Break'].includes(f.fieldtype)
|
||||||
@ -243,13 +310,36 @@ async function loadData() {
|
|||||||
const listUrl = `/api/data/${encodeURIComponent(entity.value)}`
|
const listUrl = `/api/data/${encodeURIComponent(entity.value)}`
|
||||||
// 仅请求所需字段,避免接口默认不返回
|
// 仅请求所需字段,避免接口默认不返回
|
||||||
const fieldNames = displayColumns.value.map((c: any) => c.key).filter((k: string) => k && k !== 'actions')
|
const fieldNames = displayColumns.value.map((c: any) => c.key).filter((k: string) => k && k !== 'actions')
|
||||||
|
|
||||||
|
// 构建过滤参数
|
||||||
|
const params: any = {
|
||||||
|
fields: JSON.stringify(fieldNames),
|
||||||
|
limit_start: (page.value - 1) * pageSize.value,
|
||||||
|
limit_page_length: pageSize.value,
|
||||||
|
order_by: 'modified desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加过滤条件
|
||||||
|
const filterConditions: any[] = []
|
||||||
|
Object.entries(filters.value).forEach(([fieldName, value]) => {
|
||||||
|
if (value !== null && value !== undefined && value !== '' &&
|
||||||
|
!(Array.isArray(value) && value.length === 0)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// 多选字段使用 in 操作符
|
||||||
|
filterConditions.push([fieldName, 'in', value])
|
||||||
|
} else {
|
||||||
|
// 单选字段使用 = 操作符
|
||||||
|
filterConditions.push([fieldName, '=', value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filterConditions.length > 0) {
|
||||||
|
params.filters = JSON.stringify(filterConditions)
|
||||||
|
}
|
||||||
|
|
||||||
const res = await axios.get(listUrl, {
|
const res = await axios.get(listUrl, {
|
||||||
params: {
|
params,
|
||||||
fields: JSON.stringify(fieldNames),
|
|
||||||
limit_start: (page.value - 1) * pageSize.value,
|
|
||||||
limit_page_length: pageSize.value,
|
|
||||||
order_by: 'modified desc'
|
|
||||||
},
|
|
||||||
headers: get_session_api_headers(), withCredentials: true
|
headers: get_session_api_headers(), withCredentials: true
|
||||||
})
|
})
|
||||||
rows.value = res.data?.data || []
|
rows.value = res.data?.data || []
|
||||||
@ -294,6 +384,7 @@ watch(() => route.params.entity, async (newEntity, oldEntity) => {
|
|||||||
page.value = 1
|
page.value = 1
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
selectedKeys.value = []
|
selectedKeys.value = []
|
||||||
|
filters.value = {} // 重置过滤条件
|
||||||
// 重新加载元数据和数据
|
// 重新加载元数据和数据
|
||||||
await loadMeta()
|
await loadMeta()
|
||||||
// 只有在非单页模式下才加载列表数据
|
// 只有在非单页模式下才加载列表数据
|
||||||
@ -610,6 +701,74 @@ function formatDisplayValue(value: any, fieldName: string) {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
/* 简洁的活跃过滤条件标签样式 */
|
||||||
|
.active-filters {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-separator {
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-value {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-filter-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-filter-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-filter-btn i {
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -833,5 +833,17 @@
|
|||||||
"Name Z-A": "名称 Z-A",
|
"Name Z-A": "名称 Z-A",
|
||||||
"Most Popular": "最受欢迎",
|
"Most Popular": "最受欢迎",
|
||||||
"Applications": "应用列表",
|
"Applications": "应用列表",
|
||||||
"applications": "个应用"
|
"applications": "个应用",
|
||||||
|
|
||||||
|
"是": "是",
|
||||||
|
"否": "否",
|
||||||
|
"暂无可过滤的字段": "暂无可过滤的字段",
|
||||||
|
"清除所有过滤条件": "清除所有过滤条件",
|
||||||
|
"清除": "清除",
|
||||||
|
"保存当前过滤条件": "保存当前过滤条件",
|
||||||
|
"保存": "保存",
|
||||||
|
"保存筛选条件": "保存筛选条件",
|
||||||
|
"过滤器名称": "过滤器名称",
|
||||||
|
"请输入过滤器名称": "请输入过滤器名称",
|
||||||
|
"取消": "取消"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,13 +46,15 @@
|
|||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Status",
|
"in_standard_filter": 1,
|
||||||
|
"label": "状态",
|
||||||
"options": "\n草稿\n待执行\n进行中\n未完成\n已完成"
|
"options": "\n草稿\n待执行\n进行中\n未完成\n已完成"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "enabled",
|
"fieldname": "enabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "已启用"
|
"label": "已启用"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -114,6 +116,7 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "智能体名称",
|
"label": "智能体名称",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user