对齐主站筛选结构并修复 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 ```bash
VITE_API_PROXY_TARGET=http://127.0.0.1:8080 npm run dev 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 Empty from "antd/es/empty";
import Input from "antd/es/input"; import Input from "antd/es/input";
import Pagination from "antd/es/pagination"; import Pagination from "antd/es/pagination";
import Select from "antd/es/select";
import Spin from "antd/es/spin"; import Spin from "antd/es/spin";
import Tag from "antd/es/tag"; import Tag from "antd/es/tag";
import Tooltip from "antd/es/tooltip"; import Tooltip from "antd/es/tooltip";
@@ -53,25 +52,36 @@ const initialOptions: ListingPublishOptions = {
rank_options: [], rank_options: [],
insurance_options: [], insurance_options: [],
level_options: [], level_options: [],
region_options: [],
skin_groups: [], skin_groups: [],
}; };
const fallbackLevels = ["4级", "5级", "6级", "7级"]; /** 数值区间预设(主站 HomeFilters 同款,非 publish_options */
const fallbackInsurance = ["1*2", "2*2", "2*3", "3*3"]; interface RangeOption {
const fallbackLogins = ["QQ", "微信", "Steam", "不限"]; label: string;
const fallbackRanks = ["青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "巅峰"]; min: string;
const fallbackSkins = ["信条", "坠星者", "处刑者", "影锋", "暗星", "电锯", "蚀金玫瑰", "维什戴尔"]; max: string;
}
const coinRanges = [ const coinRanges: RangeOption[] = [
{ label: "15-50", min: "15", max: "50" }, { label: "15-50", min: "15", max: "50" },
{ label: "50-100", min: "50", max: "100" }, { label: "50-100", min: "50", max: "100" },
{ label: "100-150", min: "100", max: "150" }, { label: "100-150", min: "100", max: "150" },
{ label: "150-200", min: "150", max: "200" }, { label: "150-200", min: "150", max: "200" },
{ label: "200-250", min: "200", max: "250" }, { label: "200-250", min: "200", max: "250" },
{ label: "250-300", min: "250", max: "300" },
{ label: "300以上", min: "300", max: "" }, { 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: "0-50", min: "0", max: "50" },
{ label: "50-100", min: "50", max: "100" }, { label: "50-100", min: "50", max: "100" },
{ label: "100-150", min: "100", max: "150" }, { label: "100-150", min: "100", max: "150" },
@@ -79,49 +89,85 @@ const priceRanges = [
{ label: "200以上", min: "200", max: "" }, { 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 = [ const sortOptions = [
{ key: "recommended", label: "默认排序" }, { key: "recommended", label: "默认排序" },
{ key: "priceDesc", label: "价格排序" }, { key: "priceDesc", label: "价格排序" },
{ key: "ratioDesc", label: "比例排序" }, { key: "ratioDesc", label: "比例排序" },
{ key: "coinDesc", label: "哈夫币排序" }, { key: "coinDesc", label: "哈夫币排序" },
{ key: "ratioAsc", label: "绝密KD排序" }, { key: "ratioAsc", label: "绝密KD排序" },
{ key: "coinAsc", label: "AWM排序" }, { key: "awmDesc", label: "AWM子弹排序" },
{ key: "priceAsc", label: "六头排序" }, { key: "priceAsc", label: "六头排序" },
{ key: "loadDesc", 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 { interface Filters {
keyword: string; keyword: string;
minCoin: string; minCoin: string;
maxCoin: string; maxCoin: string;
minRatio: string;
maxRatio: string;
minPrice: string; minPrice: string;
maxPrice: string; maxPrice: string;
minKd: string; minDeposit: string;
maxKd: string; maxDeposit: string;
minTotal: string;
maxTotal: string;
minFireLevel: string;
maxFireLevel: string;
stamina: string[]; stamina: string[];
load: string[]; load: string[];
insurance: string[]; insurance: string[];
skinNames: string[]; skinNames: string[];
loginMethods: string[]; loginMethods: string[];
ranks: string[]; ranks: string[];
zones: string[]; regions: string[];
} }
const emptyFilters: Filters = { const emptyFilters: Filters = {
keyword: "", keyword: "",
minCoin: "", minCoin: "",
maxCoin: "", maxCoin: "",
minRatio: "",
maxRatio: "",
minPrice: "", minPrice: "",
maxPrice: "", maxPrice: "",
minKd: "", minDeposit: "",
maxKd: "", maxDeposit: "",
minTotal: "",
maxTotal: "",
minFireLevel: "",
maxFireLevel: "",
stamina: [], stamina: [],
load: [], load: [],
insurance: [], insurance: [],
skinNames: [], skinNames: [],
loginMethods: [], loginMethods: [],
ranks: [], ranks: [],
zones: [], regions: [],
}; };
export default function App() { export default function App() {
@@ -141,9 +187,6 @@ export default function App() {
Input: { Input: {
controlHeight: 38, controlHeight: 38,
}, },
Select: {
controlHeight: 30,
},
}, },
}} }}
> >
@@ -168,46 +211,40 @@ function Showcase() {
const [copiedCode, setCopiedCode] = useState(""); const [copiedCode, setCopiedCode] = useState("");
const [copiedInfoCode, setCopiedInfoCode] = useState(""); const [copiedInfoCode, setCopiedInfoCode] = useState("");
const levelOptions = useMemo( // 枚举类筛选项全部来自 /api/mobile-home-config 的 publish_options
() => (publishOptions.level_options.length ? publishOptions.level_options : fallbackLevels), const levelOptions = publishOptions.level_options;
[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), const advancedSkinSections = useMemo(
[publishOptions.insurance_options] () => 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 noticeText = announcements[0]?.trim() || "";
const activeFilterCount = useMemo(() => countActiveFilters(filters), [filters]); const activeFilterCount = useMemo(() => countActiveFilters(filters), [filters]);
const appliedFilterCount = useMemo(() => countActiveFilters(appliedFilters), [appliedFilters]); const appliedFilterCount = useMemo(() => countActiveFilters(appliedFilters), [appliedFilters]);
const advancedActiveCount = useMemo(() => countAdvancedFilters(filters), [filters]);
const displayListings = useMemo(() => { const displayListings = useMemo(() => {
const items = [...listings]; const items = [...listings];
if (activeSort === "ratioAsc") return items.sort((a, b) => ratioValue(a) - ratioValue(b)); 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 === "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") { if (activeSort === "loadDesc") {
return items.sort((a, b) => getResourceQuantity(b, "armor6") - getResourceQuantity(a, "armor6")); return items.sort((a, b) => getResourceQuantity(b, "armor6") - getResourceQuantity(a, "armor6"));
} }
@@ -258,7 +295,7 @@ function Showcase() {
setFilters((current) => ({ ...current, [key]: value })); setFilters((current) => ({ ...current, [key]: value }));
} }
function toggleFilterList(key: "stamina" | "load" | "insurance" | "skinNames", value: string) { function toggleFilterList(key: ListFilterKey, value: string) {
setFilters((current) => { setFilters((current) => {
const nextList = current[key].includes(value) ? current[key].filter((item) => item !== value) : [...current[key], value]; const nextList = current[key].includes(value) ? current[key].filter((item) => item !== value) : [...current[key], value];
return { ...current, [key]: nextList }; return { ...current, [key]: nextList };
@@ -281,7 +318,7 @@ function Showcase() {
setAdvancedOpen(false); 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) => { setFilters((current) => {
if (current[minKey] === min && current[maxKey] === max) { if (current[minKey] === min && current[maxKey] === max) {
return { ...current, [minKey]: "", [maxKey]: "" }; 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) { function setSort(sortKey: string) {
setActiveSort(sortKey); setActiveSort(sortKey);
setPage(1); setPage(1);
@@ -411,7 +452,7 @@ function Showcase() {
{appliedFilterCount > 0 && !filterOpen ? ( {appliedFilterCount > 0 && !filterOpen ? (
<div className="filter-summary" aria-label="已选筛选"> <div className="filter-summary" aria-label="已选筛选">
{buildFilterSummary(appliedFilters, insuranceLabel).map((item) => ( {buildFilterSummary(appliedFilters).map((item) => (
<span key={item} className="filter-summary-chip"> <span key={item} className="filter-summary-chip">
{item} {item}
</span> </span>
@@ -420,26 +461,27 @@ function Showcase() {
) : null} ) : null}
<div className="filter-body"> <div className="filter-body">
{/* 基础筛选:哈夫币 / 比例 / 保险 / 体力 / 负重 / 登录 / 刀皮 / 红皮 */}
<div className="filter-grid"> <div className="filter-grid">
<FilterRow <FilterRow
active={(item) => filters.minCoin === item.min && filters.maxCoin === item.max} active={(item) => isRangeActive("minCoin", "maxCoin", item)}
label="哈夫币" label="哈夫币"
options={coinRanges} options={coinRanges}
render={(item) => item.label} render={(item) => item.label}
onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)} onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)}
/> />
<FilterRow <FilterRow
active={(item) => filters.minPrice === item.min && filters.maxPrice === item.max} active={(item) => isRangeActive("minRatio", "maxRatio", item)}
label="租金" label="比例"
options={priceRanges} options={ratioRanges}
render={(item) => item.label} render={(item) => item.label}
onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)} onSelect={(item) => selectRange("minRatio", "maxRatio", item.min, item.max)}
/> />
<FilterRow <FilterRow
active={(item) => filters.insurance.includes(item)} active={(item) => filters.insurance.includes(item)}
label="保险" label="保险"
options={insuranceOptions} options={insuranceOptions}
render={insuranceLabel} render={(item) => item}
onSelect={(item) => toggleFilterList("insurance", item)} onSelect={(item) => toggleFilterList("insurance", item)}
/> />
<FilterRow <FilterRow
@@ -456,100 +498,96 @@ function Showcase() {
render={(item) => item} render={(item) => item}
onSelect={(item) => toggleFilterList("load", item)} onSelect={(item) => toggleFilterList("load", item)}
/> />
<div className="filter-row compact-select"> <FilterRow
<span className="filter-label">IP地址</span> active={(item) => filters.loginMethods.includes(item)}
<Select label="登录方式"
allowClear options={loginOptions}
mode="multiple" render={(item) => item}
maxTagCount="responsive" onSelect={(item) => toggleFilterList("loginMethods", item)}
options={zoneOptions.map((item) => ({ label: item, value: item }))}
placeholder="全部"
value={filters.zones}
onChange={(value) => updateFilter("zones", value)}
/> />
</div> {basicSkinSections.map((section) => (
<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) => (
<FilterRow <FilterRow
key={section.key} key={section.key}
fullWidth
active={(item) => filters.skinNames.includes(item)} active={(item) => filters.skinNames.includes(item)}
label={section.title} label={section.title}
options={section.options.slice(0, 18)} options={section.options}
render={(item) => item} render={(item) => item}
onSelect={(item) => toggleFilterList("skinNames", item)} onSelect={(item) => toggleFilterList("skinNames", item)}
/> />
))} ))}
</div> </div>
<div className="filter-footer"> <div className="advanced-toggle-row">
<button className="advanced-toggle" type="button" onClick={() => setAdvancedOpen((open) => !open)}> <button className="advanced-toggle" type="button" onClick={() => setAdvancedOpen((open) => !open)}>
{advancedOpen ? "收起高级筛选" : "展开高级筛选"} {advancedOpen ? "收起高级筛选" : "展开高级筛选"}
{advancedActiveCount > 0 ? <em className="advanced-count">{advancedActiveCount}</em> : null}
<DownOutlined rotate={advancedOpen ? 180 : 0} /> <DownOutlined rotate={advancedOpen ? 180 : 0} />
</button> </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"> <div className="filter-mobile-actions">
<Button onClick={resetFilters}></Button> <Button onClick={resetFilters}></Button>
<Button type="primary" icon={<SearchOutlined />} onClick={() => applyFilters()}> <Button type="primary" icon={<SearchOutlined />} onClick={() => applyFilters()}>
@@ -644,11 +682,13 @@ interface FilterRowProps<T> {
active: (item: T) => boolean; active: (item: T) => boolean;
render: (item: T) => string; render: (item: T) => string;
onSelect: (item: T) => void; 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 ( return (
<div className="filter-row"> <div className={fullWidth ? "filter-row filter-row-full" : "filter-row"}>
<span className="filter-label">{label}</span> <span className="filter-label">{label}</span>
<div className="chip-list"> <div className="chip-list">
{options.map((item) => { {options.map((item) => {
@@ -853,15 +893,21 @@ function buildQuery(nextPage: number, filters: Filters, sort: string): PublicLis
page_size: pageSize, page_size: pageSize,
keyword: filters.keyword.trim(), keyword: filters.keyword.trim(),
sort: backendSortKey(sort), sort: backendSortKey(sort),
zone: filters.zones.join(","), region: filters.regions.join(","),
login_method: filters.loginMethods.join(","), login_method: filters.loginMethods.join(","),
rank: filters.ranks.join(","), rank: filters.ranks.join(","),
min_coin: toNumber(filters.minCoin), min_coin: toNumber(filters.minCoin),
max_coin: toNumber(filters.maxCoin), max_coin: toNumber(filters.maxCoin),
min_ratio: toNumber(filters.minRatio),
max_ratio: toNumber(filters.maxRatio),
min_price: toNumber(filters.minPrice), min_price: toNumber(filters.minPrice),
max_price: toNumber(filters.maxPrice), max_price: toNumber(filters.maxPrice),
min_secret_kd: toNumber(filters.minKd), min_deposit: toNumber(filters.minDeposit),
max_secret_kd: toNumber(filters.maxKd), 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(","), stamina: filters.stamina.join(","),
load: filters.load.join(","), load: filters.load.join(","),
insurance: filters.insurance.join(","), insurance: filters.insurance.join(","),
@@ -870,7 +916,7 @@ function buildQuery(nextPage: number, filters: Filters, sort: string): PublicLis
} }
function backendSortKey(sort: string) { 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"; return "recommended";
} }
@@ -897,60 +943,100 @@ function cloneFilters(filters: Filters): Filters {
skinNames: [...filters.skinNames], skinNames: [...filters.skinNames],
loginMethods: [...filters.loginMethods], loginMethods: [...filters.loginMethods],
ranks: [...filters.ranks], ranks: [...filters.ranks],
zones: [...filters.zones], regions: [...filters.regions],
}; };
} }
function groupTitle(key: string, title: string) { /** 从主站 publish_options.skin_groups 取对应分组,无数据则不展示 */
if (key === "melee") return "刀皮"; function resolveSkinSections(
if (key === "operatorGold") return "干员金皮"; groups: ListingPublishOptions["skin_groups"],
if (key === "operatorRed") return "干员红皮"; defs: Array<{ key: string; title: string }>
if (key === "weapon") return "枪皮"; ) {
return title; 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) { function hasRange(min: string, max: string) {
const [rows, cols] = value.split("*").map((item) => Number(item)); return Boolean(min || max);
if (rows > 0 && cols > 0) return `${rows * cols}`; }
return value;
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) { function countActiveFilters(filters: Filters) {
let count = 0; let count = 0;
if (filters.keyword.trim()) count += 1; if (filters.keyword.trim()) count += 1;
if (filters.minCoin || filters.maxCoin) count += 1; if (hasRange(filters.minCoin, filters.maxCoin)) count += 1;
if (filters.minPrice || filters.maxPrice) count += 1; if (hasRange(filters.minRatio, filters.maxRatio)) count += 1;
if (filters.minKd || filters.maxKd) 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.stamina.length;
count += filters.load.length; count += filters.load.length;
count += filters.insurance.length; count += filters.insurance.length;
count += filters.skinNames.length; count += filters.skinNames.length;
count += filters.loginMethods.length; count += filters.loginMethods.length;
count += filters.ranks.length; count += filters.ranks.length;
count += filters.zones.length; count += filters.regions.length;
return count; 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[] = []; const items: string[] = [];
if (filters.minCoin || filters.maxCoin) { if (hasRange(filters.minCoin, filters.maxCoin)) {
items.push(`哈夫币 ${filters.minCoin || "0"}-${filters.maxCoin || "∞"}`); items.push(`哈夫币 ${formatRangeText(filters.minCoin, filters.maxCoin)}`);
} }
if (filters.minPrice || filters.maxPrice) { if (hasRange(filters.minRatio, filters.maxRatio)) {
items.push(`租金 ${filters.minPrice || "0"}-${filters.maxPrice || "∞"}`); 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.stamina) items.push(`体力 ${value}`);
for (const value of filters.load) 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.loginMethods) items.push(value);
for (const value of filters.zones) items.push(value); if (hasRange(filters.minPrice, filters.maxPrice)) {
for (const value of filters.skinNames) items.push(value); items.push(`租金 ${formatRangeText(filters.minPrice, filters.maxPrice)}`);
if (filters.minKd || filters.maxKd) {
items.push(`KD ${filters.minKd || "0"}-${filters.maxKd || "∞"}`);
} }
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()}`); 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) { function getAssetText(item: Listing, key: string) {
+11
View File
@@ -52,6 +52,7 @@ export interface PublicListingQuery {
keyword?: string; keyword?: string;
sort?: string; sort?: string;
zone?: string; zone?: string;
region?: string;
login_method?: string; login_method?: string;
rank?: string; rank?: string;
insurance?: string; insurance?: string;
@@ -60,8 +61,16 @@ export interface PublicListingQuery {
skin_name?: string; skin_name?: string;
min_coin?: number; min_coin?: number;
max_coin?: number; max_coin?: number;
min_ratio?: number;
max_ratio?: number;
min_price?: number; min_price?: number;
max_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; min_secret_kd?: number;
max_secret_kd?: number; max_secret_kd?: number;
} }
@@ -81,6 +90,7 @@ export interface ListingPublishOptions {
rank_options: string[]; rank_options: string[];
insurance_options: string[]; insurance_options: string[];
level_options: string[]; level_options: string[];
region_options: string[];
skin_groups: Array<{ skin_groups: Array<{
key: string; key: string;
title: string; title: string;
@@ -141,6 +151,7 @@ function normalizeOptions(value: unknown): ListingPublishOptions {
rank_options: normalizeStringArray(options.rank_options), rank_options: normalizeStringArray(options.rank_options),
insurance_options: normalizeStringArray(options.insurance_options), insurance_options: normalizeStringArray(options.insurance_options),
level_options: normalizeStringArray(options.level_options), level_options: normalizeStringArray(options.level_options),
region_options: normalizeStringArray(options.region_options),
skin_groups: normalizeSkinGroups(options.skin_groups), skin_groups: normalizeSkinGroups(options.skin_groups),
}; };
} }
+40 -79
View File
@@ -412,8 +412,11 @@ input {
gap: 12px; gap: 12px;
} }
.filter-label, .filter-row-full {
.range-fields > label > span { grid-column: 1 / -1;
}
.filter-label {
min-height: 30px; min-height: 30px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -472,71 +475,27 @@ input {
transform: translateY(1px); 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 { .advanced-panel {
display: none; display: none;
padding-top: 16px; padding-top: 16px;
border-top: 1px dashed #e8edf3; border-top: 1px dashed #e8edf3;
margin-top: 16px; margin-top: 4px;
} }
.advanced-panel.open { .advanced-panel.open {
display: grid; display: block;
gap: 14px;
} }
.range-fields { .advanced-toggle-row {
display: grid; display: flex;
grid-template-columns: repeat(2, minmax(0, 1fr)); justify-content: center;
gap: 16px 38px; padding: 8px 0 4px;
}
.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;
} }
.filter-footer { .filter-footer {
justify-content: center; justify-content: center;
min-height: 44px; min-height: 0;
padding-top: 4px; padding-top: 10px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
} }
@@ -565,6 +524,22 @@ input {
font-size: 11px; 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 { .result-area {
margin-top: 14px; margin-top: 14px;
padding: 16px 16px 28px; padding: 16px 16px 28px;
@@ -1146,8 +1121,7 @@ input {
width: calc(100% - 24px); width: calc(100% - 24px);
} }
.filter-grid, .filter-grid {
.range-fields {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1310,17 +1284,14 @@ input {
gap: 10px; gap: 10px;
} }
.filter-row, .filter-row {
.range-fields > label { grid-template-columns: 52px minmax(0, 1fr);
grid-template-columns: 44px minmax(0, 1fr);
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.filter-label, .filter-label,
.range-fields > label > span, .filter-row > span.filter-label {
.filter-row > span.filter-label,
.compact-select > .filter-label {
min-height: 28px; min-height: 28px;
font-size: 12px; font-size: 12px;
color: #667085; color: #667085;
@@ -1349,12 +1320,11 @@ input {
font-size: 12px; font-size: 12px;
} }
.compact-select .ant-select { /* 全宽筛选(皮肤/地区)在移动端允许换行,避免横向滚太长 */
width: 100%; .filter-row-full .chip-list {
} flex-wrap: wrap;
overflow: visible;
.compact-select .ant-select-selector { mask-image: none;
min-height: 32px !important;
} }
.advanced-panel { .advanced-panel {
@@ -1362,13 +1332,8 @@ input {
padding-top: 12px; padding-top: 12px;
} }
.advanced-panel.open { .advanced-toggle-row {
gap: 10px; padding: 4px 0 0;
}
.range-fields {
grid-template-columns: 1fr;
gap: 10px;
} }
.filter-footer { .filter-footer {
@@ -1420,10 +1385,6 @@ input {
font-size: 12px; font-size: 12px;
} }
.compact-select .ant-select {
width: 100%;
}
.sort-toolbar { .sort-toolbar {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;