2079 lines
53 KiB
Vue
2079 lines
53 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||
import {
|
||
ArrowDown,
|
||
Bell,
|
||
Filter,
|
||
Refresh,
|
||
} from "@element-plus/icons-vue";
|
||
|
||
import {
|
||
defaultHomeAnnouncements,
|
||
defaultHomeBanners,
|
||
fetchMobileHomeConfig,
|
||
type HomeBannerSlide,
|
||
} from "@/api/homeConfig";
|
||
import {
|
||
emptyListingPublishOptions,
|
||
type ListingPublishOptions,
|
||
} from "@/api/listingOptions";
|
||
import { fetchListingsPage, type Listing, type PublicListingQuery } from "@/api/listings";
|
||
import {
|
||
formatHafCoinM,
|
||
getCoinWan,
|
||
getListingDisplayPrice,
|
||
getListingTitle,
|
||
getLoginMethod,
|
||
getServerRegion,
|
||
hasAcceleratedSaleRatio,
|
||
hasGiftResources,
|
||
assetRegions,
|
||
readAssetNumber,
|
||
readAssetString,
|
||
getResourceQuantity,
|
||
getOnlineTimeText,
|
||
getSkinNames,
|
||
getSkinGroup,
|
||
getRatioValue,
|
||
getDailyLoss,
|
||
} from "@/utils/listingDisplay";
|
||
|
||
const loading = ref(false);
|
||
const loadingMore = ref(false);
|
||
const listings = ref<Listing[]>([]);
|
||
const totalListings = ref(0);
|
||
const zoneCounts = ref<Record<string, number>>({});
|
||
const currentPage = ref(1);
|
||
const hasMoreListings = ref(true);
|
||
const announcements = ref<string[]>(defaultHomeAnnouncements);
|
||
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners);
|
||
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||
const filters = reactive({
|
||
keyword: "",
|
||
server: "",
|
||
region: "",
|
||
loginMethod: "",
|
||
rank: "",
|
||
insurance: "",
|
||
stamina: "",
|
||
load: "",
|
||
skinGroup: "",
|
||
skinName: "",
|
||
minCoin: undefined as number | undefined,
|
||
maxCoin: undefined as number | undefined,
|
||
minPrice: undefined as number | undefined,
|
||
maxPrice: undefined as number | undefined,
|
||
minDeposit: undefined as number | undefined,
|
||
maxDeposit: undefined as number | undefined,
|
||
minTotal: undefined as number | undefined,
|
||
maxTotal: undefined as number | undefined,
|
||
minFireLevel: undefined as number | undefined,
|
||
maxFireLevel: undefined as number | undefined,
|
||
});
|
||
const sortBy = ref("recommended");
|
||
const activeZone = ref("all");
|
||
const homePageSize = 12;
|
||
let listingRequestSeq = 0;
|
||
type FilterPopoverKey =
|
||
| "insurance"
|
||
| "stamina"
|
||
| "load"
|
||
| "region"
|
||
| "coin"
|
||
| "price"
|
||
| "deposit"
|
||
| "total"
|
||
| "skin"
|
||
| "rank"
|
||
| "fireLevel"
|
||
| "loginMethod";
|
||
type StringFilterKey =
|
||
| "insurance"
|
||
| "stamina"
|
||
| "load"
|
||
| "region"
|
||
| "rank"
|
||
| "loginMethod";
|
||
const activeFilterPopover = ref<FilterPopoverKey | "">("");
|
||
const filterPopoverBaseProps = {
|
||
placement: "bottom-start",
|
||
popperClass: "home-filter-popover",
|
||
trigger: "click",
|
||
showAfter: 0,
|
||
hideAfter: 0,
|
||
transition: "none",
|
||
} as const;
|
||
|
||
const coinRangeOptions = [
|
||
{ label: "全部区间", min: undefined, max: undefined },
|
||
{ label: "0-100", min: 0, max: 100 },
|
||
{ label: "100-300", min: 100, max: 300 },
|
||
{ label: "300-500", min: 300, max: 500 },
|
||
{ label: "500+", min: 500, max: undefined },
|
||
];
|
||
const moneyRangeOptions = [
|
||
{ label: "全部区间", min: undefined, max: undefined },
|
||
{ label: "0-50", min: 0, max: 50 },
|
||
{ label: "50-100", min: 50, max: 100 },
|
||
{ label: "100-200", min: 100, max: 200 },
|
||
{ label: "200+", min: 200, max: undefined },
|
||
];
|
||
const totalRangeOptions = [
|
||
{ label: "全部区间", min: undefined, max: undefined },
|
||
{ 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 },
|
||
];
|
||
const fireLevelRangeOptions = [
|
||
{ label: "全部等级", min: undefined, max: undefined },
|
||
{ label: "38-50", min: 38, max: 50 },
|
||
{ label: "50-60", min: 50, max: 60 },
|
||
{ label: "60-70", min: 60, max: 70 },
|
||
{ label: "70+", min: 70, max: undefined },
|
||
];
|
||
|
||
const regionOptions = computed(() =>
|
||
uniqueOptions([
|
||
...publishOptions.value.region_options,
|
||
...listings.value.flatMap((item) => assetRegions(item)),
|
||
])
|
||
);
|
||
const loginMethodOptions = computed(() =>
|
||
uniqueOptions(
|
||
publishOptions.value.login_method_options
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
)
|
||
);
|
||
const skinFilterGroups = computed(() => {
|
||
const preferred = ["operatorRed", "operatorGold"];
|
||
return preferred
|
||
.map((key) => publishOptions.value.skin_groups.find((group) => group.key === key))
|
||
.filter((group): group is ListingPublishOptions["skin_groups"][number] => Boolean(group));
|
||
});
|
||
const skinChipLabel = computed(() => {
|
||
if (filters.skinName) return filters.skinName;
|
||
if (filters.skinGroup) {
|
||
return skinFilterGroups.value.find((group) => group.key === filters.skinGroup)?.title || "皮肤";
|
||
}
|
||
return "皮肤";
|
||
});
|
||
|
||
const visibleListings = computed(() => {
|
||
return listings.value;
|
||
});
|
||
|
||
const statCards = computed(() => [
|
||
{ label: "可租账号", value: `${totalListings.value}`, hint: "当前筛选结果" },
|
||
{
|
||
label: "高哈夫币",
|
||
value: `${zoneCount("highCoin")}`,
|
||
hint: "100M 以上",
|
||
},
|
||
{
|
||
label: "账密登录",
|
||
value: `${zoneCount("password")}`,
|
||
hint: "交接更快",
|
||
},
|
||
]);
|
||
|
||
const zoneOptions = computed(() => [
|
||
{
|
||
key: "all",
|
||
label: "全部专区",
|
||
hint: "当前可租账号",
|
||
count: zoneCount("all"),
|
||
},
|
||
{
|
||
key: "sale",
|
||
label: "特惠专区",
|
||
hint: "价格更划算",
|
||
count: zoneCount("sale"),
|
||
},
|
||
{
|
||
key: "gift",
|
||
label: "赠送专区",
|
||
hint: "含赠送物品",
|
||
count: zoneCount("gift"),
|
||
},
|
||
{
|
||
key: "night",
|
||
label: "夜间专区",
|
||
hint: "夜间也好上号",
|
||
count: zoneCount("night"),
|
||
},
|
||
{
|
||
key: "password",
|
||
label: "账密专区",
|
||
hint: "交接更快",
|
||
count: zoneCount("password"),
|
||
},
|
||
{
|
||
key: "highCoin",
|
||
label: "高币专区",
|
||
hint: "100M 以上",
|
||
count: zoneCount("highCoin"),
|
||
},
|
||
]);
|
||
|
||
onMounted(() => {
|
||
loadHome();
|
||
window.addEventListener("scroll", handleWindowScroll, { passive: true });
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener("scroll", handleWindowScroll);
|
||
});
|
||
|
||
watch(
|
||
() => listingQuerySignature(),
|
||
() => {
|
||
loadListingsPage(true);
|
||
}
|
||
);
|
||
|
||
async function loadHome() {
|
||
loading.value = true;
|
||
try {
|
||
const [, config] = await Promise.all([
|
||
loadListingsPage(true),
|
||
fetchMobileHomeConfig(),
|
||
]);
|
||
announcements.value = config.announcements;
|
||
banners.value = config.banners;
|
||
publishOptions.value = config.publish_options;
|
||
} catch {
|
||
announcements.value = defaultHomeAnnouncements;
|
||
banners.value = defaultHomeBanners;
|
||
publishOptions.value = emptyListingPublishOptions;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadListingsPage(reset = false) {
|
||
if ((loadingMore.value && !reset) || (!hasMoreListings.value && !reset)) return;
|
||
const requestSeq = ++listingRequestSeq;
|
||
if (reset) {
|
||
currentPage.value = 1;
|
||
hasMoreListings.value = true;
|
||
}
|
||
loadingMore.value = true;
|
||
try {
|
||
const page = await fetchListingsPage(buildListingQuery(currentPage.value));
|
||
if (requestSeq !== listingRequestSeq) return;
|
||
listings.value = reset ? page.items : [...listings.value, ...page.items];
|
||
totalListings.value = page.total;
|
||
zoneCounts.value = page.zone_counts;
|
||
hasMoreListings.value = listings.value.length < page.total;
|
||
currentPage.value = page.page + 1;
|
||
requestAnimationFrame(handleWindowScroll);
|
||
} catch {
|
||
if (reset) {
|
||
listings.value = [];
|
||
totalListings.value = 0;
|
||
zoneCounts.value = {};
|
||
hasMoreListings.value = false;
|
||
}
|
||
} finally {
|
||
if (requestSeq === listingRequestSeq) {
|
||
loadingMore.value = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildListingQuery(page: number): PublicListingQuery {
|
||
return {
|
||
page,
|
||
page_size: homePageSize,
|
||
keyword: filters.keyword.trim(),
|
||
sort: sortBy.value,
|
||
zone: activeZone.value,
|
||
server: filters.server,
|
||
region: filters.region,
|
||
login_method: filters.loginMethod,
|
||
rank: filters.rank,
|
||
insurance: filters.insurance,
|
||
stamina: filters.stamina,
|
||
load: filters.load,
|
||
skin_group: filters.skinGroup,
|
||
skin_name: filters.skinName,
|
||
min_coin: filters.minCoin,
|
||
max_coin: filters.maxCoin,
|
||
min_price: filters.minPrice,
|
||
max_price: filters.maxPrice,
|
||
min_deposit: filters.minDeposit,
|
||
max_deposit: filters.maxDeposit,
|
||
min_total: filters.minTotal,
|
||
max_total: filters.maxTotal,
|
||
min_fire_level: filters.minFireLevel,
|
||
max_fire_level: filters.maxFireLevel,
|
||
};
|
||
}
|
||
|
||
function listingQuerySignature() {
|
||
return JSON.stringify(buildListingQuery(1));
|
||
}
|
||
|
||
function handleWindowScroll() {
|
||
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 480) return;
|
||
loadListingsPage(false);
|
||
}
|
||
|
||
function zoneCount(key: string) {
|
||
if (key === "all") return zoneCounts.value.all ?? totalListings.value;
|
||
return zoneCounts.value[key] ?? 0;
|
||
}
|
||
|
||
function resetFilters() {
|
||
filters.keyword = "";
|
||
filters.server = "";
|
||
filters.region = "";
|
||
filters.loginMethod = "";
|
||
filters.rank = "";
|
||
filters.insurance = "";
|
||
filters.stamina = "";
|
||
filters.load = "";
|
||
filters.skinGroup = "";
|
||
filters.skinName = "";
|
||
filters.minCoin = undefined;
|
||
filters.maxCoin = undefined;
|
||
filters.minPrice = undefined;
|
||
filters.maxPrice = undefined;
|
||
filters.minDeposit = undefined;
|
||
filters.maxDeposit = undefined;
|
||
filters.minTotal = undefined;
|
||
filters.maxTotal = undefined;
|
||
filters.minFireLevel = undefined;
|
||
filters.maxFireLevel = undefined;
|
||
activeZone.value = "all";
|
||
sortBy.value = "recommended";
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setActiveZone(zoneKey: string) {
|
||
activeZone.value = zoneKey;
|
||
}
|
||
|
||
function uniqueOptions(values: string[]) {
|
||
return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
|
||
}
|
||
|
||
function rangeLabel(min: number | undefined, max: number | undefined, fallback: string) {
|
||
if (min !== undefined && max !== undefined) return `${min}-${max}`;
|
||
if (min !== undefined) return `${min}+`;
|
||
if (max !== undefined) return `≤${max}`;
|
||
return fallback;
|
||
}
|
||
|
||
function isRangeSelected(
|
||
currentMin: number | undefined,
|
||
currentMax: number | undefined,
|
||
min: number | undefined,
|
||
max: number | undefined,
|
||
) {
|
||
return currentMin === min && currentMax === max;
|
||
}
|
||
|
||
function setCoinRange(min: number | undefined, max: number | undefined) {
|
||
filters.minCoin = min;
|
||
filters.maxCoin = max;
|
||
}
|
||
|
||
function setPriceRange(min: number | undefined, max: number | undefined) {
|
||
filters.minPrice = min;
|
||
filters.maxPrice = max;
|
||
}
|
||
|
||
function setDepositRange(min: number | undefined, max: number | undefined) {
|
||
filters.minDeposit = min;
|
||
filters.maxDeposit = max;
|
||
}
|
||
|
||
function setTotalRange(min: number | undefined, max: number | undefined) {
|
||
filters.minTotal = min;
|
||
filters.maxTotal = max;
|
||
}
|
||
|
||
function setFireLevelRange(min: number | undefined, max: number | undefined) {
|
||
filters.minFireLevel = min;
|
||
filters.maxFireLevel = max;
|
||
}
|
||
|
||
function setFilterPopover(key: FilterPopoverKey, visible: boolean) {
|
||
if (visible) {
|
||
activeFilterPopover.value = key;
|
||
return;
|
||
}
|
||
if (activeFilterPopover.value === key) activeFilterPopover.value = "";
|
||
}
|
||
|
||
function filterPopoverProps(key: FilterPopoverKey) {
|
||
return {
|
||
...filterPopoverBaseProps,
|
||
visible: activeFilterPopover.value === key,
|
||
"onUpdate:visible": (visible: boolean) => setFilterPopover(key, visible),
|
||
};
|
||
}
|
||
|
||
function closeFilterPopover() {
|
||
activeFilterPopover.value = "";
|
||
}
|
||
|
||
function setStringFilter(key: StringFilterKey, value: string) {
|
||
filters[key] = value;
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setCoinRangeAndClose(min: number | undefined, max: number | undefined) {
|
||
setCoinRange(min, max);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setPriceRangeAndClose(min: number | undefined, max: number | undefined) {
|
||
setPriceRange(min, max);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setDepositRangeAndClose(min: number | undefined, max: number | undefined) {
|
||
setDepositRange(min, max);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setTotalRangeAndClose(min: number | undefined, max: number | undefined) {
|
||
setTotalRange(min, max);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setFireLevelRangeAndClose(min: number | undefined, max: number | undefined) {
|
||
setFireLevelRange(min, max);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function setSkinFilter(groupKey: string, skinName = "") {
|
||
filters.skinGroup = groupKey;
|
||
filters.skinName = skinName;
|
||
}
|
||
|
||
function setSkinFilterAndClose(groupKey: string, skinName = "") {
|
||
setSkinFilter(groupKey, skinName);
|
||
closeFilterPopover();
|
||
}
|
||
|
||
function resetSkinFilter() {
|
||
filters.skinGroup = "";
|
||
filters.skinName = "";
|
||
}
|
||
|
||
function resetSkinFilterAndClose() {
|
||
resetSkinFilter();
|
||
closeFilterPopover();
|
||
}
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<section class="pc-home-redesign">
|
||
<!-- 滚动公告 (置顶于主内容上方) -->
|
||
<div class="home-announcement">
|
||
<el-icon><Bell /></el-icon>
|
||
<el-carousel
|
||
height="22px"
|
||
direction="vertical"
|
||
indicator-position="none"
|
||
:autoplay="true"
|
||
:interval="3200"
|
||
>
|
||
<el-carousel-item v-for="item in announcements" :key="item">
|
||
<span>{{ item }}</span>
|
||
</el-carousel-item>
|
||
</el-carousel>
|
||
</div>
|
||
|
||
<main class="home-content">
|
||
<!-- 轮播图 & 统计卡片 -->
|
||
<div class="hero-section">
|
||
<section class="hero-board">
|
||
<el-carousel height="240px" indicator-position="outside" :interval="3600">
|
||
<el-carousel-item v-for="slide in banners" :key="slide.title || slide.image_url">
|
||
<div
|
||
class="hero-slide"
|
||
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
|
||
>
|
||
<img
|
||
v-if="slide.image_url"
|
||
:src="slide.image_url"
|
||
:alt="slide.title || slide.eyebrow || '首页轮播'"
|
||
/>
|
||
<div class="hero-copy">
|
||
<span v-if="slide.eyebrow">{{ slide.eyebrow }}</span>
|
||
<h1 v-if="slide.title">{{ slide.title }}</h1>
|
||
<p v-if="slide.pill">{{ slide.pill }}</p>
|
||
</div>
|
||
<em v-if="slide.badge">{{ slide.badge }}</em>
|
||
</div>
|
||
</el-carousel-item>
|
||
</el-carousel>
|
||
</section>
|
||
|
||
<div class="home-stats-v2">
|
||
<div v-for="item in statCards" :key="item.label" class="stat-item">
|
||
<div class="stat-main">
|
||
<strong>{{ item.value }}</strong>
|
||
<span>{{ item.label }}</span>
|
||
</div>
|
||
<small>{{ item.hint }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 增强版横向筛选栏 -->
|
||
<section class="horizontal-filter-card">
|
||
<div class="filter-header">
|
||
<div class="filter-title">
|
||
<el-icon><Filter /></el-icon>
|
||
<strong>筛选大厅</strong>
|
||
<span>{{ totalListings }} 个结果</span>
|
||
</div>
|
||
<el-button :icon="Refresh" link @click="resetFilters">重置全部条件</el-button>
|
||
</div>
|
||
|
||
<div class="filter-chip-row">
|
||
<el-popover v-bind="filterPopoverProps('insurance')" :width="220">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.insurance }" type="button">
|
||
<span>{{ filters.insurance || "保险" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.insurance }" @click="setStringFilter('insurance', '')">全部保险</button>
|
||
<button
|
||
v-for="item in publishOptions.insurance_options"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.insurance === item }"
|
||
@click="setStringFilter('insurance', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('stamina')" :width="220">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.stamina }" type="button">
|
||
<span>{{ filters.stamina || "体力" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.stamina }" @click="setStringFilter('stamina', '')">全部体力</button>
|
||
<button
|
||
v-for="item in publishOptions.level_options"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.stamina === item }"
|
||
@click="setStringFilter('stamina', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('load')" :width="220">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.load }" type="button">
|
||
<span>{{ filters.load || "负重" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.load }" @click="setStringFilter('load', '')">全部负重</button>
|
||
<button
|
||
v-for="item in publishOptions.level_options"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.load === item }"
|
||
@click="setStringFilter('load', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('region')" :width="240">
|
||
<template #reference>
|
||
<button class="filter-chip wide" :class="{ active: filters.region }" type="button">
|
||
<span>{{ filters.region || "地区选择" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.region }" @click="setStringFilter('region', '')">全部省市</button>
|
||
<button
|
||
v-for="item in regionOptions"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.region === item }"
|
||
@click="setStringFilter('region', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('coin')" :width="350">
|
||
<template #reference>
|
||
<button class="filter-chip wide" :class="{ active: filters.minCoin !== undefined || filters.maxCoin !== undefined }" type="button">
|
||
<span>{{ rangeLabel(filters.minCoin, filters.maxCoin, "哈夫币(M)") }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="range-menu">
|
||
<button
|
||
v-for="item in coinRangeOptions"
|
||
:key="item.label"
|
||
type="button"
|
||
:class="{ active: isRangeSelected(filters.minCoin, filters.maxCoin, item.min, item.max) }"
|
||
@click="setCoinRangeAndClose(item.min, item.max)"
|
||
>
|
||
{{ item.label }}
|
||
</button>
|
||
<div class="range-manual">
|
||
<el-input-number v-model="filters.minCoin" :controls="false" :min="0" placeholder="最小值" />
|
||
<span>-</span>
|
||
<el-input-number v-model="filters.maxCoin" :controls="false" :min="0" placeholder="最大值" />
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('price')" :width="350">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.minPrice !== undefined || filters.maxPrice !== undefined }" type="button">
|
||
<span>{{ rangeLabel(filters.minPrice, filters.maxPrice, "租金") }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="range-menu">
|
||
<button
|
||
v-for="item in moneyRangeOptions"
|
||
:key="item.label"
|
||
type="button"
|
||
:class="{ active: isRangeSelected(filters.minPrice, filters.maxPrice, item.min, item.max) }"
|
||
@click="setPriceRangeAndClose(item.min, item.max)"
|
||
>
|
||
{{ item.label }}
|
||
</button>
|
||
<div class="range-manual">
|
||
<el-input-number v-model="filters.minPrice" :controls="false" :min="0" placeholder="最小值" />
|
||
<span>-</span>
|
||
<el-input-number v-model="filters.maxPrice" :controls="false" :min="0" placeholder="最大值" />
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('deposit')" :width="350">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.minDeposit !== undefined || filters.maxDeposit !== undefined }" type="button">
|
||
<span>{{ rangeLabel(filters.minDeposit, filters.maxDeposit, "押金") }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="range-menu">
|
||
<button
|
||
v-for="item in moneyRangeOptions"
|
||
:key="item.label"
|
||
type="button"
|
||
:class="{ active: isRangeSelected(filters.minDeposit, filters.maxDeposit, item.min, item.max) }"
|
||
@click="setDepositRangeAndClose(item.min, item.max)"
|
||
>
|
||
{{ item.label }}
|
||
</button>
|
||
<div class="range-manual">
|
||
<el-input-number v-model="filters.minDeposit" :controls="false" :min="0" placeholder="最小值" />
|
||
<span>-</span>
|
||
<el-input-number v-model="filters.maxDeposit" :controls="false" :min="0" placeholder="最大值" />
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('total')" :width="350">
|
||
<template #reference>
|
||
<button class="filter-chip wide" :class="{ active: filters.minTotal !== undefined || filters.maxTotal !== undefined }" type="button">
|
||
<span>{{ rangeLabel(filters.minTotal, filters.maxTotal, "合计金额") }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="range-menu">
|
||
<button
|
||
v-for="item in totalRangeOptions"
|
||
:key="item.label"
|
||
type="button"
|
||
:class="{ active: isRangeSelected(filters.minTotal, filters.maxTotal, item.min, item.max) }"
|
||
@click="setTotalRangeAndClose(item.min, item.max)"
|
||
>
|
||
{{ item.label }}
|
||
</button>
|
||
<div class="range-manual">
|
||
<el-input-number v-model="filters.minTotal" :controls="false" :min="0" placeholder="最小值" />
|
||
<span>-</span>
|
||
<el-input-number v-model="filters.maxTotal" :controls="false" :min="0" placeholder="最大值" />
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('skin')" :width="320">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.skinGroup || filters.skinName }" type="button">
|
||
<span>{{ skinChipLabel }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="skin-filter-menu">
|
||
<button class="skin-reset" type="button" :class="{ active: !filters.skinGroup && !filters.skinName }" @click="resetSkinFilterAndClose">
|
||
全部皮肤
|
||
</button>
|
||
<div v-for="group in skinFilterGroups" :key="group.key" class="skin-filter-group">
|
||
<div class="skin-filter-title">
|
||
<strong>{{ group.title }}</strong>
|
||
<button
|
||
type="button"
|
||
:class="{ active: filters.skinGroup === group.key && !filters.skinName }"
|
||
@click="setSkinFilterAndClose(group.key)"
|
||
>
|
||
全部{{ group.title.replace('干员', '') }}
|
||
</button>
|
||
</div>
|
||
<div class="skin-filter-options">
|
||
<button
|
||
v-for="skin in group.options"
|
||
:key="skin"
|
||
type="button"
|
||
:class="{ active: filters.skinGroup === group.key && filters.skinName === skin }"
|
||
@click="setSkinFilterAndClose(group.key, skin)"
|
||
>
|
||
{{ skin }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('rank')" :width="220">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.rank }" type="button">
|
||
<span>{{ filters.rank || "段位" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.rank }" @click="setStringFilter('rank', '')">全部段位</button>
|
||
<button
|
||
v-for="item in publishOptions.rank_options"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.rank === item }"
|
||
@click="setStringFilter('rank', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('fireLevel')" :width="350">
|
||
<template #reference>
|
||
<button class="filter-chip" :class="{ active: filters.minFireLevel !== undefined || filters.maxFireLevel !== undefined }" type="button">
|
||
<span>{{ rangeLabel(filters.minFireLevel, filters.maxFireLevel, "等级") }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="range-menu">
|
||
<button
|
||
v-for="item in fireLevelRangeOptions"
|
||
:key="item.label"
|
||
type="button"
|
||
:class="{ active: isRangeSelected(filters.minFireLevel, filters.maxFireLevel, item.min, item.max) }"
|
||
@click="setFireLevelRangeAndClose(item.min, item.max)"
|
||
>
|
||
{{ item.label }}
|
||
</button>
|
||
<div class="range-manual">
|
||
<el-input-number v-model="filters.minFireLevel" :controls="false" :min="0" placeholder="最小值" />
|
||
<span>-</span>
|
||
<el-input-number v-model="filters.maxFireLevel" :controls="false" :min="0" placeholder="最大值" />
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-popover v-bind="filterPopoverProps('loginMethod')" :width="220">
|
||
<template #reference>
|
||
<button class="filter-chip wide" :class="{ active: filters.loginMethod }" type="button">
|
||
<span>{{ filters.loginMethod || "登陆方式" }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
</template>
|
||
<div class="filter-menu">
|
||
<button type="button" :class="{ active: !filters.loginMethod }" @click="setStringFilter('loginMethod', '')">全部方式</button>
|
||
<button
|
||
v-for="item in loginMethodOptions"
|
||
:key="item"
|
||
type="button"
|
||
:class="{ active: filters.loginMethod === item }"
|
||
@click="setStringFilter('loginMethod', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 列表标题 & 排序 -->
|
||
<div class="list-head">
|
||
<div class="zone-head">
|
||
<p class="eyebrow">Account Zone</p>
|
||
<h2>账号专区</h2>
|
||
<div class="zone-tabs">
|
||
<button
|
||
v-for="zone in zoneOptions"
|
||
:key="zone.key"
|
||
type="button"
|
||
:class="{ active: activeZone === zone.key }"
|
||
@click="setActiveZone(zone.key)"
|
||
>
|
||
<strong>{{ zone.label }}</strong>
|
||
<span>{{ zone.hint }}</span>
|
||
<em>{{ zone.count }}</em>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="list-actions">
|
||
<el-radio-group v-model="sortBy" size="small">
|
||
<el-radio-button label="recommended">综合推荐</el-radio-button>
|
||
<el-radio-button label="coinDesc">哈夫币</el-radio-button>
|
||
<el-radio-button label="awmDesc">AWM数量</el-radio-button>
|
||
<el-radio-button label="priceAsc">价格最低</el-radio-button>
|
||
<el-radio-button label="priceDesc">价格最高</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 增强版账号列表 -->
|
||
<el-empty
|
||
v-if="!loading && visibleListings.length === 0"
|
||
description="没有符合条件的账号"
|
||
/>
|
||
<div v-else v-loading="loading" class="enhanced-desktop-list">
|
||
<RouterLink
|
||
v-for="item in visibleListings"
|
||
:key="item.id"
|
||
class="listing-card-v2"
|
||
:to="`/listings/${item.id}`"
|
||
>
|
||
<!-- 左侧:封面图 & 标签 -->
|
||
<div class="card-cover">
|
||
<img
|
||
v-if="item.cover_url"
|
||
:src="item.cover_url"
|
||
:alt="getListingTitle(item)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
/>
|
||
<div v-else class="empty-cover">HFB</div>
|
||
<div class="cover-badges">
|
||
<span v-if="hasAcceleratedSaleRatio(item)" class="badge sale">特惠</span>
|
||
<span v-if="hasGiftResources(item)" class="badge gift">有赠送</span>
|
||
</div>
|
||
<div class="ratio-tag">比例 1:{{ getRatioValue(item).toFixed(1) }}</div>
|
||
</div>
|
||
|
||
<!-- 中间:详细数据网格 -->
|
||
<div class="card-details">
|
||
<div class="card-title-row">
|
||
<h3>{{ getListingTitle(item) }}</h3>
|
||
<span class="daily-loss">日耗 {{ getDailyLoss(item) }}</span>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<!-- 第一列:基础币值 & 保险 -->
|
||
<div class="stats-col">
|
||
<div class="stat-row">
|
||
<label>哈夫币</label>
|
||
<strong>{{ formatHafCoinM(getCoinWan(item)) }}</strong>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>保险格数</label>
|
||
<span>{{ readAssetString(item, "season_insurance") || '--' }}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>体力/负重</label>
|
||
<span>{{ readAssetString(item, "stamina_level") }}/{{ readAssetString(item, "load_level") }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第二列:核心资产 -->
|
||
<div class="stats-col">
|
||
<div class="stat-row">
|
||
<label>AWM子弹</label>
|
||
<span :class="{highlight: getResourceQuantity(item, 'awmAmmo') > 0}">
|
||
{{ getResourceQuantity(item, 'awmAmmo') }}发
|
||
</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>六级头甲</label>
|
||
<span>
|
||
{{ getResourceQuantity(item, 'helmet6') }}头 / {{ getResourceQuantity(item, 'armor6') }}甲
|
||
</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>其他重器</label>
|
||
<span>巴雷特 {{ getResourceQuantity(item, 'barrett') }} / 喷子 {{ getResourceQuantity(item, 'shotgun') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第三列:战斗 & 区服 -->
|
||
<div class="stats-col">
|
||
<div class="stat-row">
|
||
<label>绝密KD</label>
|
||
<strong>{{ readAssetNumber(item, "secret_kd") || '--' }}</strong>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>游戏段位</label>
|
||
<span>{{ item.rank_level || '未公开' }}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>所属区服</label>
|
||
<span>{{ getServerRegion(item) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第四列:登录 & 时间 -->
|
||
<div class="stats-col">
|
||
<div class="stat-row">
|
||
<label>上号方式</label>
|
||
<span>{{ getLoginMethod(item) }}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<label>方便上号</label>
|
||
<span class="time-text">{{ getOnlineTimeText(item) || '全天候' }}</span>
|
||
</div>
|
||
<div class="stat-row skins-row">
|
||
<label>持有皮肤</label>
|
||
<span class="skins-text" :title="getSkinNames(item).join(', ')">
|
||
{{ getSkinNames(item).slice(0, 2).join(', ') || '暂无皮肤' }}
|
||
<em v-if="getSkinNames(item).length > 2">...</em>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:价格 & 操作 -->
|
||
<div class="card-price">
|
||
<div class="price-item total">
|
||
<small>总租金</small>
|
||
<strong>¥{{ getListingDisplayPrice(item) }}</strong>
|
||
</div>
|
||
<div class="price-item deposit">
|
||
<small>押金</small>
|
||
<span>¥{{ item.deposit_amount }}</span>
|
||
</div>
|
||
<div class="price-action">
|
||
<button class="rent-btn">立即租用</button>
|
||
</div>
|
||
</div>
|
||
</RouterLink>
|
||
</div>
|
||
<div v-if="!loading && visibleListings.length" class="infinite-load-state">
|
||
<span v-if="loadingMore">正在加载更多账号...</span>
|
||
<span v-else-if="!hasMoreListings">已经到底了</span>
|
||
</div>
|
||
</main>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.pc-home-redesign {
|
||
display: grid;
|
||
gap: 20px;
|
||
width: 100%;
|
||
max-width: 1720px;
|
||
min-width: 0;
|
||
margin: 0 auto;
|
||
overflow-x: clip;
|
||
}
|
||
|
||
/* 基础通用样式 */
|
||
.home-announcement,
|
||
.horizontal-filter-card,
|
||
.listing-card-v2 {
|
||
border: 1px solid #eef1f5;
|
||
border-radius: 16px;
|
||
background: #ffffff;
|
||
box-shadow: 0 4px 12px rgba(23, 35, 61, 0.03);
|
||
}
|
||
|
||
/* 公告 */
|
||
.home-announcement {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 20px;
|
||
color: #854d0e;
|
||
background: #fefce8;
|
||
border-color: #fef08a;
|
||
}
|
||
|
||
.home-announcement :deep(.el-carousel) {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 内容区域布局 */
|
||
.home-content {
|
||
display: grid;
|
||
gap: 20px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.infinite-load-state {
|
||
padding: 6px 0 18px;
|
||
color: #94a3b8;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.hero-section {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 320px;
|
||
gap: 20px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.hero-board {
|
||
min-width: 0;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-slide {
|
||
position: relative;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 40px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.hero-slide.tone-orange { background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%); }
|
||
.hero-slide.tone-green { background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); }
|
||
|
||
.hero-slide img {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.hero-slide.has-image::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(90deg, rgba(0,0,0,0.6) 0%, transparent 60%);
|
||
}
|
||
|
||
.hero-copy {
|
||
position: relative;
|
||
z-index: 1;
|
||
max-width: 480px;
|
||
}
|
||
|
||
.hero-slide.has-image .hero-copy { color: #ffffff; }
|
||
|
||
.hero-copy h1 {
|
||
margin: 12px 0;
|
||
font-size: 32px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.hero-copy p {
|
||
display: inline-block;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
background: rgba(255,255,255,0.2);
|
||
backdrop-filter: blur(4px);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 统计卡片 v2 */
|
||
.home-stats-v2 {
|
||
display: grid;
|
||
grid-template-rows: repeat(3, 1fr);
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: 16px;
|
||
border: 1px solid #eef1f5;
|
||
border-radius: 16px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.stat-main {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.stat-item strong {
|
||
font-size: 24px;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
.stat-item span {
|
||
font-weight: 700;
|
||
color: #17233d;
|
||
}
|
||
|
||
.stat-item small {
|
||
margin-top: 4px;
|
||
color: #7b8798;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 横向筛选栏 v2 - 审美优化版 */
|
||
.horizontal-filter-card {
|
||
padding: 24px 26px 22px;
|
||
background: #ffffff;
|
||
border-radius: 20px;
|
||
min-width: 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
.filter-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.filter-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-title .el-icon {
|
||
font-size: 18px;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
.filter-title strong {
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.filter-title span {
|
||
font-size: 13px;
|
||
color: #94a3b8;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.filter-chip-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
padding: 4px 0 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
.filter-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
flex: 1 1 96px;
|
||
min-width: 0;
|
||
max-width: 140px;
|
||
height: 40px;
|
||
padding: 0 12px;
|
||
border: 1px solid #edf1f6;
|
||
border-radius: 12px;
|
||
background: #ffffff;
|
||
color: #1f2937;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
|
||
transition: color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||
}
|
||
|
||
.filter-chip.wide {
|
||
flex-basis: 112px;
|
||
max-width: 160px;
|
||
}
|
||
|
||
.filter-chip span {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.filter-chip:hover,
|
||
.filter-chip.active {
|
||
border-color: #ffb27a;
|
||
color: #ff6a00;
|
||
box-shadow: 0 8px 18px rgba(255, 106, 0, 0.1);
|
||
}
|
||
|
||
.filter-chip:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
.filter-chip .el-icon {
|
||
font-size: 12px;
|
||
}
|
||
|
||
:global(.home-filter-popover.el-popover) {
|
||
padding: 0;
|
||
border: 1px solid #e5eaf2;
|
||
border-radius: 8px;
|
||
box-shadow: 0 16px 38px rgba(15, 23, 42, 0.14);
|
||
}
|
||
|
||
:global(.home-filter-popover .el-popper__arrow::before) {
|
||
border-color: #e5eaf2;
|
||
}
|
||
|
||
.filter-menu,
|
||
.range-menu,
|
||
.skin-filter-menu {
|
||
padding: 12px;
|
||
}
|
||
|
||
.filter-menu {
|
||
display: grid;
|
||
gap: 4px;
|
||
max-height: 300px;
|
||
overflow: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.filter-menu::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.filter-menu button,
|
||
.range-menu button {
|
||
width: 100%;
|
||
min-height: 34px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #5f6670;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: background 0.16s ease, color 0.16s ease;
|
||
}
|
||
|
||
.filter-menu button {
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.filter-menu button:hover,
|
||
.filter-menu button.active,
|
||
.range-menu button:hover,
|
||
.range-menu button.active {
|
||
background: #fff3e8;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
.range-menu {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.range-menu button {
|
||
padding: 0 36px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.range-manual {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 20px minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid #e7ecf3;
|
||
}
|
||
|
||
.range-manual > span {
|
||
color: #5f6670;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.range-manual :deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
|
||
.range-manual :deep(.el-input__wrapper),
|
||
.skin-filter-menu :deep(.el-input__wrapper) {
|
||
min-height: 38px;
|
||
border: 1px solid #dbe2ec;
|
||
border-radius: 6px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.range-manual :deep(.el-input__wrapper:hover),
|
||
.skin-filter-menu :deep(.el-input__wrapper:hover) {
|
||
border-color: #ffb27a;
|
||
}
|
||
|
||
.range-manual :deep(.el-input__wrapper.is-focus),
|
||
.skin-filter-menu :deep(.el-input__wrapper.is-focus) {
|
||
border-color: #ff6a00;
|
||
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.12);
|
||
}
|
||
|
||
.range-manual :deep(.el-input__inner),
|
||
.skin-filter-menu :deep(.el-input__inner) {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.skin-filter-menu {
|
||
display: grid;
|
||
gap: 14px;
|
||
width: 100%;
|
||
}
|
||
|
||
.skin-reset {
|
||
width: 100%;
|
||
height: 36px;
|
||
border: 0;
|
||
border-radius: 10px;
|
||
background: #fff7f0;
|
||
color: #ff6a00;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
text-align: left;
|
||
padding: 0 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.skin-reset.active {
|
||
background: #ff6a00;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.skin-filter-group {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.skin-filter-group + .skin-filter-group {
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.skin-filter-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.skin-filter-title strong {
|
||
color: #1f2937;
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.skin-filter-title button {
|
||
height: 28px;
|
||
border: 1px solid #ffd7bd;
|
||
border-radius: 999px;
|
||
background: #fffaf5;
|
||
color: #ff6a00;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
padding: 0 10px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.skin-filter-title button.active,
|
||
.skin-filter-title button:hover {
|
||
background: #ff6a00;
|
||
border-color: #ff6a00;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.skin-filter-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
max-height: 156px;
|
||
overflow: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.skin-filter-options::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.skin-filter-options button {
|
||
max-width: 100%;
|
||
min-height: 30px;
|
||
border: 1px solid #eef1f5;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
color: #5f6670;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
padding: 0 10px;
|
||
cursor: pointer;
|
||
transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease;
|
||
}
|
||
|
||
.skin-filter-options button:hover,
|
||
.skin-filter-options button.active {
|
||
border-color: #ffb27a;
|
||
background: #fff3e8;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
.filter-body-v2 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
padding-bottom: 18px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.filter-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.filter-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.filter-item label {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
color: #475569;
|
||
padding-left: 2px;
|
||
}
|
||
|
||
/* 筛选控件 */
|
||
.filter-select {
|
||
width: 100%;
|
||
}
|
||
|
||
.horizontal-filter-card :deep(.filter-select .el-select__wrapper) {
|
||
min-height: 36px;
|
||
padding: 0 12px;
|
||
border: 1px solid #e7edf5;
|
||
border-radius: 12px;
|
||
background: #fbfcfe;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||
}
|
||
|
||
.horizontal-filter-card :deep(.filter-select .el-select__wrapper:hover) {
|
||
border-color: #ffd0ad;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.horizontal-filter-card :deep(.filter-select .el-select__wrapper.is-focused) {
|
||
border-color: #ff6a00;
|
||
background: #ffffff;
|
||
box-shadow: 0 0 0 4px rgba(255, 106, 0, 0.1);
|
||
}
|
||
|
||
.horizontal-filter-card :deep(.filter-select .el-select__placeholder) {
|
||
color: #a4adba;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.horizontal-filter-card :deep(.filter-select .el-select__caret) {
|
||
color: #98a2b3;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.skin-search :deep(.el-input__wrapper) {
|
||
min-height: 36px;
|
||
padding: 0 12px;
|
||
border: 1px solid #e7edf5;
|
||
border-radius: 12px;
|
||
background: #fbfcfe;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||
}
|
||
|
||
.skin-search :deep(.el-input__wrapper:hover) {
|
||
border-color: #ffd0ad;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.skin-search :deep(.el-input__wrapper.is-focus) {
|
||
border-color: #ff6a00;
|
||
background: #ffffff;
|
||
box-shadow: 0 0 0 4px rgba(255, 106, 0, 0.1);
|
||
}
|
||
|
||
.skin-search :deep(.el-input__inner) {
|
||
color: #334155;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.skin-search :deep(.el-input__inner::placeholder) {
|
||
color: #a4adba;
|
||
}
|
||
|
||
.range-inputs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number) {
|
||
--el-color-primary: #ff6a00;
|
||
--el-input-border-color: transparent;
|
||
--el-input-hover-border-color: transparent;
|
||
--el-input-focus-border-color: transparent;
|
||
flex: 1;
|
||
min-width: 0;
|
||
width: 100%;
|
||
height: 36px;
|
||
overflow: hidden;
|
||
border: 1px solid #e7edf5;
|
||
border-radius: 12px;
|
||
background: #fbfcfe;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number:hover) {
|
||
border-color: #ffd0ad;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number.is-focus),
|
||
.range-inputs :deep(.el-input-number:focus-within) {
|
||
border-color: #ff6a00;
|
||
background: #ffffff;
|
||
box-shadow: 0 0 0 4px rgba(255, 106, 0, 0.1);
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number .el-input) {
|
||
height: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number .el-input__wrapper) {
|
||
height: 100%;
|
||
padding: 0 34px 0 10px;
|
||
border: 0;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number .el-input__wrapper:hover),
|
||
.range-inputs :deep(.el-input-number .el-input__wrapper.is-focus),
|
||
.range-inputs :deep(.el-input-number.is-focus .el-input__wrapper),
|
||
.range-inputs :deep(.el-input-number:focus-within .el-input__wrapper) {
|
||
border-color: transparent !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number .el-input__inner) {
|
||
color: #334155;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number .el-input__inner::placeholder) {
|
||
color: #a4adba;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number__increase),
|
||
.range-inputs :deep(.el-input-number__decrease) {
|
||
right: 0;
|
||
width: 34px;
|
||
height: 50%;
|
||
border: 0;
|
||
border-left: 1px solid #e7edf5;
|
||
background: #f8fafc;
|
||
color: #98a2b3;
|
||
line-height: 17px;
|
||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number__increase) {
|
||
top: 0;
|
||
border-bottom: 1px solid #eef2f7;
|
||
border-top-right-radius: 11px;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number__decrease) {
|
||
bottom: 0;
|
||
border-bottom-right-radius: 11px;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number__increase:hover),
|
||
.range-inputs :deep(.el-input-number__decrease:hover) {
|
||
border-left-color: #ffd0ad;
|
||
background: #fff7f0;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
.range-inputs :deep(.el-input-number__increase.is-disabled),
|
||
.range-inputs :deep(.el-input-number__decrease.is-disabled) {
|
||
background: #f8fafc;
|
||
color: #cbd5e1;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.range-sep {
|
||
color: #cbd5e1;
|
||
font-weight: 800;
|
||
font-size: 14px;
|
||
}
|
||
|
||
:global(.filter-select-dropdown) {
|
||
overflow: hidden;
|
||
border: 1px solid #e7edf5 !important;
|
||
border-radius: 14px !important;
|
||
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.12) !important;
|
||
}
|
||
|
||
:global(.filter-select-dropdown .el-select-dropdown__list) {
|
||
padding: 6px;
|
||
}
|
||
|
||
:global(.filter-select-dropdown .el-select-dropdown__item) {
|
||
margin: 2px 0;
|
||
border-radius: 10px;
|
||
color: #475569;
|
||
font-weight: 700;
|
||
}
|
||
|
||
:global(.filter-select-dropdown .el-select-dropdown__item.hover),
|
||
:global(.filter-select-dropdown .el-select-dropdown__item:hover) {
|
||
background: #fff3e8;
|
||
color: #ff6a00;
|
||
}
|
||
|
||
:global(.filter-select-dropdown .el-select-dropdown__item.selected) {
|
||
background: #ff6a00;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.filter-footer {
|
||
padding-top: 16px;
|
||
}
|
||
|
||
.quick-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 13px;
|
||
color: #64748b;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.quick-tags button {
|
||
padding: 6px 14px;
|
||
border: 1px solid #f1f5f9;
|
||
border-radius: 10px;
|
||
background: #f8fafc;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.quick-tags button:hover {
|
||
background: #ffffff;
|
||
border-color: #ff6a00;
|
||
color: #ff6a00;
|
||
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
|
||
}
|
||
|
||
.quick-tags button.active {
|
||
background: #ff6a00;
|
||
border-color: #ff6a00;
|
||
color: #ffffff;
|
||
box-shadow: 0 8px 20px rgba(255, 106, 0, 0.2);
|
||
}
|
||
|
||
/* 列表头部 */
|
||
.list-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
gap: 16px;
|
||
min-width: 0;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.list-head h2 {
|
||
margin: 4px 0 0;
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
color: #17233d;
|
||
}
|
||
|
||
.zone-head {
|
||
display: grid;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.zone-tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
max-width: 1120px;
|
||
}
|
||
|
||
.zone-tabs button {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 2px 12px;
|
||
min-width: 132px;
|
||
min-height: 58px;
|
||
padding: 10px 14px;
|
||
border: 1px solid #eef1f5;
|
||
border-radius: 12px;
|
||
background: #ffffff;
|
||
color: #17233d;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: all 0.18s ease;
|
||
}
|
||
|
||
.zone-tabs button:hover {
|
||
border-color: rgba(255, 106, 0, 0.36);
|
||
box-shadow: 0 8px 20px rgba(23, 35, 61, 0.06);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.zone-tabs button.active {
|
||
border-color: #ff6a00;
|
||
background: #fff7ed;
|
||
box-shadow: 0 8px 20px rgba(255, 106, 0, 0.12);
|
||
}
|
||
|
||
.zone-tabs strong {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: inherit;
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.zone-tabs span {
|
||
grid-column: 1 / -1;
|
||
color: #8b9cb5;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.zone-tabs em {
|
||
align-self: start;
|
||
min-width: 24px;
|
||
border-radius: 999px;
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-style: normal;
|
||
font-weight: 900;
|
||
line-height: 22px;
|
||
text-align: center;
|
||
}
|
||
|
||
.zone-tabs button.active em {
|
||
background: #ff6a00;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.eyebrow {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
color: #ff6a00;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.list-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
min-width: 0;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.list-actions :deep(.el-radio-button__inner) {
|
||
border-radius: 8px;
|
||
border: 1px solid #eef1f5;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.list-actions :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
|
||
background-color: #ff6a00;
|
||
border-color: #ff6a00;
|
||
box-shadow: -1px 0 0 0 #ff6a00;
|
||
}
|
||
|
||
/* 增强版卡片 v2 */
|
||
.enhanced-desktop-list {
|
||
display: grid;
|
||
gap: 16px;
|
||
}
|
||
|
||
.listing-card-v2 {
|
||
display: grid;
|
||
grid-template-columns: 240px 1fr 200px;
|
||
gap: 30px;
|
||
padding: 20px;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
.listing-card-v2:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 12px 32px rgba(23, 35, 61, 0.08);
|
||
border-color: #cbd5e1;
|
||
}
|
||
|
||
.card-cover {
|
||
position: relative;
|
||
height: 140px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.card-cover img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.empty-cover {
|
||
height: 100%;
|
||
display: grid;
|
||
place-items: center;
|
||
font-size: 24px;
|
||
font-weight: 900;
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.cover-badges {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.badge {
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.badge.sale { background: #ef4444; }
|
||
.badge.gift { background: #10b981; }
|
||
|
||
.ratio-tag {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 4px;
|
||
background: rgba(0,0,0,0.6);
|
||
color: #fbbf24;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 卡片详情区域 */
|
||
.card-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-title-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card-title-row h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: #1e293b;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.daily-loss {
|
||
padding: 4px 10px;
|
||
background: #f1f5f9;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #64748b;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
}
|
||
|
||
.stats-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.stat-row label {
|
||
font-size: 11px;
|
||
color: #94a3b8;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stat-row span, .stat-row strong {
|
||
font-size: 13px;
|
||
color: #334155;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stat-row strong {
|
||
color: #1e293b;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.highlight {
|
||
color: #ff6a00 !important;
|
||
}
|
||
|
||
.time-text, .skins-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.skins-text em {
|
||
font-style: normal;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
/* 右侧价格区域 */
|
||
.card-price {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding-left: 24px;
|
||
border-left: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.price-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.price-item small {
|
||
color: #94a3b8;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.price-item.total strong {
|
||
font-size: 28px;
|
||
color: #ef4444;
|
||
line-height: 1;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.price-item.deposit span {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.rent-btn {
|
||
width: 100%;
|
||
height: 40px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: #ff6a00;
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.rent-btn:hover {
|
||
background: #e55d00;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.hero-section { grid-template-columns: 1fr; }
|
||
.home-stats-v2 { grid-template-rows: 1fr; grid-template-columns: repeat(3, 1fr); }
|
||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.listing-card-v2 { grid-template-columns: 160px 1fr 140px; }
|
||
}
|
||
</style>
|