Files
hfb_sys/frontend/src/views/public/ListingsView.vue
T

217 lines
6.7 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, onMounted, reactive, ref } from "vue";
import { Refresh, Search } from "@element-plus/icons-vue";
import { fetchListings, type Listing } from "@/api/listings";
const loading = ref(false);
const listings = ref<Listing[]>([]);
const sortBy = ref("recommended");
const filters = reactive({
keyword: "",
region: "",
platform: "",
minCoin: undefined as number | undefined,
maxPrice: undefined as number | undefined,
});
const regionOptions = computed(() =>
uniqueOptions(listings.value.map((item) => item.server_region))
);
const platformOptions = computed(() =>
uniqueOptions(listings.value.map((item) => item.login_platform))
);
const filteredListings = computed(() => {
const keyword = filters.keyword.trim().toLowerCase();
const result = listings.value.filter((item) => {
const matchKeyword =
!keyword ||
[item.title, item.description, item.rank_level, item.server_region]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword));
const matchRegion =
!filters.region || item.server_region === filters.region;
const matchPlatform =
!filters.platform || item.login_platform === filters.platform;
const matchCoin =
!filters.minCoin || item.haf_coin_amount >= filters.minCoin;
const matchPrice =
!filters.maxPrice || item.price_hourly <= filters.maxPrice;
return (
matchKeyword && matchRegion && matchPlatform && matchCoin && matchPrice
);
});
return [...result].sort((a, b) => {
if (sortBy.value === "price-asc") return a.price_hourly - b.price_hourly;
if (sortBy.value === "coin-desc")
return b.haf_coin_amount - a.haf_coin_amount;
if (sortBy.value === "deposit-asc")
return a.deposit_amount - b.deposit_amount;
return (
b.haf_coin_amount - a.haf_coin_amount || a.price_hourly - b.price_hourly
);
});
});
onMounted(loadListings);
async function loadListings() {
loading.value = true;
try {
listings.value = await fetchListings();
} finally {
loading.value = false;
}
}
function resetFilters() {
filters.keyword = "";
filters.region = "";
filters.platform = "";
filters.minCoin = undefined;
filters.maxPrice = undefined;
sortBy.value = "recommended";
}
function uniqueOptions(values: string[]) {
return [...new Set(values.filter(Boolean))];
}
</script>
<template>
<section class="pc-listings page">
<div class="anti-fraud-strip compact">
<span
>防骗提示:只在平台内沟通、下单和确认归还,谨防冒充客服与低价私单。</span
>
</div>
<div class="page-header-row listings-header">
<div>
<p class="eyebrow">Account Market</p>
<h1>租号大厅</h1>
<p>参考大锤商行 PC 信息密度,支持高级筛选、排序和卡片式资源浏览。</p>
</div>
<RouterLink class="pc-post-link large" to="/seller/listings/create"
>账号发布</RouterLink
>
</div>
<section class="advanced-filter-card">
<div class="filter-title">
<strong>高级筛选</strong>
<span>共 {{ filteredListings.length }} 个可租账号</span>
</div>
<el-form label-position="top" class="pc-filter-grid">
<el-form-item label="关键词">
<el-input
v-model="filters.keyword"
placeholder="标题 / 段位 / 区服"
:prefix-icon="Search"
clearable
/>
</el-form-item>
<el-form-item label="区服">
<el-select v-model="filters.region" placeholder="全部区服" clearable>
<el-option
v-for="item in regionOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="登录平台">
<el-select
v-model="filters.platform"
placeholder="全部平台"
clearable
>
<el-option
v-for="item in platformOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="最低哈夫币">
<el-input-number
v-model="filters.minCoin"
:min="0"
:step="10000"
placeholder="不限"
class="full-control"
/>
</el-form-item>
<el-form-item label="最高时租">
<el-input-number
v-model="filters.maxPrice"
:min="0"
:step="1"
placeholder="不限"
class="full-control"
/>
</el-form-item>
</el-form>
<div class="sort-toolbar">
<el-radio-group v-model="sortBy">
<el-radio-button label="recommended">综合推荐</el-radio-button>
<el-radio-button label="price-asc">价格最低</el-radio-button>
<el-radio-button label="coin-desc">哈夫币最多</el-radio-button>
<el-radio-button label="deposit-asc">押金最低</el-radio-button>
</el-radio-group>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</div>
</section>
<el-empty
v-if="!loading && filteredListings.length === 0"
description="暂无符合条件的租号"
/>
<div v-else v-loading="loading" class="pc-resource-list">
<RouterLink
v-for="item in filteredListings"
:key="item.id"
class="resource-card"
:to="`/listings/${item.id}`"
>
<div class="resource-cover">
<img
v-if="item.cover_url"
:src="item.cover_url"
:alt="item.title"
/>
<span v-else>HFB</span>
</div>
<div class="resource-main">
<div class="resource-title-line">
<strong>{{ item.title }}</strong>
<em>{{ item.status === "published" ? "可租" : item.status }}</em>
</div>
<p>
{{
item.description || "号主暂未填写说明,可下单后按平台流程交接。"
}}
</p>
<div class="resource-tags">
<span>{{ item.server_region }}</span>
<span>{{ item.login_platform }}</span>
<span>{{ item.rank_level || "未填写段位" }}</span>
<span>哈夫币 {{ item.haf_coin_amount }}</span>
</div>
</div>
<div class="resource-price-box">
<span>时租</span>
<strong>¥{{ item.price_hourly }}</strong>
<em>押金 ¥{{ item.deposit_amount }}</em>
<small
>{{ item.min_rent_hours }}-{{ item.max_rent_hours }} 小时</small
>
</div>
</RouterLink>
</div>
</section>
</template>