refactor(listings): 统一移动端与PC端筛选逻辑并支持日消耗与绝密KD
- 统一预设体系:在 useFilterOptions.ts 集中导出 defaultRangePresets,两端共享档位定义,消除移动端私有硬编码档位 - 统一参数模型:在 useHomeFilters.ts 增加 mobileFiltersToHomeFilters,将移动端字典结构标准化转换为领域层 HomeFilters 模型 - 统一查询构建:在 useListingQuery.ts 抽象纯函数 buildListingQueryFromFilters,双端共用同一个后端发包构建器 - 功能对齐:移动端首页完整补充「日消耗」区间筛选;PC端高级抽屉对齐补充「绝密KD」筛选 - 验证通过:补充双端测试用例,全站门禁 ./scripts/check.sh(SQL/Go/MySQL/Vitest/Vite构建)全部绿灯
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
totalRangeOptions,
|
totalRangeOptions,
|
||||||
fireLevelRangeOptions,
|
fireLevelRangeOptions,
|
||||||
dailyLossRangeOptions,
|
dailyLossRangeOptions,
|
||||||
|
secretKdRangeOptions,
|
||||||
isRangeSelected,
|
isRangeSelected,
|
||||||
} from '@/features/listings/composables/useFilterOptions'
|
} from '@/features/listings/composables/useFilterOptions'
|
||||||
import type { ListingPublishOptions } from '@/features/listings/api/listingOptions'
|
import type { ListingPublishOptions } from '@/features/listings/api/listingOptions'
|
||||||
@@ -72,6 +73,7 @@ function toggleRange(
|
|||||||
| 'minTotal'
|
| 'minTotal'
|
||||||
| 'minFireLevel'
|
| 'minFireLevel'
|
||||||
| 'minDailyLoss'
|
| 'minDailyLoss'
|
||||||
|
| 'minSecretKd'
|
||||||
| 'minAwmAmmo',
|
| 'minAwmAmmo',
|
||||||
maxKey:
|
maxKey:
|
||||||
| 'maxCoin'
|
| 'maxCoin'
|
||||||
@@ -81,6 +83,7 @@ function toggleRange(
|
|||||||
| 'maxTotal'
|
| 'maxTotal'
|
||||||
| 'maxFireLevel'
|
| 'maxFireLevel'
|
||||||
| 'maxDailyLoss'
|
| 'maxDailyLoss'
|
||||||
|
| 'maxSecretKd'
|
||||||
| 'maxAwmAmmo',
|
| 'maxAwmAmmo',
|
||||||
option: RangeOption
|
option: RangeOption
|
||||||
) {
|
) {
|
||||||
@@ -102,6 +105,7 @@ function isRangeActive(
|
|||||||
| 'minTotal'
|
| 'minTotal'
|
||||||
| 'minFireLevel'
|
| 'minFireLevel'
|
||||||
| 'minDailyLoss'
|
| 'minDailyLoss'
|
||||||
|
| 'minSecretKd'
|
||||||
| 'minAwmAmmo',
|
| 'minAwmAmmo',
|
||||||
maxKey:
|
maxKey:
|
||||||
| 'maxCoin'
|
| 'maxCoin'
|
||||||
@@ -111,6 +115,7 @@ function isRangeActive(
|
|||||||
| 'maxTotal'
|
| 'maxTotal'
|
||||||
| 'maxFireLevel'
|
| 'maxFireLevel'
|
||||||
| 'maxDailyLoss'
|
| 'maxDailyLoss'
|
||||||
|
| 'maxSecretKd'
|
||||||
| 'maxAwmAmmo',
|
| 'maxAwmAmmo',
|
||||||
option: RangeOption
|
option: RangeOption
|
||||||
) {
|
) {
|
||||||
@@ -375,6 +380,22 @@ const redSkinGroup = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-field">
|
||||||
|
<label>绝密KD</label>
|
||||||
|
<div class="pill-row">
|
||||||
|
<button
|
||||||
|
v-for="item in secretKdRangeOptions.filter(o => o.label !== '全部')"
|
||||||
|
:key="`kd-${item.label}`"
|
||||||
|
type="button"
|
||||||
|
class="pill-chip"
|
||||||
|
:class="{ active: isRangeActive('minSecretKd', 'maxSecretKd', item) }"
|
||||||
|
@click="toggleRange('minSecretKd', 'maxSecretKd', item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="filter-field">
|
<div class="filter-field">
|
||||||
<label>段位</label>
|
<label>段位</label>
|
||||||
<div class="pill-row">
|
<div class="pill-row">
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { describe, it, expect } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
awmRangeOptions,
|
awmRangeOptions,
|
||||||
depositRangeOptions,
|
depositRangeOptions,
|
||||||
|
defaultRangePresets,
|
||||||
isRangeSelected,
|
isRangeSelected,
|
||||||
moneyRangeOptions,
|
moneyRangeOptions,
|
||||||
rangeLabel,
|
rangeLabel,
|
||||||
ratioRangeOptions,
|
ratioRangeOptions,
|
||||||
|
secretKdRangeOptions,
|
||||||
} from '@/features/listings/composables/useFilterOptions'
|
} from '@/features/listings/composables/useFilterOptions'
|
||||||
|
|
||||||
describe('useFilterOptions', () => {
|
describe('useFilterOptions', () => {
|
||||||
@@ -65,4 +67,32 @@ describe('useFilterOptions', () => {
|
|||||||
max: undefined,
|
max: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('应该包含绝密KD预设选项', () => {
|
||||||
|
expect(secretKdRangeOptions.map(option => option.label)).toEqual([
|
||||||
|
'全部',
|
||||||
|
'1-2',
|
||||||
|
'2-3',
|
||||||
|
'3-5',
|
||||||
|
'5以上',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaultRangePresets 应该为移动端提供统一的日消耗与绝密KD档位', () => {
|
||||||
|
expect(defaultRangePresets.dailyLoss).toEqual([
|
||||||
|
{ label: '≤10', min: '', max: '10' },
|
||||||
|
{ label: '10-20', min: '10', max: '20' },
|
||||||
|
{ label: '20-30', min: '20', max: '30' },
|
||||||
|
{ label: '30-40', min: '30', max: '40' },
|
||||||
|
{ label: '40以上', min: '40', max: '' },
|
||||||
|
])
|
||||||
|
expect(defaultRangePresets.secretKd).toEqual([
|
||||||
|
{ label: '1-2', min: '1', max: '2' },
|
||||||
|
{ label: '2-3', min: '2', max: '3' },
|
||||||
|
{ label: '3-5', min: '3', max: '5' },
|
||||||
|
{ label: '5以上', min: '5', max: '' },
|
||||||
|
])
|
||||||
|
expect(defaultRangePresets.price.length).toBeGreaterThan(0)
|
||||||
|
expect(defaultRangePresets.coin.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { useHomeFilters } from '@/features/listings/composables/useHomeFilters'
|
import {
|
||||||
|
useHomeFilters,
|
||||||
|
mobileFiltersToHomeFilters,
|
||||||
|
} from '@/features/listings/composables/useHomeFilters'
|
||||||
|
import { buildListingQueryFromFilters } from '@/features/listings/composables/useListingQuery'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { emptyListingPublishOptions } from '@/features/listings/api/listingOptions'
|
import { emptyListingPublishOptions } from '@/features/listings/api/listingOptions'
|
||||||
|
|
||||||
@@ -90,4 +94,110 @@ describe('useHomeFilters', () => {
|
|||||||
expect(filters.minCoin).toBe(100)
|
expect(filters.minCoin).toBe(100)
|
||||||
expect(filters.maxCoin).toBe(500)
|
expect(filters.maxCoin).toBe(500)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('应该支持绝密KD与日消耗并正确统计高级筛选数量', () => {
|
||||||
|
const publishOptions = ref(emptyListingPublishOptions)
|
||||||
|
const listings = ref([])
|
||||||
|
|
||||||
|
const { filters, advancedActiveCount, setDailyLossRange, setSecretKdRange, resetFilters } =
|
||||||
|
useHomeFilters(publishOptions, listings)
|
||||||
|
|
||||||
|
expect(advancedActiveCount.value).toBe(0)
|
||||||
|
|
||||||
|
setDailyLossRange(10, 20)
|
||||||
|
expect(filters.minDailyLoss).toBe(10)
|
||||||
|
expect(filters.maxDailyLoss).toBe(20)
|
||||||
|
expect(advancedActiveCount.value).toBe(1)
|
||||||
|
|
||||||
|
setSecretKdRange(2, 3)
|
||||||
|
expect(filters.minSecretKd).toBe(2)
|
||||||
|
expect(filters.maxSecretKd).toBe(3)
|
||||||
|
expect(advancedActiveCount.value).toBe(2)
|
||||||
|
|
||||||
|
resetFilters()
|
||||||
|
expect(filters.minDailyLoss).toBeUndefined()
|
||||||
|
expect(filters.maxDailyLoss).toBeUndefined()
|
||||||
|
expect(filters.minSecretKd).toBeUndefined()
|
||||||
|
expect(filters.maxSecretKd).toBeUndefined()
|
||||||
|
expect(advancedActiveCount.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mobileFiltersToHomeFilters & buildListingQueryFromFilters', () => {
|
||||||
|
it('应该正确转换移动端结构为统一 HomeFilters 并生成对齐的查询参数', () => {
|
||||||
|
const selectedFilters = {
|
||||||
|
server: ['微信区'],
|
||||||
|
region: ['北京', '上海'],
|
||||||
|
login: ['账密'],
|
||||||
|
rank: ['大师'],
|
||||||
|
insurance: ['3*3'],
|
||||||
|
melee: ['小刀'],
|
||||||
|
}
|
||||||
|
const rangeFilters = {
|
||||||
|
price: { min: '50', max: '100' },
|
||||||
|
coin: { min: '100', max: '300' },
|
||||||
|
dailyLoss: { min: '10', max: '20' },
|
||||||
|
secretKd: { min: '2', max: '3' },
|
||||||
|
resource_awmAmmo: { min: '50', max: '100' },
|
||||||
|
resource_testItem: { min: '1', max: '5' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeFilters = mobileFiltersToHomeFilters(selectedFilters, rangeFilters, '搜索关键字', [
|
||||||
|
'melee',
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(homeFilters.keyword).toBe('搜索关键字')
|
||||||
|
expect(homeFilters.server).toBe('微信区')
|
||||||
|
expect(homeFilters.region).toEqual(['北京', '上海'])
|
||||||
|
expect(homeFilters.loginMethod).toEqual(['账密'])
|
||||||
|
expect(homeFilters.rank).toEqual(['大师'])
|
||||||
|
expect(homeFilters.insurance).toEqual(['3*3'])
|
||||||
|
expect(homeFilters.skinGroups).toEqual(['melee'])
|
||||||
|
expect(homeFilters.skinNames).toEqual(['小刀'])
|
||||||
|
expect(homeFilters.minPrice).toBe(50)
|
||||||
|
expect(homeFilters.maxPrice).toBe(100)
|
||||||
|
expect(homeFilters.minCoin).toBe(100)
|
||||||
|
expect(homeFilters.maxCoin).toBe(300)
|
||||||
|
expect(homeFilters.minDailyLoss).toBe(10)
|
||||||
|
expect(homeFilters.maxDailyLoss).toBe(20)
|
||||||
|
expect(homeFilters.minSecretKd).toBe(2)
|
||||||
|
expect(homeFilters.maxSecretKd).toBe(3)
|
||||||
|
expect(homeFilters.minAwmAmmo).toBe(50)
|
||||||
|
expect(homeFilters.maxAwmAmmo).toBe(100)
|
||||||
|
expect(homeFilters.resources).toEqual({ testItem: { min: 1, max: 5 } })
|
||||||
|
|
||||||
|
const query = buildListingQueryFromFilters(homeFilters, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sort: 'recommended',
|
||||||
|
zone: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(query).toMatchObject({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
keyword: '搜索关键字',
|
||||||
|
sort: 'recommended',
|
||||||
|
zone: 'all',
|
||||||
|
server: '微信区',
|
||||||
|
region: '北京,上海',
|
||||||
|
login_method: '账密',
|
||||||
|
rank: '大师',
|
||||||
|
insurance: '3*3',
|
||||||
|
skin_group: 'melee',
|
||||||
|
skin_name: '小刀',
|
||||||
|
min_price: 50,
|
||||||
|
max_price: 100,
|
||||||
|
min_coin: 100,
|
||||||
|
max_coin: 300,
|
||||||
|
min_daily_loss: 10,
|
||||||
|
max_daily_loss: 20,
|
||||||
|
min_secret_kd: 2,
|
||||||
|
max_secret_kd: 3,
|
||||||
|
resource_awmAmmo_min: 50,
|
||||||
|
resource_awmAmmo_max: 100,
|
||||||
|
resource_testItem_min: 1,
|
||||||
|
resource_testItem_max: 5,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,6 +71,44 @@ export const dailyLossRangeOptions = [
|
|||||||
{ label: '40以上', min: 40, max: undefined },
|
{ label: '40以上', min: 40, max: undefined },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const secretKdRangeOptions = [
|
||||||
|
{ label: '全部', min: undefined, max: undefined },
|
||||||
|
{ label: '1-2', min: 1, max: 2 },
|
||||||
|
{ label: '2-3', min: 2, max: 3 },
|
||||||
|
{ label: '3-5', min: 3, max: 5 },
|
||||||
|
{ label: '5以上', min: 5, max: undefined },
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface RangePresetOption {
|
||||||
|
label: string
|
||||||
|
min: string
|
||||||
|
max: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRangePresets(
|
||||||
|
options: Array<{ label: string; min?: number; max?: number }>
|
||||||
|
): RangePresetOption[] {
|
||||||
|
return options
|
||||||
|
.filter(option => option.label !== '全部')
|
||||||
|
.map(option => ({
|
||||||
|
label: option.label,
|
||||||
|
min: option.min === undefined ? '' : String(option.min),
|
||||||
|
max: option.max === undefined ? '' : String(option.max),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultRangePresets: Record<string, RangePresetOption[]> = {
|
||||||
|
price: toRangePresets(moneyRangeOptions),
|
||||||
|
coin: toRangePresets(coinRangeOptions),
|
||||||
|
ratio: toRangePresets(ratioRangeOptions),
|
||||||
|
deposit: toRangePresets(depositRangeOptions),
|
||||||
|
total: toRangePresets(totalRangeOptions),
|
||||||
|
fireLevel: toRangePresets(fireLevelRangeOptions),
|
||||||
|
dailyLoss: toRangePresets(dailyLossRangeOptions),
|
||||||
|
secretKd: toRangePresets(secretKdRangeOptions),
|
||||||
|
resource_awmAmmo: toRangePresets(awmRangeOptions),
|
||||||
|
}
|
||||||
|
|
||||||
export function rangeLabel(min: number | undefined, max: number | undefined, fallback: string) {
|
export function rangeLabel(min: number | undefined, max: number | undefined, fallback: string) {
|
||||||
if (min !== undefined && max !== undefined) return `${min}-${max}`
|
if (min !== undefined && max !== undefined) return `${min}-${max}`
|
||||||
if (min !== undefined) return `${min}+`
|
if (min !== undefined) return `${min}+`
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ export interface HomeFilters {
|
|||||||
/** 日消耗(M/天)区间 */
|
/** 日消耗(M/天)区间 */
|
||||||
minDailyLoss: number | undefined
|
minDailyLoss: number | undefined
|
||||||
maxDailyLoss: number | undefined
|
maxDailyLoss: number | undefined
|
||||||
|
/** 绝密KD区间 */
|
||||||
|
minSecretKd?: number | undefined
|
||||||
|
maxSecretKd?: number | undefined
|
||||||
|
/** 皮肤分类列表 */
|
||||||
|
skinGroups?: string[] | undefined
|
||||||
|
/** 动态资源区间 */
|
||||||
|
resources?: Record<string, { min?: number; max?: number }> | undefined
|
||||||
/** AWM 子弹数量区间 */
|
/** AWM 子弹数量区间 */
|
||||||
minAwmAmmo: number | undefined
|
minAwmAmmo: number | undefined
|
||||||
maxAwmAmmo: number | undefined
|
maxAwmAmmo: number | undefined
|
||||||
@@ -66,6 +73,107 @@ export interface SkinFilterGroup {
|
|||||||
options: string[]
|
options: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseOptionalNumber(value: string | undefined): number | undefined {
|
||||||
|
if (!value || value.trim() === '') return undefined
|
||||||
|
const num = Number(value.trim())
|
||||||
|
return Number.isFinite(num) ? num : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将移动端筛选抽屉的 (selectedFilters, rangeFilters) 映射为统一的 HomeFilters 模型 */
|
||||||
|
export function mobileFiltersToHomeFilters(
|
||||||
|
selectedFilters: Record<string, string[]>,
|
||||||
|
rangeFilters: Record<string, { min: string; max: string }>,
|
||||||
|
keyword = '',
|
||||||
|
knownSkinGroupKeys: string[] = []
|
||||||
|
): HomeFilters {
|
||||||
|
const isSkinGroup = (key: string) =>
|
||||||
|
knownSkinGroupKeys.includes(key) || key === 'melee' || key === 'operatorRed' || key === 'weapon'
|
||||||
|
|
||||||
|
const skinGroups: string[] = []
|
||||||
|
const skinNames: string[] = []
|
||||||
|
let server = ''
|
||||||
|
const region: string[] = []
|
||||||
|
const loginMethod: string[] = []
|
||||||
|
const rank: string[] = []
|
||||||
|
const insurance: string[] = []
|
||||||
|
const stamina: string[] = []
|
||||||
|
const load: string[] = []
|
||||||
|
|
||||||
|
for (const [key, values] of Object.entries(selectedFilters)) {
|
||||||
|
const validValues = values.map(v => v.trim()).filter(Boolean)
|
||||||
|
if (!validValues.length) continue
|
||||||
|
|
||||||
|
if (key === 'server') {
|
||||||
|
server = validValues.join(',')
|
||||||
|
} else if (key === 'region') {
|
||||||
|
region.push(...validValues)
|
||||||
|
} else if (key === 'login') {
|
||||||
|
loginMethod.push(...validValues)
|
||||||
|
} else if (key === 'rank') {
|
||||||
|
rank.push(...validValues)
|
||||||
|
} else if (key === 'insurance') {
|
||||||
|
insurance.push(...validValues)
|
||||||
|
} else if (key === 'stamina') {
|
||||||
|
stamina.push(...validValues)
|
||||||
|
} else if (key === 'load') {
|
||||||
|
load.push(...validValues)
|
||||||
|
} else if (isSkinGroup(key)) {
|
||||||
|
skinGroups.push(key)
|
||||||
|
skinNames.push(...validValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources: Record<string, { min?: number; max?: number }> = {}
|
||||||
|
let minAwmAmmo: number | undefined
|
||||||
|
let maxAwmAmmo: number | undefined
|
||||||
|
|
||||||
|
for (const [key, range] of Object.entries(rangeFilters)) {
|
||||||
|
if (key.startsWith('resource_')) {
|
||||||
|
const resKey = key.replace('resource_', '')
|
||||||
|
const min = parseOptionalNumber(range.min)
|
||||||
|
const max = parseOptionalNumber(range.max)
|
||||||
|
if (resKey === 'awmAmmo') {
|
||||||
|
minAwmAmmo = min
|
||||||
|
maxAwmAmmo = max
|
||||||
|
} else if (min !== undefined || max !== undefined) {
|
||||||
|
resources[resKey] = { min, max }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword,
|
||||||
|
server,
|
||||||
|
region,
|
||||||
|
loginMethod,
|
||||||
|
rank,
|
||||||
|
insurance,
|
||||||
|
stamina,
|
||||||
|
load,
|
||||||
|
skinNames,
|
||||||
|
skinGroups: skinGroups.length ? skinGroups : undefined,
|
||||||
|
resources: Object.keys(resources).length ? resources : undefined,
|
||||||
|
minCoin: parseOptionalNumber(rangeFilters.coin?.min),
|
||||||
|
maxCoin: parseOptionalNumber(rangeFilters.coin?.max),
|
||||||
|
minRatio: parseOptionalNumber(rangeFilters.ratio?.min),
|
||||||
|
maxRatio: parseOptionalNumber(rangeFilters.ratio?.max),
|
||||||
|
minPrice: parseOptionalNumber(rangeFilters.price?.min),
|
||||||
|
maxPrice: parseOptionalNumber(rangeFilters.price?.max),
|
||||||
|
minDeposit: parseOptionalNumber(rangeFilters.deposit?.min),
|
||||||
|
maxDeposit: parseOptionalNumber(rangeFilters.deposit?.max),
|
||||||
|
minTotal: parseOptionalNumber(rangeFilters.total?.min),
|
||||||
|
maxTotal: parseOptionalNumber(rangeFilters.total?.max),
|
||||||
|
minFireLevel: parseOptionalNumber(rangeFilters.fireLevel?.min),
|
||||||
|
maxFireLevel: parseOptionalNumber(rangeFilters.fireLevel?.max),
|
||||||
|
minDailyLoss: parseOptionalNumber(rangeFilters.dailyLoss?.min),
|
||||||
|
maxDailyLoss: parseOptionalNumber(rangeFilters.dailyLoss?.max),
|
||||||
|
minSecretKd: parseOptionalNumber(rangeFilters.secretKd?.min),
|
||||||
|
maxSecretKd: parseOptionalNumber(rangeFilters.secretKd?.max),
|
||||||
|
minAwmAmmo,
|
||||||
|
maxAwmAmmo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useHomeFilters(
|
export function useHomeFilters(
|
||||||
publishOptions: Ref<ListingPublishOptions>,
|
publishOptions: Ref<ListingPublishOptions>,
|
||||||
listings: Ref<PublicListingItem[]>
|
listings: Ref<PublicListingItem[]>
|
||||||
@@ -94,6 +202,8 @@ export function useHomeFilters(
|
|||||||
maxFireLevel: undefined,
|
maxFireLevel: undefined,
|
||||||
minDailyLoss: undefined,
|
minDailyLoss: undefined,
|
||||||
maxDailyLoss: undefined,
|
maxDailyLoss: undefined,
|
||||||
|
minSecretKd: undefined,
|
||||||
|
maxSecretKd: undefined,
|
||||||
minAwmAmmo: undefined,
|
minAwmAmmo: undefined,
|
||||||
maxAwmAmmo: undefined,
|
maxAwmAmmo: undefined,
|
||||||
})
|
})
|
||||||
@@ -159,6 +269,7 @@ export function useHomeFilters(
|
|||||||
if (filters.minTotal !== undefined || filters.maxTotal !== undefined) count += 1
|
if (filters.minTotal !== undefined || filters.maxTotal !== undefined) count += 1
|
||||||
if (filters.minFireLevel !== undefined || filters.maxFireLevel !== undefined) count += 1
|
if (filters.minFireLevel !== undefined || filters.maxFireLevel !== undefined) count += 1
|
||||||
if (filters.minDailyLoss !== undefined || filters.maxDailyLoss !== undefined) count += 1
|
if (filters.minDailyLoss !== undefined || filters.maxDailyLoss !== undefined) count += 1
|
||||||
|
if (filters.minSecretKd !== undefined || filters.maxSecretKd !== undefined) count += 1
|
||||||
return count
|
return count
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,6 +301,8 @@ export function useHomeFilters(
|
|||||||
filters.maxFireLevel = undefined
|
filters.maxFireLevel = undefined
|
||||||
filters.minDailyLoss = undefined
|
filters.minDailyLoss = undefined
|
||||||
filters.maxDailyLoss = undefined
|
filters.maxDailyLoss = undefined
|
||||||
|
filters.minSecretKd = undefined
|
||||||
|
filters.maxSecretKd = undefined
|
||||||
filters.minAwmAmmo = undefined
|
filters.minAwmAmmo = undefined
|
||||||
filters.maxAwmAmmo = undefined
|
filters.maxAwmAmmo = undefined
|
||||||
}
|
}
|
||||||
@@ -249,6 +362,11 @@ export function useHomeFilters(
|
|||||||
filters.maxDailyLoss = max
|
filters.maxDailyLoss = max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSecretKdRange(min: number | undefined, max: number | undefined) {
|
||||||
|
filters.minSecretKd = min
|
||||||
|
filters.maxSecretKd = max
|
||||||
|
}
|
||||||
|
|
||||||
function setAwmAmmoRange(min: number | undefined, max: number | undefined) {
|
function setAwmAmmoRange(min: number | undefined, max: number | undefined) {
|
||||||
filters.minAwmAmmo = min
|
filters.minAwmAmmo = min
|
||||||
filters.maxAwmAmmo = max
|
filters.maxAwmAmmo = max
|
||||||
@@ -302,6 +420,7 @@ export function useHomeFilters(
|
|||||||
setTotalRange,
|
setTotalRange,
|
||||||
setFireLevelRange,
|
setFireLevelRange,
|
||||||
setDailyLossRange,
|
setDailyLossRange,
|
||||||
|
setSecretKdRange,
|
||||||
setAwmAmmoRange,
|
setAwmAmmoRange,
|
||||||
toggleRange,
|
toggleRange,
|
||||||
setFilterPopover,
|
setFilterPopover,
|
||||||
|
|||||||
@@ -4,6 +4,65 @@ import type { HomeFilters } from './useHomeFilters'
|
|||||||
|
|
||||||
const homePageSize = 12
|
const homePageSize = 12
|
||||||
|
|
||||||
|
function joinCSV(values: string[] | undefined) {
|
||||||
|
if (!values?.length) return undefined
|
||||||
|
return values.filter(Boolean).join(',') || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildListingQueryFromFilters(
|
||||||
|
filters: HomeFilters,
|
||||||
|
options: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
sort?: string
|
||||||
|
zone?: string
|
||||||
|
}
|
||||||
|
): PublicListingQuery {
|
||||||
|
const query: PublicListingQuery = {
|
||||||
|
page: options.page,
|
||||||
|
page_size: options.pageSize,
|
||||||
|
keyword: filters.keyword?.trim() || undefined,
|
||||||
|
sort: options.sort,
|
||||||
|
zone: options.zone,
|
||||||
|
server: filters.server || undefined,
|
||||||
|
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: joinCSV(filters.skinGroups),
|
||||||
|
skin_name: joinCSV(filters.skinNames),
|
||||||
|
min_coin: filters.minCoin,
|
||||||
|
max_coin: filters.maxCoin,
|
||||||
|
min_ratio: filters.minRatio,
|
||||||
|
max_ratio: filters.maxRatio,
|
||||||
|
min_price: filters.minPrice,
|
||||||
|
max_price: filters.maxPrice,
|
||||||
|
min_deposit: filters.minDeposit,
|
||||||
|
max_deposit: filters.maxDeposit,
|
||||||
|
min_total: filters.minTotal,
|
||||||
|
max_total: filters.maxTotal,
|
||||||
|
min_fire_level: filters.minFireLevel,
|
||||||
|
max_fire_level: filters.maxFireLevel,
|
||||||
|
min_daily_loss: filters.minDailyLoss,
|
||||||
|
max_daily_loss: filters.maxDailyLoss,
|
||||||
|
min_secret_kd: filters.minSecretKd,
|
||||||
|
max_secret_kd: filters.maxSecretKd,
|
||||||
|
resource_awmAmmo_min: filters.minAwmAmmo,
|
||||||
|
resource_awmAmmo_max: filters.maxAwmAmmo,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.resources) {
|
||||||
|
for (const [key, range] of Object.entries(filters.resources)) {
|
||||||
|
if (range.min !== undefined) query[`resource_${key}_min`] = range.min
|
||||||
|
if (range.max !== undefined) query[`resource_${key}_max`] = range.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
export function useListingQuery(
|
export function useListingQuery(
|
||||||
filters: HomeFilters,
|
filters: HomeFilters,
|
||||||
sortBy: Ref<string>,
|
sortBy: Ref<string>,
|
||||||
@@ -18,43 +77,13 @@ export function useListingQuery(
|
|||||||
const hasMoreListings = ref(true)
|
const hasMoreListings = ref(true)
|
||||||
let listingRequestSeq = 0
|
let listingRequestSeq = 0
|
||||||
|
|
||||||
function joinCSV(values: string[]) {
|
|
||||||
return values.filter(Boolean).join(',') || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildListingQuery(page: number): PublicListingQuery {
|
function buildListingQuery(page: number): PublicListingQuery {
|
||||||
return {
|
return buildListingQueryFromFilters(filters, {
|
||||||
page,
|
page,
|
||||||
page_size: homePageSize,
|
pageSize: homePageSize,
|
||||||
keyword: filters.keyword.trim(),
|
|
||||||
sort: sortBy.value,
|
sort: sortBy.value,
|
||||||
zone: activeZone.value,
|
zone: activeZone.value,
|
||||||
server: filters.server,
|
})
|
||||||
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_name CSV 为 AND 匹配(账号需包含所有选中皮肤)
|
|
||||||
skin_name: joinCSV(filters.skinNames),
|
|
||||||
min_coin: filters.minCoin,
|
|
||||||
max_coin: filters.maxCoin,
|
|
||||||
min_ratio: filters.minRatio,
|
|
||||||
max_ratio: filters.maxRatio,
|
|
||||||
min_price: filters.minPrice,
|
|
||||||
max_price: filters.maxPrice,
|
|
||||||
min_deposit: filters.minDeposit,
|
|
||||||
max_deposit: filters.maxDeposit,
|
|
||||||
min_total: filters.minTotal,
|
|
||||||
max_total: filters.maxTotal,
|
|
||||||
min_fire_level: filters.minFireLevel,
|
|
||||||
max_fire_level: filters.maxFireLevel,
|
|
||||||
min_daily_loss: filters.minDailyLoss,
|
|
||||||
max_daily_loss: filters.maxDailyLoss,
|
|
||||||
resource_awmAmmo_min: filters.minAwmAmmo,
|
|
||||||
resource_awmAmmo_max: filters.maxAwmAmmo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function listingQuerySignature() {
|
function listingQuerySignature() {
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ import {
|
|||||||
storeHomeQuery,
|
storeHomeQuery,
|
||||||
type HomeQueryValue,
|
type HomeQueryValue,
|
||||||
} from '@/features/listings/composables/useHomeFilterQuery'
|
} from '@/features/listings/composables/useHomeFilterQuery'
|
||||||
import { depositRangeOptions } from '@/features/listings/composables/useFilterOptions'
|
import { defaultRangePresets } from '@/features/listings/composables/useFilterOptions'
|
||||||
|
import { mobileFiltersToHomeFilters } from '@/features/listings/composables/useHomeFilters'
|
||||||
|
import { buildListingQueryFromFilters } from '@/features/listings/composables/useListingQuery'
|
||||||
import {
|
import {
|
||||||
getListingChips,
|
getListingChips,
|
||||||
getListingDisplayPrice,
|
getListingDisplayPrice,
|
||||||
@@ -139,51 +141,8 @@ const zoneOptions = [
|
|||||||
{ key: 'highCoin', label: '高币', hint: '100M以上' },
|
{ key: 'highCoin', label: '高币', hint: '100M以上' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 押金档位直接复用首页高级筛选项,保证移动端与桌面端完全一致 */
|
/** 统一使用领域层导出的预设区间,保证与桌面端完全一致 */
|
||||||
const depositPresets = depositRangeOptions
|
const rangePresets = defaultRangePresets
|
||||||
.filter(option => option.label !== '全部')
|
|
||||||
.map(option => ({
|
|
||||||
label: option.label,
|
|
||||||
min: option.min === undefined ? '' : String(option.min),
|
|
||||||
max: option.max === undefined ? '' : String(option.max),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const rangePresets: Record<string, Array<{ label: string; min: string; max: string }>> = {
|
|
||||||
price: [
|
|
||||||
{ label: '50-100', min: '50', max: '100' },
|
|
||||||
{ label: '100-200', min: '100', max: '200' },
|
|
||||||
{ label: '200以上', min: '200', max: '' },
|
|
||||||
],
|
|
||||||
coin: [
|
|
||||||
{ label: '0-100', min: '0', max: '100' },
|
|
||||||
{ label: '100-300', min: '100', max: '300' },
|
|
||||||
{ label: '300-500', min: '300', max: '500' },
|
|
||||||
{ label: '500以上', min: '500', max: '' },
|
|
||||||
],
|
|
||||||
ratio: [
|
|
||||||
{ label: '30-40', min: '30', max: '40' },
|
|
||||||
{ label: '40以上', min: '40', max: '' },
|
|
||||||
],
|
|
||||||
deposit: depositPresets,
|
|
||||||
total: [
|
|
||||||
{ label: '0-500', min: '0', max: '500' },
|
|
||||||
{ label: '500-1000', min: '500', max: '1000' },
|
|
||||||
{ label: '1000-2000', min: '1000', max: '2000' },
|
|
||||||
{ label: '2000-5000', min: '2000', max: '5000' },
|
|
||||||
],
|
|
||||||
fireLevel: [
|
|
||||||
{ label: '38-50', min: '38', max: '50' },
|
|
||||||
{ label: '50-60', min: '50', max: '60' },
|
|
||||||
{ label: '60-70', min: '60', max: '70' },
|
|
||||||
{ label: '70以上', min: '70', max: '' },
|
|
||||||
],
|
|
||||||
resource_awmAmmo: [
|
|
||||||
{ label: '20-50', min: '20', max: '50' },
|
|
||||||
{ label: '50-100', min: '50', max: '100' },
|
|
||||||
{ label: '100-200', min: '100', max: '200' },
|
|
||||||
{ label: '200以上', min: '200', max: '' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeSortLabel = computed(
|
const activeSortLabel = computed(
|
||||||
() => sortOptions.find(option => option.key === activeSort.value)?.label || '综合排序'
|
() => sortOptions.find(option => option.key === activeSort.value)?.label || '综合排序'
|
||||||
@@ -279,6 +238,14 @@ const filterSections = computed<FilterSection[]>(() => [
|
|||||||
minPlaceholder: '最低',
|
minPlaceholder: '最低',
|
||||||
maxPlaceholder: '最高',
|
maxPlaceholder: '最高',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'dailyLoss',
|
||||||
|
title: '日消耗',
|
||||||
|
type: 'range',
|
||||||
|
unit: 'M/天',
|
||||||
|
minPlaceholder: '最低',
|
||||||
|
maxPlaceholder: '最高',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'fireLevel',
|
key: 'fireLevel',
|
||||||
title: '等级',
|
title: '等级',
|
||||||
@@ -602,65 +569,18 @@ function clearFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildListingQuery(page: number): PublicListingQuery {
|
function buildListingQuery(page: number): PublicListingQuery {
|
||||||
const query: PublicListingQuery = {
|
const homeFilters = mobileFiltersToHomeFilters(
|
||||||
|
selectedFilters.value,
|
||||||
|
rangeFilters.value,
|
||||||
|
searchValue.value,
|
||||||
|
publishOptions.value.skin_groups.map(group => group.key)
|
||||||
|
)
|
||||||
|
return buildListingQueryFromFilters(homeFilters, {
|
||||||
page,
|
page,
|
||||||
page_size: mobilePageSize,
|
pageSize: mobilePageSize,
|
||||||
keyword: searchValue.value.trim(),
|
|
||||||
sort: activeSort.value,
|
sort: activeSort.value,
|
||||||
zone: activeZone.value,
|
zone: activeZone.value,
|
||||||
}
|
})
|
||||||
const skinGroups: string[] = []
|
|
||||||
const skinNames: string[] = []
|
|
||||||
for (const [key, values] of Object.entries(selectedFilters.value)) {
|
|
||||||
const value = values.filter(Boolean).join(',')
|
|
||||||
if (!value) continue
|
|
||||||
if (key === 'server') query.server = value
|
|
||||||
else if (key === 'region') query.region = value
|
|
||||||
else if (key === 'login') query.login_method = value
|
|
||||||
else if (key === 'insurance') query.insurance = value
|
|
||||||
else if (key === 'stamina') query.stamina = value
|
|
||||||
else if (key === 'load') query.load = value
|
|
||||||
else if (key === 'rank') query.rank = value
|
|
||||||
else if (isSkinGroupKey(key)) {
|
|
||||||
skinGroups.push(key)
|
|
||||||
skinNames.push(...values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (skinGroups.length) query.skin_group = skinGroups.join(',')
|
|
||||||
if (skinNames.length) query.skin_name = skinNames.join(',')
|
|
||||||
|
|
||||||
for (const [key, range] of Object.entries(rangeFilters.value)) {
|
|
||||||
if (!range.min && !range.max) continue
|
|
||||||
const min = parseOptionalNumber(range.min)
|
|
||||||
const max = parseOptionalNumber(range.max)
|
|
||||||
if (key === 'price') {
|
|
||||||
query.min_price = min
|
|
||||||
query.max_price = max
|
|
||||||
} else if (key === 'coin') {
|
|
||||||
query.min_coin = min
|
|
||||||
query.max_coin = max
|
|
||||||
} else if (key === 'ratio') {
|
|
||||||
query.min_ratio = min
|
|
||||||
query.max_ratio = max
|
|
||||||
} else if (key === 'secretKd') {
|
|
||||||
query.min_secret_kd = min
|
|
||||||
query.max_secret_kd = max
|
|
||||||
} else if (key === 'deposit') {
|
|
||||||
query.min_deposit = min
|
|
||||||
query.max_deposit = max
|
|
||||||
} else if (key === 'total') {
|
|
||||||
query.min_total = min
|
|
||||||
query.max_total = max
|
|
||||||
} else if (key === 'fireLevel') {
|
|
||||||
query.min_fire_level = min
|
|
||||||
query.max_fire_level = max
|
|
||||||
} else if (key.startsWith('resource_')) {
|
|
||||||
const resourceKey = key.replace('resource_', '')
|
|
||||||
query[`resource_${resourceKey}_min`] = min
|
|
||||||
query[`resource_${resourceKey}_max`] = max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterQuerySignature() {
|
function filterQuerySignature() {
|
||||||
@@ -672,12 +592,6 @@ function filterQuerySignature() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOptionalNumber(value: string) {
|
|
||||||
if (value === '') return undefined
|
|
||||||
const number = Number(value)
|
|
||||||
return Number.isFinite(number) ? number : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyRouteQueryToMobileState() {
|
function applyRouteQueryToMobileState() {
|
||||||
const storedQuery = readStoredHomeQuery(mobileHomeFilterStorageKey)
|
const storedQuery = readStoredHomeQuery(mobileHomeFilterStorageKey)
|
||||||
const query = hasHomeQueryValues(route.query, mobileHomeFilterQueryKeys)
|
const query = hasHomeQueryValues(route.query, mobileHomeFilterQueryKeys)
|
||||||
@@ -776,10 +690,6 @@ function scrollToTop() {
|
|||||||
homeCache.saveScrollTop(0)
|
homeCache.saveScrollTop(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSkinGroupKey(key: string) {
|
|
||||||
return publishOptions.value.skin_groups.some(group => group.key === key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQuantityUnit(price: string) {
|
function parseQuantityUnit(price: string) {
|
||||||
const unit = price.split('/')[1]?.trim()
|
const unit = price.split('/')[1]?.trim()
|
||||||
return unit || undefined
|
return unit || undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user