对齐主站筛选结构并修复 AWM 子弹排序

基础/高级筛选项顺序与主站一致,枚举选项改为读取 mobile-home-config;AWM 排序改为按子弹数量降序。
This commit is contained in:
yml2213
2026-07-18 21:31:03 +08:00
parent 4ed938dea2
commit 2ecc6a3770
4 changed files with 308 additions and 248 deletions
+2
View File
@@ -24,6 +24,8 @@ npm run dev
```bash
VITE_API_PROXY_TARGET=http://127.0.0.1:8080 npm run dev
VITE_API_PROXY_TARGET=https://hao858.com/ npm run dev
```
## 构建
+254 -168
View File
@@ -10,7 +10,6 @@ import ConfigProvider from "antd/es/config-provider";
import Empty from "antd/es/empty";
import Input from "antd/es/input";
import Pagination from "antd/es/pagination";
import Select from "antd/es/select";
import Spin from "antd/es/spin";
import Tag from "antd/es/tag";
import Tooltip from "antd/es/tooltip";
@@ -53,25 +52,36 @@ const initialOptions: ListingPublishOptions = {
rank_options: [],
insurance_options: [],
level_options: [],
region_options: [],
skin_groups: [],
};
const fallbackLevels = ["4级", "5级", "6级", "7级"];
const fallbackInsurance = ["1*2", "2*2", "2*3", "3*3"];
const fallbackLogins = ["QQ", "微信", "Steam", "不限"];
const fallbackRanks = ["青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "巅峰"];
const fallbackSkins = ["信条", "坠星者", "处刑者", "影锋", "暗星", "电锯", "蚀金玫瑰", "维什戴尔"];
/** 数值区间预设(主站 HomeFilters 同款,非 publish_options */
interface RangeOption {
label: string;
min: string;
max: string;
}
const coinRanges = [
const coinRanges: RangeOption[] = [
{ label: "15-50", min: "15", max: "50" },
{ label: "50-100", min: "50", max: "100" },
{ label: "100-150", min: "100", max: "150" },
{ label: "150-200", min: "150", max: "200" },
{ label: "200-250", min: "200", max: "250" },
{ label: "250-300", min: "250", max: "300" },
{ label: "300以上", min: "300", max: "" },
];
const priceRanges = [
const ratioRanges: RangeOption[] = [
{ label: "≤30", min: "", max: "30" },
{ label: "30-35", min: "30", max: "35" },
{ label: "35-40", min: "35", max: "40" },
{ label: "40-45", min: "40", max: "45" },
{ label: "45+", min: "45", max: "" },
];
const priceRanges: RangeOption[] = [
{ label: "0-50", min: "0", max: "50" },
{ label: "50-100", min: "50", max: "100" },
{ label: "100-150", min: "100", max: "150" },
@@ -79,49 +89,85 @@ const priceRanges = [
{ label: "200以上", min: "200", max: "" },
];
const depositRanges: RangeOption[] = [
{ label: "50-100", min: "50", max: "100" },
{ label: "100-150", min: "100", max: "150" },
{ label: "150-200", min: "150", max: "200" },
{ label: "200以上", min: "200", max: "" },
];
const totalRanges: RangeOption[] = [
{ 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" },
{ label: "5000+", min: "5000", max: "" },
];
const fireLevelRanges: RangeOption[] = [
{ label: "38-50", min: "38", max: "50" },
{ label: "50-60", min: "50", max: "60" },
];
const sortOptions = [
{ key: "recommended", label: "默认排序" },
{ key: "priceDesc", label: "价格排序" },
{ key: "ratioDesc", label: "比例排序" },
{ key: "coinDesc", label: "哈夫币排序" },
{ key: "ratioAsc", label: "绝密KD排序" },
{ key: "coinAsc", label: "AWM排序" },
{ key: "awmDesc", label: "AWM子弹排序" },
{ key: "priceAsc", label: "六头排序" },
{ key: "loadDesc", label: "六甲排序" },
];
type RangeMinKey = "minCoin" | "minRatio" | "minPrice" | "minDeposit" | "minTotal" | "minFireLevel";
type RangeMaxKey = "maxCoin" | "maxRatio" | "maxPrice" | "maxDeposit" | "maxTotal" | "maxFireLevel";
type ListFilterKey = "stamina" | "load" | "insurance" | "skinNames" | "loginMethods" | "ranks" | "regions";
interface Filters {
keyword: string;
minCoin: string;
maxCoin: string;
minRatio: string;
maxRatio: string;
minPrice: string;
maxPrice: string;
minKd: string;
maxKd: string;
minDeposit: string;
maxDeposit: string;
minTotal: string;
maxTotal: string;
minFireLevel: string;
maxFireLevel: string;
stamina: string[];
load: string[];
insurance: string[];
skinNames: string[];
loginMethods: string[];
ranks: string[];
zones: string[];
regions: string[];
}
const emptyFilters: Filters = {
keyword: "",
minCoin: "",
maxCoin: "",
minRatio: "",
maxRatio: "",
minPrice: "",
maxPrice: "",
minKd: "",
maxKd: "",
minDeposit: "",
maxDeposit: "",
minTotal: "",
maxTotal: "",
minFireLevel: "",
maxFireLevel: "",
stamina: [],
load: [],
insurance: [],
skinNames: [],
loginMethods: [],
ranks: [],
zones: [],
regions: [],
};
export default function App() {
@@ -141,9 +187,6 @@ export default function App() {
Input: {
controlHeight: 38,
},
Select: {
controlHeight: 30,
},
},
}}
>
@@ -168,46 +211,40 @@ function Showcase() {
const [copiedCode, setCopiedCode] = useState("");
const [copiedInfoCode, setCopiedInfoCode] = useState("");
const levelOptions = useMemo(
() => (publishOptions.level_options.length ? publishOptions.level_options : fallbackLevels),
[publishOptions.level_options]
// 枚举类筛选项全部来自 /api/mobile-home-config 的 publish_options
const levelOptions = publishOptions.level_options;
const insuranceOptions = publishOptions.insurance_options;
const loginOptions = publishOptions.login_method_options;
const rankOptions = publishOptions.rank_options;
const regionOptions = publishOptions.region_options;
/** 基础筛选:刀皮 / 红皮 */
const basicSkinSections = useMemo(
() =>
resolveSkinSections(publishOptions.skin_groups, [
{ key: "melee", title: "刀皮" },
{ key: "operatorRed", title: "红皮" },
]),
[publishOptions.skin_groups]
);
const insuranceOptions = useMemo(
() => (publishOptions.insurance_options.length ? publishOptions.insurance_options : fallbackInsurance),
[publishOptions.insurance_options]
/** 高级筛选:枪皮 */
const advancedSkinSections = useMemo(
() => resolveSkinSections(publishOptions.skin_groups, [{ key: "weapon", title: "枪皮" }]),
[publishOptions.skin_groups]
);
const loginOptions = useMemo(
() => (publishOptions.login_method_options.length ? publishOptions.login_method_options : fallbackLogins),
[publishOptions.login_method_options]
);
const rankOptions = useMemo(
() => (publishOptions.rank_options.length ? publishOptions.rank_options : fallbackRanks),
[publishOptions.rank_options]
);
const zoneOptions = useMemo(() => publishOptions.server_options, [publishOptions.server_options]);
const skinSections = useMemo(() => {
const groups = publishOptions.skin_groups.filter((group) =>
["melee", "operatorGold", "operatorRed", "weapon"].includes(group.key)
);
if (groups.length) {
return groups.map((group) => ({
key: group.key,
title: groupTitle(group.key, group.title),
options: group.options,
}));
}
return [{ key: "skin", title: "稀有皮肤", options: fallbackSkins }];
}, [publishOptions.skin_groups]);
const noticeText = announcements[0]?.trim() || "";
const activeFilterCount = useMemo(() => countActiveFilters(filters), [filters]);
const appliedFilterCount = useMemo(() => countActiveFilters(appliedFilters), [appliedFilters]);
const advancedActiveCount = useMemo(() => countAdvancedFilters(filters), [filters]);
const displayListings = useMemo(() => {
const items = [...listings];
if (activeSort === "ratioAsc") return items.sort((a, b) => ratioValue(a) - ratioValue(b));
if (activeSort === "ratioDesc") return items.sort((a, b) => ratioValue(b) - ratioValue(a));
if (activeSort === "coinAsc") return items.sort((a, b) => a.haf_coin_amount - b.haf_coin_amount);
// 与后端 awmDesc 一致:按 AW子弹数量降序(当前页兜底)
if (activeSort === "awmDesc") {
return items.sort((a, b) => getResourceQuantity(b, "awmAmmo") - getResourceQuantity(a, "awmAmmo"));
}
if (activeSort === "loadDesc") {
return items.sort((a, b) => getResourceQuantity(b, "armor6") - getResourceQuantity(a, "armor6"));
}
@@ -258,7 +295,7 @@ function Showcase() {
setFilters((current) => ({ ...current, [key]: value }));
}
function toggleFilterList(key: "stamina" | "load" | "insurance" | "skinNames", value: string) {
function toggleFilterList(key: ListFilterKey, value: string) {
setFilters((current) => {
const nextList = current[key].includes(value) ? current[key].filter((item) => item !== value) : [...current[key], value];
return { ...current, [key]: nextList };
@@ -281,7 +318,7 @@ function Showcase() {
setAdvancedOpen(false);
}
function selectRange(minKey: "minCoin" | "minPrice", maxKey: "maxCoin" | "maxPrice", min: string, max: string) {
function selectRange(minKey: RangeMinKey, maxKey: RangeMaxKey, min: string, max: string) {
setFilters((current) => {
if (current[minKey] === min && current[maxKey] === max) {
return { ...current, [minKey]: "", [maxKey]: "" };
@@ -290,6 +327,10 @@ function Showcase() {
});
}
function isRangeActive(minKey: RangeMinKey, maxKey: RangeMaxKey, item: RangeOption) {
return filters[minKey] === item.min && filters[maxKey] === item.max;
}
function setSort(sortKey: string) {
setActiveSort(sortKey);
setPage(1);
@@ -411,7 +452,7 @@ function Showcase() {
{appliedFilterCount > 0 && !filterOpen ? (
<div className="filter-summary" aria-label="已选筛选">
{buildFilterSummary(appliedFilters, insuranceLabel).map((item) => (
{buildFilterSummary(appliedFilters).map((item) => (
<span key={item} className="filter-summary-chip">
{item}
</span>
@@ -420,26 +461,27 @@ function Showcase() {
) : null}
<div className="filter-body">
{/* 基础筛选:哈夫币 / 比例 / 保险 / 体力 / 负重 / 登录 / 刀皮 / 红皮 */}
<div className="filter-grid">
<FilterRow
active={(item) => filters.minCoin === item.min && filters.maxCoin === item.max}
active={(item) => isRangeActive("minCoin", "maxCoin", item)}
label="哈夫币"
options={coinRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)}
/>
<FilterRow
active={(item) => filters.minPrice === item.min && filters.maxPrice === item.max}
label="租金"
options={priceRanges}
active={(item) => isRangeActive("minRatio", "maxRatio", item)}
label="比例"
options={ratioRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)}
onSelect={(item) => selectRange("minRatio", "maxRatio", item.min, item.max)}
/>
<FilterRow
active={(item) => filters.insurance.includes(item)}
label="保险"
options={insuranceOptions}
render={insuranceLabel}
render={(item) => item}
onSelect={(item) => toggleFilterList("insurance", item)}
/>
<FilterRow
@@ -456,100 +498,96 @@ function Showcase() {
render={(item) => item}
onSelect={(item) => toggleFilterList("load", item)}
/>
<div className="filter-row compact-select">
<span className="filter-label">IP地址</span>
<Select
allowClear
mode="multiple"
maxTagCount="responsive"
options={zoneOptions.map((item) => ({ label: item, value: item }))}
placeholder="全部"
value={filters.zones}
onChange={(value) => updateFilter("zones", value)}
<FilterRow
active={(item) => filters.loginMethods.includes(item)}
label="登录方式"
options={loginOptions}
render={(item) => item}
onSelect={(item) => toggleFilterList("loginMethods", item)}
/>
</div>
<div className="filter-row compact-select">
<span className="filter-label"></span>
<Select
allowClear
mode="multiple"
maxTagCount="responsive"
options={loginOptions.map((item) => ({ label: item, value: item }))}
placeholder="不限"
value={filters.loginMethods}
onChange={(value) => updateFilter("loginMethods", value)}
/>
</div>
<div className="filter-row compact-select">
<span className="filter-label"></span>
<Select
allowClear
mode="multiple"
maxTagCount="responsive"
options={rankOptions.map((item) => ({ label: item, value: item }))}
placeholder="不限"
value={filters.ranks}
onChange={(value) => updateFilter("ranks", value)}
/>
</div>
</div>
<div className={advancedOpen ? "advanced-panel open" : "advanced-panel"}>
<div className="range-fields">
<label>
<span></span>
<div className="range-inputs">
<Input
inputMode="numeric"
placeholder="最小"
value={filters.minCoin}
onChange={(event) => updateFilter("minCoin", event.target.value)}
/>
<em></em>
<Input
inputMode="numeric"
placeholder="最大"
value={filters.maxCoin}
onChange={(event) => updateFilter("maxCoin", event.target.value)}
/>
</div>
</label>
<label>
<span>KD范围</span>
<div className="range-inputs">
<Input
inputMode="decimal"
placeholder="最小"
value={filters.minKd}
onChange={(event) => updateFilter("minKd", event.target.value)}
/>
<em></em>
<Input
inputMode="decimal"
placeholder="最大"
value={filters.maxKd}
onChange={(event) => updateFilter("maxKd", event.target.value)}
/>
</div>
</label>
</div>
{skinSections.map((section) => (
{basicSkinSections.map((section) => (
<FilterRow
key={section.key}
fullWidth
active={(item) => filters.skinNames.includes(item)}
label={section.title}
options={section.options.slice(0, 18)}
options={section.options}
render={(item) => item}
onSelect={(item) => toggleFilterList("skinNames", item)}
/>
))}
</div>
<div className="filter-footer">
<div className="advanced-toggle-row">
<button className="advanced-toggle" type="button" onClick={() => setAdvancedOpen((open) => !open)}>
{advancedOpen ? "收起高级筛选" : "展开高级筛选"}
{advancedActiveCount > 0 ? <em className="advanced-count">{advancedActiveCount}</em> : null}
<DownOutlined rotate={advancedOpen ? 180 : 0} />
</button>
</div>
{/* 高级筛选:租金 / 押金 / 合计 / 等级 / 段位 / IP / 枪皮 */}
<div className={advancedOpen ? "advanced-panel open" : "advanced-panel"}>
<div className="filter-grid">
<FilterRow
active={(item) => isRangeActive("minPrice", "maxPrice", item)}
label="租金"
options={priceRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)}
/>
<FilterRow
active={(item) => isRangeActive("minDeposit", "maxDeposit", item)}
label="押金"
options={depositRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minDeposit", "maxDeposit", item.min, item.max)}
/>
<FilterRow
active={(item) => isRangeActive("minTotal", "maxTotal", item)}
label="合计金额"
options={totalRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minTotal", "maxTotal", item.min, item.max)}
/>
<FilterRow
active={(item) => isRangeActive("minFireLevel", "maxFireLevel", item)}
label="等级"
options={fireLevelRanges}
render={(item) => item.label}
onSelect={(item) => selectRange("minFireLevel", "maxFireLevel", item.min, item.max)}
/>
<FilterRow
fullWidth
active={(item) => filters.ranks.includes(item)}
label="段位"
options={rankOptions}
render={(item) => item}
onSelect={(item) => toggleFilterList("ranks", item)}
/>
<FilterRow
fullWidth
active={(item) => filters.regions.includes(item)}
label="IP地区"
options={regionOptions}
render={(item) => item}
onSelect={(item) => toggleFilterList("regions", item)}
/>
{advancedSkinSections.map((section) => (
<FilterRow
key={section.key}
fullWidth
active={(item) => filters.skinNames.includes(item)}
label={section.title}
options={section.options}
render={(item) => item}
onSelect={(item) => toggleFilterList("skinNames", item)}
/>
))}
</div>
</div>
<div className="filter-footer">
<div className="filter-mobile-actions">
<Button onClick={resetFilters}></Button>
<Button type="primary" icon={<SearchOutlined />} onClick={() => applyFilters()}>
@@ -644,11 +682,13 @@ interface FilterRowProps<T> {
active: (item: T) => boolean;
render: (item: T) => string;
onSelect: (item: T) => void;
fullWidth?: boolean;
}
function FilterRow<T>({ active, label, onSelect, options, render }: FilterRowProps<T>) {
function FilterRow<T>({ active, fullWidth, label, onSelect, options, render }: FilterRowProps<T>) {
if (!options.length) return null;
return (
<div className="filter-row">
<div className={fullWidth ? "filter-row filter-row-full" : "filter-row"}>
<span className="filter-label">{label}</span>
<div className="chip-list">
{options.map((item) => {
@@ -853,15 +893,21 @@ function buildQuery(nextPage: number, filters: Filters, sort: string): PublicLis
page_size: pageSize,
keyword: filters.keyword.trim(),
sort: backendSortKey(sort),
zone: filters.zones.join(","),
region: filters.regions.join(","),
login_method: filters.loginMethods.join(","),
rank: filters.ranks.join(","),
min_coin: toNumber(filters.minCoin),
max_coin: toNumber(filters.maxCoin),
min_ratio: toNumber(filters.minRatio),
max_ratio: toNumber(filters.maxRatio),
min_price: toNumber(filters.minPrice),
max_price: toNumber(filters.maxPrice),
min_secret_kd: toNumber(filters.minKd),
max_secret_kd: toNumber(filters.maxKd),
min_deposit: toNumber(filters.minDeposit),
max_deposit: toNumber(filters.maxDeposit),
min_total: toNumber(filters.minTotal),
max_total: toNumber(filters.maxTotal),
min_fire_level: toNumber(filters.minFireLevel),
max_fire_level: toNumber(filters.maxFireLevel),
stamina: filters.stamina.join(","),
load: filters.load.join(","),
insurance: filters.insurance.join(","),
@@ -870,7 +916,7 @@ function buildQuery(nextPage: number, filters: Filters, sort: string): PublicLis
}
function backendSortKey(sort: string) {
if (sort === "priceAsc" || sort === "priceDesc" || sort === "coinDesc") return sort;
if (sort === "priceAsc" || sort === "priceDesc" || sort === "coinDesc" || sort === "awmDesc") return sort;
return "recommended";
}
@@ -897,60 +943,100 @@ function cloneFilters(filters: Filters): Filters {
skinNames: [...filters.skinNames],
loginMethods: [...filters.loginMethods],
ranks: [...filters.ranks],
zones: [...filters.zones],
regions: [...filters.regions],
};
}
function groupTitle(key: string, title: string) {
if (key === "melee") return "刀皮";
if (key === "operatorGold") return "干员金皮";
if (key === "operatorRed") return "干员红皮";
if (key === "weapon") return "枪皮";
return title;
/** 从主站 publish_options.skin_groups 取对应分组,无数据则不展示 */
function resolveSkinSections(
groups: ListingPublishOptions["skin_groups"],
defs: Array<{ key: string; title: string }>
) {
return defs
.map((def) => {
const group = groups.find((item) => item.key === def.key);
if (!group?.options?.length) return null;
return {
key: def.key,
title: def.title,
options: group.options,
};
})
.filter((item): item is { key: string; title: string; options: string[] } => item !== null);
}
function insuranceLabel(value: string) {
const [rows, cols] = value.split("*").map((item) => Number(item));
if (rows > 0 && cols > 0) return `${rows * cols}`;
return value;
function hasRange(min: string, max: string) {
return Boolean(min || max);
}
function formatRangeText(min: string, max: string) {
if (min && max) return `${min}-${max}`;
if (min) return `${min}+`;
if (max) return `${max}`;
return "";
}
function countActiveFilters(filters: Filters) {
let count = 0;
if (filters.keyword.trim()) count += 1;
if (filters.minCoin || filters.maxCoin) count += 1;
if (filters.minPrice || filters.maxPrice) count += 1;
if (filters.minKd || filters.maxKd) count += 1;
if (hasRange(filters.minCoin, filters.maxCoin)) count += 1;
if (hasRange(filters.minRatio, filters.maxRatio)) count += 1;
if (hasRange(filters.minPrice, filters.maxPrice)) count += 1;
if (hasRange(filters.minDeposit, filters.maxDeposit)) count += 1;
if (hasRange(filters.minTotal, filters.maxTotal)) count += 1;
if (hasRange(filters.minFireLevel, filters.maxFireLevel)) count += 1;
count += filters.stamina.length;
count += filters.load.length;
count += filters.insurance.length;
count += filters.skinNames.length;
count += filters.loginMethods.length;
count += filters.ranks.length;
count += filters.zones.length;
count += filters.regions.length;
return count;
}
function buildFilterSummary(filters: Filters, formatInsurance: (value: string) => string) {
/** 高级筛选区已选项数(用于展开按钮角标) */
function countAdvancedFilters(filters: Filters) {
let count = 0;
if (hasRange(filters.minPrice, filters.maxPrice)) count += 1;
if (hasRange(filters.minDeposit, filters.maxDeposit)) count += 1;
if (hasRange(filters.minTotal, filters.maxTotal)) count += 1;
if (hasRange(filters.minFireLevel, filters.maxFireLevel)) count += 1;
if (filters.ranks.length) count += 1;
if (filters.regions.length) count += 1;
// 枪皮无法单独区分,用 skinNames 有选中时在高级区也提示(保守计 0,避免误导)
return count;
}
function buildFilterSummary(filters: Filters) {
const items: string[] = [];
if (filters.minCoin || filters.maxCoin) {
items.push(`哈夫币 ${filters.minCoin || "0"}-${filters.maxCoin || "∞"}`);
if (hasRange(filters.minCoin, filters.maxCoin)) {
items.push(`哈夫币 ${formatRangeText(filters.minCoin, filters.maxCoin)}`);
}
if (filters.minPrice || filters.maxPrice) {
items.push(`租金 ${filters.minPrice || "0"}-${filters.maxPrice || "∞"}`);
if (hasRange(filters.minRatio, filters.maxRatio)) {
items.push(`比例 ${formatRangeText(filters.minRatio, filters.maxRatio)}`);
}
for (const value of filters.insurance) items.push(`保险 ${formatInsurance(value)}`);
for (const value of filters.insurance) items.push(`保险 ${value}`);
for (const value of filters.stamina) items.push(`体力 ${value}`);
for (const value of filters.load) items.push(`负重 ${value}`);
for (const value of filters.ranks) items.push(value);
for (const value of filters.loginMethods) items.push(value);
for (const value of filters.zones) items.push(value);
for (const value of filters.skinNames) items.push(value);
if (filters.minKd || filters.maxKd) {
items.push(`KD ${filters.minKd || "0"}-${filters.maxKd || "∞"}`);
if (hasRange(filters.minPrice, filters.maxPrice)) {
items.push(`租金 ${formatRangeText(filters.minPrice, filters.maxPrice)}`);
}
if (hasRange(filters.minDeposit, filters.maxDeposit)) {
items.push(`押金 ${formatRangeText(filters.minDeposit, filters.maxDeposit)}`);
}
if (hasRange(filters.minTotal, filters.maxTotal)) {
items.push(`合计 ${formatRangeText(filters.minTotal, filters.maxTotal)}`);
}
if (hasRange(filters.minFireLevel, filters.maxFireLevel)) {
items.push(`等级 ${formatRangeText(filters.minFireLevel, filters.maxFireLevel)}`);
}
for (const value of filters.ranks) items.push(value);
for (const value of filters.regions) items.push(value);
for (const value of filters.skinNames) items.push(value);
if (filters.keyword.trim()) items.push(`编号 ${filters.keyword.trim()}`);
return items.slice(0, 6);
return items.slice(0, 8);
}
function getAssetText(item: Listing, key: string) {
+11
View File
@@ -52,6 +52,7 @@ export interface PublicListingQuery {
keyword?: string;
sort?: string;
zone?: string;
region?: string;
login_method?: string;
rank?: string;
insurance?: string;
@@ -60,8 +61,16 @@ export interface PublicListingQuery {
skin_name?: string;
min_coin?: number;
max_coin?: number;
min_ratio?: number;
max_ratio?: number;
min_price?: number;
max_price?: number;
min_deposit?: number;
max_deposit?: number;
min_total?: number;
max_total?: number;
min_fire_level?: number;
max_fire_level?: number;
min_secret_kd?: number;
max_secret_kd?: number;
}
@@ -81,6 +90,7 @@ export interface ListingPublishOptions {
rank_options: string[];
insurance_options: string[];
level_options: string[];
region_options: string[];
skin_groups: Array<{
key: string;
title: string;
@@ -141,6 +151,7 @@ function normalizeOptions(value: unknown): ListingPublishOptions {
rank_options: normalizeStringArray(options.rank_options),
insurance_options: normalizeStringArray(options.insurance_options),
level_options: normalizeStringArray(options.level_options),
region_options: normalizeStringArray(options.region_options),
skin_groups: normalizeSkinGroups(options.skin_groups),
};
}
+40 -79
View File
@@ -412,8 +412,11 @@ input {
gap: 12px;
}
.filter-label,
.range-fields > label > span {
.filter-row-full {
grid-column: 1 / -1;
}
.filter-label {
min-height: 30px;
display: inline-flex;
align-items: center;
@@ -472,71 +475,27 @@ input {
transform: translateY(1px);
}
.compact-select .ant-select {
width: min(280px, 100%);
}
.compact-select .ant-select-selector {
min-height: 34px !important;
border-radius: 10px !important;
border-color: #e6ebf2 !important;
background: #f8fafc !important;
}
.compact-select .ant-select-focused .ant-select-selector {
border-color: var(--brand) !important;
box-shadow: 0 0 0 2px rgba(255, 107, 0, 0.12) !important;
}
.advanced-panel {
display: none;
padding-top: 16px;
border-top: 1px dashed #e8edf3;
margin-top: 16px;
margin-top: 4px;
}
.advanced-panel.open {
display: grid;
gap: 14px;
display: block;
}
.range-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px 38px;
}
.range-fields > label {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
align-items: center;
gap: 12px;
}
.range-inputs {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 8px;
}
.range-inputs em {
color: #98a2b3;
font-size: 12px;
font-style: normal;
font-weight: 700;
}
.range-inputs .ant-input {
border-radius: 10px;
background: #f8fafc;
.advanced-toggle-row {
display: flex;
justify-content: center;
padding: 8px 0 4px;
}
.filter-footer {
justify-content: center;
min-height: 44px;
padding-top: 4px;
min-height: 0;
padding-top: 10px;
flex-wrap: wrap;
gap: 10px;
}
@@ -565,6 +524,22 @@ input {
font-size: 11px;
}
.advanced-count {
min-width: 16px;
height: 16px;
padding: 0 5px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #fff;
background: var(--brand);
font-size: 11px;
font-style: normal;
font-weight: 900;
line-height: 1;
}
.result-area {
margin-top: 14px;
padding: 16px 16px 28px;
@@ -1146,8 +1121,7 @@ input {
width: calc(100% - 24px);
}
.filter-grid,
.range-fields {
.filter-grid {
grid-template-columns: 1fr;
}
@@ -1310,17 +1284,14 @@ input {
gap: 10px;
}
.filter-row,
.range-fields > label {
grid-template-columns: 44px minmax(0, 1fr);
.filter-row {
grid-template-columns: 52px minmax(0, 1fr);
align-items: center;
gap: 8px;
}
.filter-label,
.range-fields > label > span,
.filter-row > span.filter-label,
.compact-select > .filter-label {
.filter-row > span.filter-label {
min-height: 28px;
font-size: 12px;
color: #667085;
@@ -1349,12 +1320,11 @@ input {
font-size: 12px;
}
.compact-select .ant-select {
width: 100%;
}
.compact-select .ant-select-selector {
min-height: 32px !important;
/* 全宽筛选(皮肤/地区)在移动端允许换行,避免横向滚太长 */
.filter-row-full .chip-list {
flex-wrap: wrap;
overflow: visible;
mask-image: none;
}
.advanced-panel {
@@ -1362,13 +1332,8 @@ input {
padding-top: 12px;
}
.advanced-panel.open {
gap: 10px;
}
.range-fields {
grid-template-columns: 1fr;
gap: 10px;
.advanced-toggle-row {
padding: 4px 0 0;
}
.filter-footer {
@@ -1420,10 +1385,6 @@ input {
font-size: 12px;
}
.compact-select .ant-select {
width: 100%;
}
.sort-toolbar {
align-items: stretch;
flex-direction: column;