-
-
-
+
+
+
+
{{ section.title }}
+ 单位:{{ section.unit }}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
@@ -156,8 +268,13 @@ function resetFilters() {
diff --git a/frontend/src/features/listings/views/MobileHomeView.css b/frontend/src/features/listings/views/MobileHomeView.css
index abca478..9ce3773 100644
--- a/frontend/src/features/listings/views/MobileHomeView.css
+++ b/frontend/src/features/listings/views/MobileHomeView.css
@@ -1,20 +1,27 @@
.mobile-shell {
+ width: 100%;
+ max-width: 430px;
min-height: 100vh;
+ margin: 0 auto;
padding-bottom: 68px;
background: #f6f8fb;
color: #17233d;
}
.mobile-hero {
+ position: sticky;
+ top: 0;
+ z-index: 20;
padding: 12px 12px 10px;
background: #fff;
+ box-shadow: 0 1px 0 rgba(15, 23, 42, 0.06);
}
.mobile-topbar {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- gap: 10px;
+ display: flex;
align-items: center;
+ justify-content: space-between;
+ gap: 12px;
}
.mobile-brand {
@@ -26,12 +33,13 @@
.mobile-logo {
display: grid;
- width: 34px;
- height: 34px;
+ width: 42px;
+ height: 42px;
place-items: center;
- border-radius: 8px;
- background: #2563eb;
+ border-radius: 10px;
+ background: linear-gradient(135deg, #ff8a1f 0%, #ff5a00 100%);
color: #fff;
+ font-size: 20px;
font-weight: 900;
}
@@ -43,25 +51,37 @@
.mobile-brand strong {
color: #0f172a;
- font-size: 14px;
+ font-size: 17px;
}
.mobile-brand small {
color: #64748b;
- font-size: 11px;
+ font-size: 13px;
}
.home-search {
+ margin-top: 10px;
padding: 0;
}
+.home-search :deep(.van-search__content) {
+ height: 38px;
+ background: #f2f5f9;
+}
+
+.home-search :deep(.van-field__control) {
+ font-size: 14px;
+}
+
.mobile-service {
- height: 32px;
- padding: 0 10px;
- border: 1px solid #dbe3ee;
- border-radius: 8px;
+ flex: 0 0 auto;
+ height: 40px;
+ padding: 0 16px;
+ border: 1px solid #fed7aa;
+ border-radius: 12px;
background: #fff;
- color: #2563eb;
+ color: #ff6a00;
+ font-size: 16px;
font-weight: 700;
}
@@ -108,25 +128,58 @@
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
+ background: #fff7ed;
+}
+
+.banner-swipe :deep(.van-swipe__indicators) {
+ right: 16px;
+ bottom: 12px;
+ left: auto;
+ transform: none;
+}
+
+.banner-swipe :deep(.van-swipe__indicator) {
+ width: 5px;
+ height: 5px;
+ background: rgba(255, 255, 255, 0.52);
+ opacity: 1;
+}
+
+.banner-swipe :deep(.van-swipe__indicator--active) {
+ width: 16px;
+ border-radius: 999px;
+ background: #ff6a00;
}
.mobile-banner {
position: relative;
display: flex;
- min-height: 116px;
+ min-height: 128px;
align-items: flex-end;
- padding: 16px;
+ padding: 16px 16px 24px;
overflow: hidden;
- background: #0f172a;
+ background: #9a3412;
color: #fff;
}
.mobile-banner.tone-warm {
- background: #7c2d12;
+ background: #9a3412;
}
.mobile-banner.tone-cool {
- background: #1e3a8a;
+ background: #7c2d12;
+}
+
+.mobile-banner.tone-blue {
+ background: #7c2d12;
+}
+
+.mobile-banner.tone-green {
+ background: #854d0e;
+}
+
+.mobile-banner.tone-orange {
+ background: #c2410c;
}
.banner-image {
@@ -141,12 +194,16 @@
position: absolute;
inset: 0;
content: '';
- background: linear-gradient(180deg, rgba(15, 23, 42, 0.05), rgba(15, 23, 42, 0.72));
+ background: linear-gradient(180deg, transparent 40%, rgba(15, 23, 42, 0.58));
}
-.mobile-banner > div:not(.banner-badge) {
+.banner-copy {
position: relative;
z-index: 1;
+ display: grid;
+ gap: 5px;
+ max-width: 270px;
+ padding-right: 44px;
}
.mobile-banner p,
@@ -154,30 +211,116 @@
margin: 0;
}
-.mobile-banner p,
-.mobile-banner span {
+.mobile-banner p {
+ color: rgba(255, 247, 237, 0.86);
font-size: 12px;
- opacity: 0.9;
+ font-weight: 800;
+ letter-spacing: 0;
}
.mobile-banner h1 {
- margin-top: 4px;
- font-size: 20px;
+ font-size: 19px;
+ line-height: 1.32;
+}
+
+.banner-pill {
+ width: fit-content;
+ max-width: 100%;
+ color: #ffedd5;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.2;
+}
+
+.mobile-banner.has-image .banner-pill {
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: rgba(15, 23, 42, 0.34);
+ color: #fff7ed;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.2;
}
.banner-badge {
position: absolute;
- top: 12px;
- right: 12px;
+ top: 14px;
+ right: 14px;
z-index: 1;
- padding: 4px 8px;
+ padding: 4px 9px;
border-radius: 999px;
- background: rgba(255, 255, 255, 0.88);
- color: #0f172a;
+ background: rgba(255, 247, 237, 0.94);
+ color: #9a3412;
font-size: 12px;
font-weight: 800;
}
+.zone-strip {
+ display: flex;
+ gap: 8px;
+ margin: 0 -12px 12px;
+ padding: 0 12px 2px;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scrollbar-width: none;
+}
+
+.zone-strip::-webkit-scrollbar {
+ display: none;
+}
+
+.zone-pill {
+ display: grid;
+ grid-template-columns: auto auto;
+ grid-template-areas:
+ 'label count'
+ 'hint hint';
+ column-gap: 6px;
+ row-gap: 3px;
+ flex: 0 0 74px;
+ min-height: 54px;
+ align-items: center;
+ padding: 8px 9px;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ background: #fff;
+ color: #334155;
+}
+
+.zone-pill strong {
+ grid-area: label;
+ font-size: 14px;
+ line-height: 1;
+}
+
+.zone-pill span {
+ grid-area: count;
+ color: #ff6a00;
+ font-size: 13px;
+ font-weight: 900;
+ text-align: right;
+}
+
+.zone-pill small {
+ grid-area: hint;
+ overflow: hidden;
+ color: #7b8798;
+ font-size: 10px;
+ line-height: 1.2;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.zone-pill.active {
+ border-color: #ff6a00;
+ background: #fff7ed;
+ color: #9a3412;
+}
+
+.zone-pill.active span {
+ color: #ff6a00;
+}
+
.list-toolbar {
display: flex;
gap: 8px;
@@ -213,7 +356,7 @@
height: 18px;
place-items: center;
border-radius: 999px;
- background: #2563eb;
+ background: #ff6a00;
color: #fff;
font-size: 11px;
font-style: normal;
@@ -242,24 +385,50 @@
}
.sort-panel button.active {
- background: #eff6ff;
- color: #1d4ed8;
+ background: #fff7ed;
+ color: #c2410c;
}
.result-count {
+ display: flex;
+ align-items: baseline;
+ gap: 1px;
margin-bottom: 10px;
color: #64748b;
font-size: 13px;
}
.result-count strong {
- color: #2563eb;
+ color: #ff6a00;
}
.state-loading {
padding: 32px 0;
}
+.mobile-empty {
+ min-height: 280px;
+ justify-content: center;
+ padding: 36px 0 28px;
+}
+
+.mobile-empty :deep(.van-empty__image) {
+ width: 118px;
+ height: 118px;
+ opacity: 0.58;
+}
+
+.mobile-empty :deep(.van-empty__description) {
+ margin-top: 12px;
+ color: #8b95a1;
+ font-size: 14px;
+}
+
+.mobile-empty :deep(.van-button--primary) {
+ border-color: #ff6a00;
+ background: #ff6a00;
+}
+
.mobile-list {
display: grid;
gap: 12px;
@@ -303,7 +472,7 @@
.card-cover-labels em {
padding: 3px 7px;
border-radius: 999px;
- background: rgba(37, 99, 235, 0.92);
+ background: rgba(255, 106, 0, 0.92);
color: #fff;
font-size: 11px;
font-style: normal;
@@ -329,8 +498,8 @@
margin-top: 6px;
padding: 3px 7px;
border-radius: 999px;
- background: #eff6ff;
- color: #2563eb;
+ background: #fff7ed;
+ color: #c2410c;
font-size: 11px;
font-weight: 800;
}
@@ -389,7 +558,14 @@
display: none;
}
- .mobile-topbar {
- grid-template-columns: 34px minmax(0, 1fr) auto;
+ .mobile-logo {
+ width: 38px;
+ height: 38px;
+ }
+
+ .mobile-service {
+ height: 36px;
+ padding: 0 12px;
+ font-size: 14px;
}
}
diff --git a/frontend/src/features/listings/views/MobileHomeView.vue b/frontend/src/features/listings/views/MobileHomeView.vue
index f21964d..45a83c9 100644
--- a/frontend/src/features/listings/views/MobileHomeView.vue
+++ b/frontend/src/features/listings/views/MobileHomeView.vue
@@ -29,6 +29,7 @@ import {
formatListingCode,
getLoginMethod,
getServerRegion,
+ assetRegions,
hasAcceleratedSaleRatio,
hasGiftResources,
} from '@/utils/listingDisplay'
@@ -41,11 +42,13 @@ const loadingMore = ref(false)
const loadFailed = ref(false)
const listings = ref
([])
const totalListings = ref(0)
+const zoneCounts = ref>({})
const currentPage = ref(1)
const hasMoreListings = ref(true)
const publishOptions = ref(emptyListingPublishOptions)
const sortOpen = ref(false)
-const activeSort = ref('comprehensive')
+const activeSort = ref('recommended')
+const activeZone = ref('all')
const filterOpen = ref(false)
const selectedFilters = ref>({})
const rangeFilters = ref>({})
@@ -56,23 +59,57 @@ const bannerSlides = ref(defaultHomeBanners)
const supportLoading = ref(false)
const mobilePageSize = 10
let listingRequestSeq = 0
+let searchDebounceTimer: ReturnType | null = null
const sortOptions = [
- { key: 'comprehensive', label: '综合排序' },
- { key: 'published', label: '发布时间' },
+ { key: 'recommended', label: '综合推荐' },
+ { key: 'coinDesc', label: '哈夫币' },
{ key: 'awmDesc', label: 'AWM数量' },
{ key: 'priceAsc', label: '价格最低' },
{ key: 'priceDesc', label: '价格最高' },
+ { key: 'published', label: '最新发布' },
+]
+
+const zoneOptions = [
+ { key: 'all', label: '全部', hint: '当前可租' },
+ { key: 'sale', label: '特惠', hint: '价格更划算' },
+ { key: 'gift', label: '赠送', hint: '含赠送物品' },
+ { key: 'night', label: '夜间', hint: '夜间好上号' },
+ { key: 'password', label: '账密', hint: '交接更快' },
+ { key: 'highCoin', label: '高币', hint: '100M以上' },
]
const rangePresets: Record> = {
- coin: [
+ price: [
+ { label: '0-50', min: '0', max: '50' },
{ label: '50-100', min: '50', max: '100' },
{ label: '100-200', min: '100', max: '200' },
- { label: '200-300', min: '200', max: '300' },
+ { label: '200以上', min: '200', max: '' },
+ ],
+ coin: [
+ { 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: '' },
],
+ deposit: [
+ { 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: '' },
+ ],
+ total: [
+ { 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' },
+ ],
+ fireLevel: [
+ { 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: '' },
+ ],
resource_awmAmmo: [
{ label: '0-20', min: '0', max: '20' },
{ label: '20-50', min: '20', max: '50' },
@@ -111,6 +148,13 @@ const loginMethodFilterOptions = computed(() =>
uniqueOptions(publishOptions.value.login_method_options.map(item => item.trim()).filter(Boolean))
)
+const regionFilterOptions = computed(() =>
+ uniqueOptions([
+ ...publishOptions.value.region_options,
+ ...listings.value.flatMap(item => assetRegions(item)),
+ ])
+)
+
const filterSections = computed(() => [
{
key: 'price',
@@ -129,6 +173,7 @@ const filterSections = computed(() => [
maxPlaceholder: '最高',
},
{ key: 'server', title: '区服', type: 'chips', options: serverFilterOptions.value },
+ { key: 'region', title: '资产地区', type: 'chips', options: regionFilterOptions.value },
{ key: 'login', title: '上号方式', type: 'chips', options: loginMethodFilterOptions.value },
{
key: 'insurance',
@@ -159,6 +204,13 @@ const filterSections = computed(() => [
minPlaceholder: '最低',
maxPlaceholder: '最高',
},
+ {
+ key: 'fireLevel',
+ title: '等级',
+ type: 'range',
+ minPlaceholder: '最低',
+ maxPlaceholder: '最高',
+ },
{ key: 'rank', title: '段位', type: 'chips', options: publishOptions.value.rank_options },
{
key: 'deposit',
@@ -168,6 +220,14 @@ const filterSections = computed(() => [
minPlaceholder: '最低',
maxPlaceholder: '最高',
},
+ {
+ key: 'total',
+ title: '合计金额',
+ type: 'range',
+ unit: '元',
+ minPlaceholder: '最低',
+ maxPlaceholder: '最高',
+ },
])
const activeFilterCount = computed(() => {
@@ -185,6 +245,13 @@ const displayListings = computed(() => {
return listings.value
})
+const visibleZoneOptions = computed(() =>
+ zoneOptions.map(zone => ({
+ ...zone,
+ count: zoneCount(zone.key),
+ }))
+)
+
onMounted(() => {
loadListings()
loadHomeConfig()
@@ -193,15 +260,23 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleWindowScroll)
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
})
watch(
- () => listingQuerySignature(),
+ () => filterQuerySignature(),
() => {
loadListings(true)
}
)
+watch(searchValue, () => {
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
+ searchDebounceTimer = setTimeout(() => {
+ loadListings(true)
+ }, 300)
+})
+
async function loadListings(reset = true) {
if ((loadingMore.value && !reset) || (!hasMoreListings.value && !reset)) return
const requestSeq = ++listingRequestSeq
@@ -217,6 +292,7 @@ async function loadListings(reset = true) {
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)
@@ -224,6 +300,7 @@ async function loadListings(reset = true) {
if (reset) {
listings.value = []
totalListings.value = 0
+ zoneCounts.value = {}
hasMoreListings.value = false
loadFailed.value = true
}
@@ -277,10 +354,16 @@ function selectSort(sortKey: string) {
sortOpen.value = false
}
+function selectZone(zoneKey: string) {
+ activeZone.value = zoneKey
+ sortOpen.value = false
+}
+
function clearFilters() {
selectedFilters.value = {}
rangeFilters.value = {}
searchValue.value = ''
+ activeZone.value = 'all'
}
function buildListingQuery(page: number): PublicListingQuery {
@@ -289,6 +372,7 @@ function buildListingQuery(page: number): PublicListingQuery {
page_size: mobilePageSize,
keyword: searchValue.value.trim(),
sort: activeSort.value,
+ zone: activeZone.value,
}
const skinGroups: string[] = []
const skinNames: string[] = []
@@ -296,6 +380,7 @@ function buildListingQuery(page: number): PublicListingQuery {
const value = values.filter(Boolean).join(',')
if (!value) continue
if (key === 'server') query.server = value
+ else if (key === 'region') query.region = value
else if (key === 'login') query.login_method = value
else if (key === 'insurance') query.insurance = value
else if (key === 'stamina') query.stamina = value
@@ -325,6 +410,12 @@ function buildListingQuery(page: number): PublicListingQuery {
} else if (key === 'deposit') {
query.min_deposit = min
query.max_deposit = max
+ } else if (key === 'total') {
+ query.min_total = min
+ query.max_total = max
+ } else if (key === 'fireLevel') {
+ query.min_fire_level = min
+ query.max_fire_level = max
} else if (key.startsWith('resource_')) {
const resourceKey = key.replace('resource_', '')
query[`resource_${resourceKey}_min`] = min
@@ -334,8 +425,13 @@ function buildListingQuery(page: number): PublicListingQuery {
return query
}
-function listingQuerySignature() {
- return JSON.stringify(buildListingQuery(1))
+function filterQuerySignature() {
+ return JSON.stringify({
+ sort: activeSort.value,
+ zone: activeZone.value,
+ selected: selectedFilters.value,
+ ranges: rangeFilters.value,
+ })
}
function parseOptionalNumber(value: string) {
@@ -361,6 +457,11 @@ function parseQuantityUnit(price: string) {
function uniqueOptions(values: string[]) {
return [...new Set(values.map(item => item.trim()).filter(Boolean))]
}
+
+function zoneCount(key: string) {
+ if (key === 'all') return zoneCounts.value.all ?? totalListings.value
+ return zoneCounts.value[key] ?? 0
+}
@@ -375,12 +476,6 @@ function uniqueOptions(values: string[]) {
哈夫币租号