对齐主站筛选结构并修复 AWM 子弹排序
基础/高级筛选项顺序与主站一致,枚举选项改为读取 mobile-home-config;AWM 排序改为按子弹数量降序。
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
+255
-169
@@ -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)}
|
||||
/>
|
||||
</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) => (
|
||||
<FilterRow
|
||||
active={(item) => filters.loginMethods.includes(item)}
|
||||
label="登录方式"
|
||||
options={loginOptions}
|
||||
render={(item) => item}
|
||||
onSelect={(item) => toggleFilterList("loginMethods", item)}
|
||||
/>
|
||||
{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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user