筛选增加多选功能
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user