From b36ec48edd7dcc12e0ff8ca725aa2a9a4a90d2c7 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 16 Jul 2026 23:50:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E4=B8=BA=E5=8F=AF=E6=8A=98=E5=8F=A0=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=20key=20=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 筛选默认收起、展开后单行横滑;chip 使用唯一 key 消除 React 警告。 --- src/App.tsx | 337 ++++++++++++++++++++++++++---------------- src/listingDisplay.ts | 24 +-- src/styles.css | 229 +++++++++++++++++++++++++++- 3 files changed, 443 insertions(+), 147 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3f44043..780a9b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import CopyOutlined from "@ant-design/icons/es/icons/CopyOutlined"; import CustomerServiceOutlined from "@ant-design/icons/es/icons/CustomerServiceOutlined"; import DownOutlined from "@ant-design/icons/es/icons/DownOutlined"; +import FilterOutlined from "@ant-design/icons/es/icons/FilterOutlined"; import ReloadOutlined from "@ant-design/icons/es/icons/ReloadOutlined"; import SearchOutlined from "@ant-design/icons/es/icons/SearchOutlined"; import Alert from "antd/es/alert"; @@ -160,6 +161,7 @@ function Showcase() { const [filters, setFilters] = useState(cloneFilters(emptyFilters)); const [appliedFilters, setAppliedFilters] = useState(cloneFilters(emptyFilters)); const [activeSort, setActiveSort] = useState("recommended"); + const [filterOpen, setFilterOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [copiedCode, setCopiedCode] = useState(""); const [copiedInfoCode, setCopiedInfoCode] = useState(""); @@ -196,6 +198,8 @@ function Showcase() { }, [publishOptions.skin_groups]); const noticeText = announcements[0]?.trim() || ""; + const activeFilterCount = useMemo(() => countActiveFilters(filters), [filters]); + const appliedFilterCount = useMemo(() => countActiveFilters(appliedFilters), [appliedFilters]); const displayListings = useMemo(() => { const items = [...listings]; @@ -262,6 +266,7 @@ function Showcase() { function applyFilters(nextFilters = filters) { setAppliedFilters(cloneFilters(nextFilters)); setPage(1); + setFilterOpen(false); } function resetFilters() { @@ -270,6 +275,8 @@ function Showcase() { setAppliedFilters(cloneFilters(nextFilters)); setActiveSort("recommended"); setPage(1); + setFilterOpen(false); + setAdvancedOpen(false); } function selectRange(minKey: "minCoin" | "minPrice", maxKey: "maxCoin" | "maxPrice", min: string, max: string) { @@ -381,141 +388,173 @@ function Showcase() { ) : null} -
-
- - 筛选条件 -
-
- filters.minCoin === item.min && filters.maxCoin === item.max} - label="哈夫币(M)" - options={coinRanges} - render={(item) => item.label} - onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)} - /> - filters.minPrice === item.min && filters.maxPrice === item.max} - label="租金" - options={priceRanges} - render={(item) => item.label} - onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)} - /> - filters.insurance.includes(item)} - label="保险" - options={insuranceOptions} - render={insuranceLabel} - onSelect={(item) => toggleFilterList("insurance", item)} - /> - filters.stamina.includes(item)} - label="体力" - options={levelOptions} - render={(item) => item} - onSelect={(item) => toggleFilterList("stamina", item)} - /> - filters.load.includes(item)} - label="负重" - options={levelOptions} - render={(item) => item} - onSelect={(item) => toggleFilterList("load", item)} - /> -
- IP地址 - ({ label: item, value: item }))} - placeholder="不限" - value={filters.loginMethods} - onChange={(value) => updateFilter("loginMethods", value)} - /> -
-
- 段位 - updateFilter("minCoin", event.target.value)} - /> - - updateFilter("maxCoin", event.target.value)} - /> -
- - + {appliedFilterCount > 0 && !filterOpen ? ( +
+ {buildFilterSummary(appliedFilters, insuranceLabel).map((item) => ( + + {item} + + ))}
- {skinSections.map((section) => ( + ) : null} + +
+
filters.skinNames.includes(item)} - label={section.title} - options={section.options.slice(0, 18)} - render={(item) => item} - onSelect={(item) => toggleFilterList("skinNames", item)} + active={(item) => filters.minCoin === item.min && filters.maxCoin === item.max} + label="哈夫币" + options={coinRanges} + render={(item) => item.label} + onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)} /> - ))} -
+ filters.minPrice === item.min && filters.maxPrice === item.max} + label="租金" + options={priceRanges} + render={(item) => item.label} + onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)} + /> + filters.insurance.includes(item)} + label="保险" + options={insuranceOptions} + render={insuranceLabel} + onSelect={(item) => toggleFilterList("insurance", item)} + /> + filters.stamina.includes(item)} + label="体力" + options={levelOptions} + render={(item) => item} + onSelect={(item) => toggleFilterList("stamina", item)} + /> + filters.load.includes(item)} + label="负重" + options={levelOptions} + render={(item) => item} + onSelect={(item) => toggleFilterList("load", item)} + /> +
+ IP地址 + ({ label: item, value: item }))} + placeholder="不限" + value={filters.loginMethods} + onChange={(value) => updateFilter("loginMethods", value)} + /> +
+
+ 段位 + updateFilter("minCoin", event.target.value)} + /> + + updateFilter("maxCoin", event.target.value)} + /> +
+ + +
+ {skinSections.map((section) => ( + filters.skinNames.includes(item)} + label={section.title} + options={section.options.slice(0, 18)} + render={(item) => item} + onSelect={(item) => toggleFilterList("skinNames", item)} + /> + ))} +
+ +
+ +
+ + +
+
@@ -706,7 +745,7 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li {getMainChips(item) .slice(0, 5) .map((chip) => ( - + {chip.label} ))} @@ -872,6 +911,44 @@ function insuranceLabel(value: string) { return value; } +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; + 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; + return count; +} + +function buildFilterSummary(filters: Filters, formatInsurance: (value: string) => string) { + const items: string[] = []; + if (filters.minCoin || filters.maxCoin) { + items.push(`哈夫币 ${filters.minCoin || "0"}-${filters.maxCoin || "∞"}`); + } + if (filters.minPrice || filters.maxPrice) { + items.push(`租金 ${filters.minPrice || "0"}-${filters.maxPrice || "∞"}`); + } + for (const value of filters.insurance) items.push(`保险 ${formatInsurance(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 (filters.keyword.trim()) items.push(`编号 ${filters.keyword.trim()}`); + return items.slice(0, 6); +} + function getAssetText(item: Listing, key: string) { const value = item.asset_summary?.[key]; return typeof value === "string" ? value : ""; diff --git a/src/listingDisplay.ts b/src/listingDisplay.ts index d9bb691..f1e8742 100644 --- a/src/listingDisplay.ts +++ b/src/listingDisplay.ts @@ -200,16 +200,20 @@ export function getOnlineTimeText(item: Listing) { export function getMainChips(item: Listing) { return [ - { label: readAssetString(item, "stamina_level") || "体力--", tone: "green" }, - { label: readAssetString(item, "load_level") || "负重--", tone: "green" }, - { label: readAssetString(item, "season_insurance") || "保险--", tone: "red" }, - { label: item.rank_level || "段位--", tone: "red" }, - { label: `${getResourceQuantity(item, "armor6")}甲`, tone: "red" }, - { label: `${getResourceQuantity(item, "helmet6")}头`, tone: "red" }, - { label: `AWM子弹:${getResourceQuantity(item, "awmAmmo")}发`, tone: "red" }, - { label: `KD:${readAssetNumber(item, "secret_kd") || "--"}`, tone: "red" }, - ...getRegions(item).slice(0, 2).map((region) => ({ label: region, tone: "purple" })), - ...getSkinNames(item).slice(0, 2).map((skin) => ({ label: skin, tone: "gold" })), + { key: "stamina", label: readAssetString(item, "stamina_level") || "体力--", tone: "green" }, + { key: "load", label: readAssetString(item, "load_level") || "负重--", tone: "green" }, + { key: "insurance", label: readAssetString(item, "season_insurance") || "保险--", tone: "red" }, + { key: "rank", label: item.rank_level || "段位--", tone: "red" }, + { key: "armor6", label: `${getResourceQuantity(item, "armor6")}甲`, tone: "red" }, + { key: "helmet6", label: `${getResourceQuantity(item, "helmet6")}头`, tone: "red" }, + { key: "awm", label: `AWM子弹:${getResourceQuantity(item, "awmAmmo")}发`, tone: "red" }, + { key: "kd", label: `KD:${readAssetNumber(item, "secret_kd") || "--"}`, tone: "red" }, + ...getRegions(item) + .slice(0, 2) + .map((region, index) => ({ key: `region-${index}`, label: region, tone: "purple" })), + ...getSkinNames(item) + .slice(0, 2) + .map((skin, index) => ({ key: `skin-${index}`, label: skin, tone: "gold" })), ].filter((chip) => chip.label && !chip.label.includes(":0发") && chip.label !== "0甲" && chip.label !== "0头"); } diff --git a/src/styles.css b/src/styles.css index fdd3f4b..9d559c8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -317,6 +317,69 @@ input { padding: 18px 22px 12px; } +.filter-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + margin: 0 0 14px; + padding: 0; + border: none; + background: transparent; + color: inherit; + text-align: left; + cursor: default; +} + +.filter-toggle .section-title { + margin-bottom: 0; +} + +.filter-title-icon { + display: none; + color: var(--brand); + font-size: 14px; +} + +.filter-count { + min-width: 18px; + height: 18px; + padding: 0 6px; + 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; +} + +.filter-toggle-meta { + display: none; + align-items: center; + gap: 6px; + color: #667085; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.filter-toggle-meta .anticon { + font-size: 11px; +} + +.filter-summary { + display: none; +} + +.filter-mobile-actions { + display: none; +} + .section-title { display: flex; align-items: center; @@ -474,6 +537,8 @@ input { justify-content: center; min-height: 44px; padding-top: 4px; + flex-wrap: wrap; + gap: 10px; } .advanced-toggle { @@ -1155,8 +1220,78 @@ input { } .filter-board { - padding: 14px 14px 10px; + padding: 0; border-radius: 12px; + overflow: hidden; + } + + .filter-toggle { + margin: 0; + padding: 12px 14px; + cursor: pointer; + transition: background 0.15s ease; + } + + .filter-toggle:active { + background: #fafbfc; + } + + .filter-board.is-open .filter-toggle { + border-bottom: 1px solid #eef2f6; + } + + .filter-title-icon { + display: inline-flex; + } + + .filter-toggle .section-title i { + display: none; + } + + .filter-toggle-meta { + display: inline-flex; + } + + .filter-summary { + display: flex; + flex-wrap: nowrap; + gap: 6px; + padding: 0 14px 12px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .filter-summary::-webkit-scrollbar { + display: none; + } + + .filter-summary-chip { + flex: none; + height: 26px; + padding: 0 10px; + display: inline-flex; + align-items: center; + border-radius: 999px; + color: var(--brand-dark); + background: var(--brand-soft); + border: 1px solid var(--brand-line); + font-size: 11px; + font-weight: 800; + white-space: nowrap; + } + + .filter-body { + display: none; + padding: 12px 14px 14px; + } + + .filter-board.is-open .filter-body { + display: block; + } + + .filter-board.is-open .filter-summary { + display: none; } .result-area { @@ -1166,24 +1301,104 @@ input { } .section-title { - margin-bottom: 10px; + margin-bottom: 0; font-size: 14px; } + .filter-grid { + grid-template-columns: 1fr; + gap: 10px; + } + .filter-row, .range-fields > label { - grid-template-columns: 1fr; - gap: 7px; + grid-template-columns: 44px minmax(0, 1fr); + align-items: center; + gap: 8px; + } + + .filter-label, + .range-fields > label > span, + .filter-row > span.filter-label, + .compact-select > .filter-label { + min-height: 28px; + font-size: 12px; + color: #667085; } .chip-list { + flex-wrap: nowrap; gap: 6px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding-bottom: 1px; + mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 18px), transparent 100%); + } + + .chip-list::-webkit-scrollbar { + display: none; } .chip-list button { - min-height: 32px; - height: 32px; - padding: 0 11px; + flex: none; + min-width: 0; + min-height: 28px; + height: 28px; + padding: 0 10px; + font-size: 12px; + } + + .compact-select .ant-select { + width: 100%; + } + + .compact-select .ant-select-selector { + min-height: 32px !important; + } + + .advanced-panel { + margin-top: 12px; + padding-top: 12px; + } + + .advanced-panel.open { + gap: 10px; + } + + .range-fields { + grid-template-columns: 1fr; + gap: 10px; + } + + .filter-footer { + flex-direction: column; + align-items: stretch; + min-height: 0; + margin-top: 4px; + padding-top: 10px; + gap: 10px; + } + + .filter-mobile-actions { + display: grid; + grid-template-columns: 1fr 1.6fr; + gap: 8px; + } + + .filter-mobile-actions .ant-btn { + height: 40px; + border-radius: 10px; + font-weight: 800; + } + + .filter-mobile-actions .ant-btn-primary { + box-shadow: 0 6px 14px rgba(255, 107, 0, 0.18); + } + + .advanced-toggle { + justify-content: center; + width: 100%; } .sort-segment {