[Snow Team] Merge frontend-mobile's work

This commit is contained in:
yml
2026-05-22 19:11:17 +08:00
3 changed files with 755 additions and 69 deletions
@@ -0,0 +1,567 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { fetchListings, type Listing } from '@/api/listings'
const loading = ref(false)
const loadFailed = ref(false)
const listings = ref<Listing[]>([])
const filterOpen = ref(false)
const activeSort = ref('推荐')
const sortOptions = ['推荐', '低价', '高哈夫币', '免押优先']
const quickFilters = ['三角洲行动', '安卓/扫码', '高段位', '可包夜']
const fallbackListings: Listing[] = [
{
id: 9001,
account_id: 0,
owner_id: 0,
title: '烽火地带 · 80W 哈夫币安全号',
description: '支持扫码上号,号主在线交接,适合快速体验高资产账号。',
game_name: '三角洲行动',
server_region: '微信区',
login_platform: '安卓扫码',
rank_level: '机密行动 王牌',
haf_coin_amount: 800000,
screenshot_urls: [],
price_hourly: 6.8,
price_daily: 58,
price_weekly: 288,
deposit_amount: 200,
min_rent_hours: 2,
max_rent_hours: 168,
status: 'published',
review_status: 'approved',
review_reason: '',
created_at: '',
updated_at: '',
},
{
id: 9002,
account_id: 0,
owner_id: 0,
title: '航天基地 · 高配皮肤成品号',
description: '热门枪皮齐全,低押金,新手友好。',
game_name: '三角洲行动',
server_region: 'QQ区',
login_platform: 'iOS扫码',
rank_level: '钻石',
haf_coin_amount: 520000,
screenshot_urls: [],
price_hourly: 4.5,
price_daily: 39,
price_weekly: 198,
deposit_amount: 120,
min_rent_hours: 1,
max_rent_hours: 72,
status: 'published',
review_status: 'approved',
review_reason: '',
created_at: '',
updated_at: '',
},
{
id: 9003,
account_id: 0,
owner_id: 0,
title: '零号大坝 · 搬砖专用稳定号',
description: '可长租,支持包周,交接说明完整。',
game_name: '三角洲行动',
server_region: '微信区',
login_platform: '安卓/模拟器',
rank_level: '铂金',
haf_coin_amount: 360000,
screenshot_urls: [],
price_hourly: 3.9,
price_daily: 32,
price_weekly: 168,
deposit_amount: 80,
min_rent_hours: 3,
max_rent_hours: 168,
status: 'published',
review_status: 'approved',
review_reason: '',
created_at: '',
updated_at: '',
},
]
const displayListings = computed(() => (listings.value.length > 0 ? listings.value : fallbackListings))
onMounted(loadListings)
async function loadListings() {
loading.value = true
loadFailed.value = false
try {
listings.value = await fetchListings()
} catch {
loadFailed.value = true
} finally {
loading.value = false
}
}
function formatCoin(amount: number) {
if (amount >= 10000) {
return `${Math.round(amount / 10000)}W`
}
return `${amount}`
}
</script>
<template>
<main class="mobile-shell">
<section class="mobile-hero">
<div class="mobile-topbar">
<div class="mobile-brand">
<span class="mobile-logo"></span>
<div>
<strong>大锤商行</strong>
<small>哈夫币租号</small>
</div>
</div>
<button class="mobile-service" type="button">客服</button>
</div>
<div class="fraud-alert">
<span>防骗提示</span>
平台担保交易拒绝私下转账/共享验证码交接全程留痕
</div>
<label class="mobile-search">
<span>🔍</span>
<input type="search" placeholder="搜索区服 / 段位 / 哈夫币数量" />
</label>
<div class="mobile-banner">
<div>
<p>三角洲行动账号专区</p>
<h1>高哈夫币 · 安全交接 · 随租随玩</h1>
<span>新人领券最高减 20 </span>
</div>
<div class="banner-badge">HOT</div>
</div>
</section>
<section class="mobile-content">
<button class="filter-entry" type="button" @click="filterOpen = !filterOpen">
<span>高级筛选</span>
<strong>{{ filterOpen ? '收起' : '展开' }}</strong>
</button>
<div v-if="filterOpen" class="filter-panel">
<button v-for="item in quickFilters" :key="item" type="button">{{ item }}</button>
</div>
<div class="sort-pills" aria-label="排序方式">
<button
v-for="item in sortOptions"
:key="item"
:class="{ active: activeSort === item }"
type="button"
@click="activeSort = item"
>
{{ item }}
</button>
</div>
<p v-if="loadFailed" class="mobile-tip">接口暂不可用当前展示移动端示例数据</p>
<p v-else-if="loading" class="mobile-tip">正在加载优质账号...</p>
<div class="mobile-list">
<RouterLink v-for="item in displayListings" :key="item.id" class="mobile-card" :to="`/listings/${item.id}`">
<div class="card-cover">
<span>{{ formatCoin(item.haf_coin_amount) }}</span>
<small>哈夫币</small>
</div>
<div class="card-body">
<div class="card-title-row">
<h2>{{ item.title }}</h2>
<em>可租</em>
</div>
<p>{{ item.server_region }} · {{ item.login_platform }} · {{ item.rank_level || '未填写段位' }}</p>
<div class="card-tags">
<span>担保</span>
<span>秒接单</span>
<span>{{ item.min_rent_hours }}小时起租</span>
</div>
<div class="card-footer">
<strong>¥{{ item.price_hourly }}<small>/小时</small></strong>
<span>押金 ¥{{ item.deposit_amount }}</span>
</div>
</div>
</RouterLink>
</div>
</section>
<nav class="mobile-tabbar" aria-label="底部导航">
<RouterLink to="/" class="tab-item active">首页</RouterLink>
<RouterLink to="/listings" class="tab-item">租号</RouterLink>
<RouterLink to="/seller/listings/create" class="publish-tab">发布</RouterLink>
<RouterLink to="/orders" class="tab-item">订单</RouterLink>
<RouterLink to="/login" class="tab-item">我的</RouterLink>
</nav>
</main>
</template>
<style scoped>
.mobile-shell {
min-height: 100vh;
background: #eef5ff;
color: #17233d;
padding-bottom: 92px;
}
.mobile-hero {
overflow: hidden;
border-radius: 0 0 28px 28px;
background: linear-gradient(180deg, #1479ff 0%, #0b66e4 58%, #eef5ff 58%);
padding: 14px 14px 0;
}
.mobile-topbar,
.mobile-brand,
.card-title-row,
.card-footer,
.mobile-tabbar {
display: flex;
align-items: center;
}
.mobile-topbar {
justify-content: space-between;
color: #ffffff;
}
.mobile-brand {
gap: 10px;
}
.mobile-logo {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 12px;
background: #ffffff;
color: #1479ff;
font-weight: 900;
}
.mobile-brand strong,
.mobile-brand small {
display: block;
}
.mobile-brand small {
margin-top: 2px;
color: rgba(255, 255, 255, 0.78);
font-size: 12px;
}
.mobile-service {
border: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
color: #ffffff;
padding: 7px 12px;
}
.fraud-alert {
margin-top: 14px;
border-radius: 12px;
background: #ffe27a;
color: #5c3b00;
padding: 10px 12px;
font-size: 12px;
line-height: 1.45;
}
.fraud-alert span {
margin-right: 6px;
font-weight: 800;
}
.mobile-search {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
border-radius: 999px;
background: #ffffff;
padding: 11px 14px;
box-shadow: 0 10px 24px rgba(0, 54, 135, 0.12);
}
.mobile-search input {
min-width: 0;
flex: 1;
border: 0;
outline: 0;
color: #17233d;
font-size: 14px;
}
.mobile-banner {
display: flex;
justify-content: space-between;
gap: 14px;
margin-top: 14px;
border-radius: 20px;
background: linear-gradient(135deg, #ffffff, #dcecff);
padding: 18px;
box-shadow: 0 14px 34px rgba(20, 121, 255, 0.18);
}
.mobile-banner p,
.mobile-banner h1 {
margin: 0;
}
.mobile-banner p {
color: #1479ff;
font-size: 13px;
font-weight: 800;
}
.mobile-banner h1 {
margin-top: 6px;
font-size: 22px;
line-height: 1.22;
}
.mobile-banner span {
display: inline-flex;
margin-top: 12px;
border-radius: 999px;
background: #ffefb5;
color: #7a4b00;
padding: 6px 10px;
font-size: 12px;
font-weight: 700;
}
.banner-badge {
align-self: flex-start;
border-radius: 12px;
background: #ff6a00;
color: #ffffff;
padding: 8px 9px;
font-size: 12px;
font-weight: 900;
transform: rotate(8deg);
}
.mobile-content {
padding: 14px;
}
.filter-entry {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
border: 0;
border-radius: 16px;
background: #ffffff;
color: #17233d;
padding: 14px;
font-size: 15px;
box-shadow: 0 8px 22px rgba(23, 35, 61, 0.06);
}
.filter-entry strong {
color: #1479ff;
}
.filter-panel,
.sort-pills {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 10px 0 2px;
}
.filter-panel button,
.sort-pills button {
flex: 0 0 auto;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #4d5b73;
padding: 9px 12px;
font-weight: 700;
}
.sort-pills button.active {
background: #1479ff;
color: #ffffff;
}
.mobile-tip {
margin: 12px 2px;
color: #6b7a90;
font-size: 13px;
}
.mobile-list {
display: grid;
gap: 12px;
margin-top: 12px;
}
.mobile-card {
display: grid;
grid-template-columns: 92px 1fr;
gap: 12px;
border-radius: 18px;
background: #ffffff;
padding: 12px;
box-shadow: 0 10px 26px rgba(23, 35, 61, 0.07);
}
.card-cover {
display: grid;
min-height: 104px;
place-content: center;
border-radius: 16px;
background: linear-gradient(145deg, #126cff, #63b3ff);
color: #ffffff;
text-align: center;
}
.card-cover span {
font-size: 24px;
font-weight: 900;
}
.card-cover small {
margin-top: 2px;
font-size: 12px;
}
.card-body {
min-width: 0;
}
.card-title-row {
justify-content: space-between;
gap: 8px;
}
.card-title-row h2 {
overflow: hidden;
margin: 0;
font-size: 15px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-title-row em {
flex: 0 0 auto;
border-radius: 999px;
background: #e9f8ef;
color: #0f9f57;
padding: 3px 7px;
font-size: 11px;
font-style: normal;
font-weight: 800;
}
.card-body p {
overflow: hidden;
margin: 7px 0 0;
color: #6b7a90;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 9px;
}
.card-tags span {
border-radius: 6px;
background: #f0f6ff;
color: #1479ff;
padding: 3px 5px;
font-size: 11px;
}
.card-footer {
justify-content: space-between;
margin-top: 11px;
}
.card-footer strong {
color: #ff5f00;
font-size: 18px;
}
.card-footer small,
.card-footer span {
color: #8a96a8;
font-size: 11px;
font-weight: 500;
}
.mobile-tabbar {
position: fixed;
right: 12px;
bottom: 12px;
left: 12px;
z-index: 20;
justify-content: space-around;
height: 60px;
border-radius: 22px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 16px 36px rgba(23, 35, 61, 0.16);
backdrop-filter: blur(14px);
}
.tab-item {
color: #708097;
font-size: 12px;
font-weight: 800;
}
.tab-item.active {
color: #1479ff;
}
.publish-tab {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border: 5px solid #eef5ff;
border-radius: 50%;
background: linear-gradient(180deg, #ffb21a, #ff6a00);
color: #ffffff;
font-size: 13px;
font-weight: 900;
transform: translateY(-18px);
box-shadow: 0 10px 24px rgba(255, 106, 0, 0.35);
}
@media (min-width: 520px) {
.mobile-shell {
max-width: 430px;
margin: 0 auto;
box-shadow: 0 0 0 1px rgba(23, 35, 61, 0.08);
}
.mobile-tabbar {
right: calc((100vw - 430px) / 2 + 12px);
left: calc((100vw - 430px) / 2 + 12px);
}
}
</style>