筛选增加多选功能

This commit is contained in:
yml2213
2026-07-11 23:21:54 +08:00
parent 0958991847
commit f81842d46e
9 changed files with 165 additions and 65 deletions
@@ -79,7 +79,6 @@ function closePopover() {
:options="publishOptions.insurance_options"
:popover-props="filterPopoverProps('insurance')"
@update:model-value="updateFilter('insurance', $event)"
@close="closePopover"
/>
<StringFilter
@@ -89,7 +88,6 @@ function closePopover() {
:options="publishOptions.level_options"
:popover-props="filterPopoverProps('stamina')"
@update:model-value="updateFilter('stamina', $event)"
@close="closePopover"
/>
<StringFilter
@@ -99,7 +97,6 @@ function closePopover() {
:options="publishOptions.level_options"
:popover-props="filterPopoverProps('load')"
@update:model-value="updateFilter('load', $event)"
@close="closePopover"
/>
<StringFilter
@@ -109,7 +106,6 @@ function closePopover() {
:options="loginMethodOptions"
:popover-props="filterPopoverProps('loginMethod')"
@update:model-value="updateFilter('loginMethod', $event)"
@close="closePopover"
/>
<StringFilter
@@ -120,7 +116,6 @@ function closePopover() {
:popover-props="filterPopoverProps('region')"
:wide="true"
@update:model-value="updateFilter('region', $event)"
@close="closePopover"
/>
<RangeFilter
@@ -197,7 +192,6 @@ function closePopover() {
:options="publishOptions.rank_options"
:popover-props="filterPopoverProps('rank')"
@update:model-value="updateFilter('rank', $event)"
@close="closePopover"
/>
<RangeFilter
@@ -6,30 +6,50 @@ import { computed } from 'vue'
interface Props {
label: string
placeholder: string
modelValue: string
modelValue: string[]
options: string[]
popoverProps: Record<string, any>
wide?: boolean
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
})
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:modelValue': [value: string[]]
close: []
}>()
const isActive = computed(() => {
return !!props.modelValue
})
const selectedValues = computed(() => props.modelValue.filter(Boolean))
const isActive = computed(() => selectedValues.value.length > 0)
const displayLabel = computed(() => {
return props.modelValue || props.placeholder
const values = selectedValues.value
if (!values.length) return props.placeholder
if (values.length === 1) return values[0]
if (values.length === 2) return values.join('、')
return `${values[0]}${values.length}`
})
function selectOption(value: string) {
emit('update:modelValue', value)
emit('close')
function isSelected(value: string) {
return selectedValues.value.includes(value)
}
function toggleOption(value: string) {
if (!value) {
emit('update:modelValue', [])
return
}
if (isSelected(value)) {
emit(
'update:modelValue',
selectedValues.value.filter(item => item !== value)
)
return
}
emit('update:modelValue', [...selectedValues.value, value])
}
</script>
@@ -42,16 +62,17 @@ function selectOption(value: string) {
</button>
</template>
<div class="filter-menu">
<button type="button" :class="{ active: !modelValue }" @click="selectOption('')">
{{ placeholder }}
<button type="button" :class="{ active: !selectedValues.length }" @click="toggleOption('')">
全部
</button>
<button
v-for="item in options"
:key="item"
type="button"
:class="{ active: modelValue === item }"
@click="selectOption(item)"
:class="{ active: isSelected(item) }"
@click="toggleOption(item)"
>
<span class="filter-option-check" :class="{ checked: isSelected(item) }" aria-hidden="true" />
{{ item }}
</button>
</div>
@@ -8,6 +8,8 @@ import {
isSameQuery,
mergeHomeQuery,
readStoredHomeQuery,
joinQueryCSV,
readQueryCSV,
readQueryNumber,
readQueryString,
storeHomeQuery,
@@ -26,6 +28,14 @@ describe('useHomeFilterQuery', () => {
expect(readQueryNumber(query, 'max_coin')).toBeUndefined()
})
it('应该读写 CSV 多选筛选参数', () => {
expect(readQueryCSV({ insurance: '3*3,2*2,3*3' }, 'insurance')).toEqual(['3*3', '2*2'])
expect(readQueryCSV({ insurance: '3*3' }, 'insurance')).toEqual(['3*3'])
expect(readQueryCSV({}, 'insurance')).toEqual([])
expect(joinQueryCSV(['3*3', '2*2', '3*3'])).toBe('3*3,2*2')
expect(joinQueryCSV([])).toBeUndefined()
})
it('应该合并新筛选并移除旧的托管参数', () => {
const query = mergeHomeQuery(
{ keyword: '旧关键词', page: '2', region: '上海' },
@@ -11,6 +11,8 @@ describe('useHomeFilters', () => {
const { filters } = useHomeFilters(publishOptions, listings)
expect(filters.keyword).toBe('')
expect(filters.region).toEqual([])
expect(filters.insurance).toEqual([])
expect(filters.minCoin).toBeUndefined()
expect(filters.maxCoin).toBeUndefined()
})
@@ -23,24 +25,42 @@ describe('useHomeFilters', () => {
filters.keyword = '测试'
filters.minCoin = 100
filters.region = '北京'
filters.region = ['北京', '上海']
filters.insurance = ['3*3', '2*2']
resetFilters()
expect(filters.keyword).toBe('')
expect(filters.minCoin).toBeUndefined()
expect(filters.region).toBe('')
expect(filters.region).toEqual([])
expect(filters.insurance).toEqual([])
})
it('应该正确设置字符串筛选器', () => {
it('应该正确设置字符串多选筛选器', () => {
const publishOptions = ref(emptyListingPublishOptions)
const listings = ref([])
const { filters, setStringFilter } = useHomeFilters(publishOptions, listings)
setStringFilter('region', '上海')
expect(filters.region).toEqual(['上海'])
expect(filters.region).toBe('上海')
setStringFilter('region', ['北京', '上海', '上海'])
expect(filters.region).toEqual(['北京', '上海'])
})
it('应该支持切换字符串筛选项', () => {
const publishOptions = ref(emptyListingPublishOptions)
const listings = ref([])
const { filters, toggleStringFilter } = useHomeFilters(publishOptions, listings)
toggleStringFilter('insurance', '3*3')
toggleStringFilter('insurance', '2*2')
expect(filters.insurance).toEqual(['3*3', '2*2'])
toggleStringFilter('insurance', '3*3')
expect(filters.insurance).toEqual(['2*2'])
})
it('应该正确设置范围筛选器', () => {
@@ -48,6 +48,20 @@ export function readQueryNumber(query: LocationQuery | LocationQueryRaw, key: st
return Number.isFinite(number) ? number : undefined
}
/** 读取 CSV 多选筛选参数,兼容旧版单值与逗号分隔多值。 */
export function readQueryCSV(query: LocationQuery | LocationQueryRaw, key: string): string[] {
const raw = readQueryString(query, key)
if (!raw) return []
return [...new Set(raw.split(',').map(item => item.trim()).filter(Boolean))]
}
/** 将多选数组序列化为 CSV;空数组返回 undefined 以便从 query 中省略。 */
export function joinQueryCSV(values: string[] | undefined | null): string | undefined {
if (!values?.length) return undefined
const joined = [...new Set(values.map(item => item.trim()).filter(Boolean))].join(',')
return joined || undefined
}
export function mergeHomeQuery(
currentQuery: LocationQuery,
managedKeys: readonly string[],
@@ -23,12 +23,12 @@ export type StringFilterKey = 'insurance' | 'stamina' | 'load' | 'region' | 'ran
export interface HomeFilters {
keyword: string
server: string
region: string
loginMethod: string
rank: string
insurance: string
stamina: string
load: string
region: string[]
loginMethod: string[]
rank: string[]
insurance: string[]
stamina: string[]
load: string[]
skinGroup: string
skinName: string
minCoin: number | undefined
@@ -52,12 +52,12 @@ export function useHomeFilters(
const filters = reactive<HomeFilters>({
keyword: '',
server: '',
region: '',
loginMethod: '',
rank: '',
insurance: '',
stamina: '',
load: '',
region: [],
loginMethod: [],
rank: [],
insurance: [],
stamina: [],
load: [],
skinGroup: '',
skinName: '',
minCoin: undefined,
@@ -111,12 +111,12 @@ export function useHomeFilters(
function resetFilters() {
filters.keyword = ''
filters.server = ''
filters.region = ''
filters.loginMethod = ''
filters.rank = ''
filters.insurance = ''
filters.stamina = ''
filters.load = ''
filters.region = []
filters.loginMethod = []
filters.rank = []
filters.insurance = []
filters.stamina = []
filters.load = []
filters.skinGroup = ''
filters.skinName = ''
filters.minCoin = undefined
@@ -133,11 +133,23 @@ export function useHomeFilters(
filters.maxFireLevel = undefined
}
function setStringFilter(key: StringFilterKey, value: string) {
filters[key] = value
function setStringFilter(key: StringFilterKey, value: string | string[]) {
filters[key] = Array.isArray(value)
? uniqueOptions(value)
: value.trim()
? [value.trim()]
: []
closeFilterPopover()
}
function toggleStringFilter(key: StringFilterKey, value: string) {
const current = filters[key]
const next = current.includes(value)
? current.filter(item => item !== value)
: uniqueOptions([...current, value])
filters[key] = next
}
function setCoinRange(min: number | undefined, max: number | undefined) {
filters.minCoin = min
filters.maxCoin = max
@@ -194,6 +206,7 @@ export function useHomeFilters(
skinChipLabel,
resetFilters,
setStringFilter,
toggleStringFilter,
setCoinRange,
setPriceRange,
setDepositRange,
@@ -18,6 +18,10 @@ export function useListingQuery(
const hasMoreListings = ref(true)
let listingRequestSeq = 0
function joinCSV(values: string[]) {
return values.filter(Boolean).join(',') || undefined
}
function buildListingQuery(page: number): PublicListingQuery {
return {
page,
@@ -26,12 +30,12 @@ export function useListingQuery(
sort: sortBy.value,
zone: activeZone.value,
server: filters.server,
region: filters.region,
login_method: filters.loginMethod,
rank: filters.rank,
insurance: filters.insurance,
stamina: filters.stamina,
load: filters.load,
region: joinCSV(filters.region),
login_method: joinCSV(filters.loginMethod),
rank: joinCSV(filters.rank),
insurance: joinCSV(filters.insurance),
stamina: joinCSV(filters.stamina),
load: joinCSV(filters.load),
skin_group: filters.skinGroup,
skin_name: filters.skinName,
min_coin: filters.minCoin,
@@ -20,6 +20,8 @@ import {
isSameQuery,
mergeHomeQuery,
readStoredHomeQuery,
joinQueryCSV,
readQueryCSV,
readQueryNumber,
readQueryString,
storeHomeQuery,
@@ -170,12 +172,12 @@ function applyRouteQueryToHomeState() {
: storedQuery
filters.keyword = readQueryString(query, 'keyword')
filters.server = readQueryString(query, 'server')
filters.region = readQueryString(query, 'region')
filters.loginMethod = readQueryString(query, 'login_method')
filters.rank = readQueryString(query, 'rank')
filters.insurance = readQueryString(query, 'insurance')
filters.stamina = readQueryString(query, 'stamina')
filters.load = readQueryString(query, 'load')
filters.region = readQueryCSV(query, 'region')
filters.loginMethod = readQueryCSV(query, 'login_method')
filters.rank = readQueryCSV(query, 'rank')
filters.insurance = readQueryCSV(query, 'insurance')
filters.stamina = readQueryCSV(query, 'stamina')
filters.load = readQueryCSV(query, 'load')
filters.skinGroup = readQueryString(query, 'skin_group')
filters.skinName = readQueryString(query, 'skin_name')
filters.minCoin = readQueryNumber(query, 'min_coin')
@@ -215,15 +217,15 @@ function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
return {
keyword: filters.keyword.trim() || undefined,
sort: sortBy.value === 'recommended' ? undefined : sortBy.value,
server: filters.server,
region: filters.region,
login_method: filters.loginMethod,
rank: filters.rank,
insurance: filters.insurance,
stamina: filters.stamina,
load: filters.load,
skin_group: filters.skinGroup,
skin_name: filters.skinName,
server: filters.server || undefined,
region: joinQueryCSV(filters.region),
login_method: joinQueryCSV(filters.loginMethod),
rank: joinQueryCSV(filters.rank),
insurance: joinQueryCSV(filters.insurance),
stamina: joinQueryCSV(filters.stamina),
load: joinQueryCSV(filters.load),
skin_group: filters.skinGroup || undefined,
skin_name: filters.skinName || undefined,
min_coin: filters.minCoin,
max_coin: filters.maxCoin,
min_ratio: filters.minRatio,