Files
hfb_sys/frontend/src/views/public/HomeView.vue
T
2026-06-02 13:24:04 +08:00

2079 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>