1104 lines
28 KiB
Vue
1104 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { RouterLink, onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||
import { usePcHomeCacheStore } from '@/stores/pcHomeCache'
|
||
import {
|
||
defaultAWRecycleConfig,
|
||
defaultCooperationFeedbackConfig,
|
||
defaultHomeAnnouncements,
|
||
defaultHomeBanners,
|
||
fetchMobileHomeConfig,
|
||
normalizeHomeBanner,
|
||
type AWRecycleConfig,
|
||
type CooperationFeedbackConfig,
|
||
type HomeBannerSlide,
|
||
} from '@/features/listings/api/homeConfig'
|
||
import { ElMessage } from 'element-plus'
|
||
import AnnouncementPopup from '@/features/announcement/components/AnnouncementPopup.vue'
|
||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||
import {
|
||
emptyListingPublishOptions,
|
||
type ListingPublishOptions,
|
||
} from '@/features/listings/api/listingOptions'
|
||
import { useHomeFilters } from '@/features/listings/composables/useHomeFilters'
|
||
import {
|
||
clearStoredHomeQuery,
|
||
desktopHomeFilterStorageKey,
|
||
desktopHomeFilterQueryKeys,
|
||
hasHomeQueryValues,
|
||
isSameQuery,
|
||
mergeHomeQuery,
|
||
readStoredHomeQuery,
|
||
joinQueryCSV,
|
||
readQueryCSV,
|
||
readQueryNumber,
|
||
readQueryString,
|
||
storeHomeQuery,
|
||
type HomeQueryValue,
|
||
} from '@/features/listings/composables/useHomeFilterQuery'
|
||
import { useListingQuery } from '@/features/listings/composables/useListingQuery'
|
||
import HomeAnnouncement from '../components/HomeAnnouncement.vue'
|
||
import HomeBanner from '../components/HomeBanner.vue'
|
||
import HomeFilters from '../components/HomeFilters.vue'
|
||
import HomeZones from '../components/HomeZones.vue'
|
||
import ListingCard from '../components/ListingCard.vue'
|
||
import { mapDistributionHomeBanners, useDistributionSiteStore } from '@/stores/distributionSite'
|
||
|
||
const siteStore = useDistributionSiteStore()
|
||
|
||
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
||
const displayAnnouncements = computed(() => {
|
||
if (siteStore.isDistribution) {
|
||
if (siteStore.announcements && siteStore.announcements.length > 0) {
|
||
return siteStore.announcements
|
||
}
|
||
return [
|
||
`【${siteStore.siteName}】精选三角洲账号:官方验号,安全秒交接,售后有保障!`,
|
||
...announcements.value,
|
||
]
|
||
}
|
||
return announcements.value
|
||
})
|
||
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
||
const displayBanners = computed<HomeBannerSlide[]>(() => {
|
||
const list =
|
||
siteStore.isDistribution && siteStore.banners.length > 0
|
||
? mapDistributionHomeBanners(siteStore.banners, siteStore.siteName)
|
||
: banners.value
|
||
return list.filter(slide => slide.enabled !== false)
|
||
})
|
||
const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
|
||
const awRecycleDialogVisible = ref(false)
|
||
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
|
||
const cooperationFeedbackDialogVisible = ref(false)
|
||
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
|
||
interface SortButtonItem {
|
||
key: string
|
||
label: string
|
||
hasDirection?: boolean
|
||
defaultSort?: string
|
||
altSort?: string
|
||
}
|
||
|
||
const sortBy = ref('recommended')
|
||
const sortButtons: SortButtonItem[] = [
|
||
{ key: 'recommended', label: '综合推荐' },
|
||
{
|
||
key: 'ratio',
|
||
label: '比例',
|
||
hasDirection: true,
|
||
defaultSort: 'ratioDesc',
|
||
altSort: 'ratioAsc',
|
||
},
|
||
{
|
||
key: 'coin',
|
||
label: '哈夫币',
|
||
hasDirection: true,
|
||
defaultSort: 'coinDesc',
|
||
altSort: 'coinAsc',
|
||
},
|
||
{
|
||
key: 'awm',
|
||
label: 'AWM数量',
|
||
hasDirection: true,
|
||
defaultSort: 'awmDesc',
|
||
altSort: 'awmAsc',
|
||
},
|
||
{
|
||
key: 'price',
|
||
label: '价格',
|
||
hasDirection: true,
|
||
defaultSort: 'priceAsc',
|
||
altSort: 'priceDesc',
|
||
},
|
||
]
|
||
|
||
function isSortActive(item: SortButtonItem): boolean {
|
||
if (!item.hasDirection) {
|
||
return sortBy.value === 'recommended' || !sortBy.value
|
||
}
|
||
return sortBy.value === item.defaultSort || sortBy.value === item.altSort
|
||
}
|
||
|
||
function getSortDirection(item: SortButtonItem): 'asc' | 'desc' | null {
|
||
if (!item.hasDirection) return null
|
||
if (sortBy.value === item.defaultSort) {
|
||
return item.defaultSort?.endsWith('Asc') ? 'asc' : 'desc'
|
||
}
|
||
if (sortBy.value === item.altSort) {
|
||
return item.altSort?.endsWith('Asc') ? 'asc' : 'desc'
|
||
}
|
||
return null
|
||
}
|
||
|
||
function handleSortClick(item: SortButtonItem) {
|
||
if (!item.hasDirection) {
|
||
sortBy.value = 'recommended'
|
||
return
|
||
}
|
||
if (sortBy.value === item.defaultSort) {
|
||
sortBy.value = item.altSort!
|
||
} else if (sortBy.value === item.altSort) {
|
||
sortBy.value = item.defaultSort!
|
||
} else {
|
||
sortBy.value = item.defaultSort!
|
||
}
|
||
}
|
||
const activeZone = ref('all')
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const {
|
||
filters,
|
||
regionOptions,
|
||
loginMethodOptions,
|
||
skinFilterGroups,
|
||
advancedSkinFilterGroups,
|
||
advancedFiltersOpen,
|
||
advancedActiveCount,
|
||
resetFilters,
|
||
} = useHomeFilters(
|
||
publishOptions,
|
||
computed(() => listings.value)
|
||
)
|
||
|
||
applyRouteQueryToHomeState()
|
||
|
||
const {
|
||
loading,
|
||
loadingMore,
|
||
listings,
|
||
totalListings,
|
||
hasMoreListings,
|
||
loadListingsPage,
|
||
zoneCount,
|
||
restoreSnapshot,
|
||
getSnapshot,
|
||
} = useListingQuery(filters, sortBy, activeZone)
|
||
|
||
watch(
|
||
() => route.query,
|
||
() => {
|
||
applyRouteQueryToHomeState()
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => desktopHomeStateSignature(),
|
||
() => {
|
||
syncHomeQuery()
|
||
}
|
||
)
|
||
|
||
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: '0-2点可响应上号',
|
||
count: zoneCount('night'),
|
||
},
|
||
{
|
||
key: 'password',
|
||
label: '账密专区',
|
||
hint: '交接更快',
|
||
count: zoneCount('password'),
|
||
},
|
||
{
|
||
key: 'highCoin',
|
||
label: '高币专区',
|
||
hint: '100M 以上',
|
||
count: zoneCount('highCoin'),
|
||
},
|
||
])
|
||
|
||
async function loadHome() {
|
||
loading.value = true
|
||
try {
|
||
const [, config] = await Promise.all([loadListingsPage(true), fetchMobileHomeConfig()])
|
||
announcements.value = config.announcements
|
||
banners.value = config.banners
|
||
awRecycle.value = config.aw_recycle
|
||
cooperationFeedback.value = config.cooperation_feedback
|
||
publishOptions.value = config.publish_options
|
||
} catch {
|
||
announcements.value = defaultHomeAnnouncements
|
||
banners.value = defaultHomeBanners
|
||
awRecycle.value = { ...defaultAWRecycleConfig }
|
||
cooperationFeedback.value = { ...defaultCooperationFeedbackConfig }
|
||
publishOptions.value = emptyListingPublishOptions
|
||
} finally {
|
||
loading.value = false
|
||
saveToCache()
|
||
restorePcHomeScroll()
|
||
}
|
||
}
|
||
|
||
function openAWRecycle() {
|
||
if (!awRecycle.value.image_url) {
|
||
ElMessage.warning('回收说明图片尚未配置,请稍后再试')
|
||
return
|
||
}
|
||
awRecycleDialogVisible.value = true
|
||
}
|
||
|
||
function openCooperationFeedback() {
|
||
if (!cooperationFeedback.value.image_url) {
|
||
ElMessage.warning('合作与反馈图片尚未配置,请稍后再试')
|
||
return
|
||
}
|
||
cooperationFeedbackDialogVisible.value = true
|
||
}
|
||
|
||
function updateFilters(partial: Partial<typeof filters>) {
|
||
Object.assign(filters, partial)
|
||
}
|
||
|
||
function handleResetFilters() {
|
||
clearStoredHomeQuery(desktopHomeFilterStorageKey)
|
||
resetFilters()
|
||
activeZone.value = 'all'
|
||
sortBy.value = 'recommended'
|
||
syncHomeQuery()
|
||
}
|
||
|
||
function applyRouteQueryToHomeState() {
|
||
const storedQuery = readStoredHomeQuery(desktopHomeFilterStorageKey)
|
||
const routeHasManaged = hasHomeQueryValues(route.query, desktopHomeFilterQueryKeys)
|
||
// 路由有托管参数时以路由为准;否则回退 session。
|
||
// 注意:综合推荐时 sort 不在 URL,若路由仍有其它筛选参数,也不要用 storage 覆盖 sort。
|
||
const query = routeHasManaged ? route.query : storedQuery
|
||
filters.keyword = readQueryString(query, 'keyword')
|
||
filters.server = readQueryString(query, 'server')
|
||
filters.region = readQueryCSV(query, 'region')
|
||
filters.loginMethod = readQueryCSV(query, 'login_method')
|
||
filters.rank = readQueryCSV(query, 'rank')
|
||
filters.insurance = readQueryCSV(query, 'insurance')
|
||
filters.stamina = readQueryCSV(query, 'stamina')
|
||
filters.load = readQueryCSV(query, 'load')
|
||
// 兼容旧单值 skin_name / 新 CSV 多选
|
||
filters.skinNames = readQueryCSV(query, 'skin_name')
|
||
filters.minCoin = readQueryNumber(query, 'min_coin')
|
||
filters.maxCoin = readQueryNumber(query, 'max_coin')
|
||
filters.minRatio = readQueryNumber(query, 'min_ratio')
|
||
filters.maxRatio = readQueryNumber(query, 'max_ratio')
|
||
filters.minPrice = readQueryNumber(query, 'min_price')
|
||
filters.maxPrice = readQueryNumber(query, 'max_price')
|
||
filters.minDeposit = readQueryNumber(query, 'min_deposit')
|
||
filters.maxDeposit = readQueryNumber(query, 'max_deposit')
|
||
filters.minTotal = readQueryNumber(query, 'min_total')
|
||
filters.maxTotal = readQueryNumber(query, 'max_total')
|
||
filters.minFireLevel = readQueryNumber(query, 'min_fire_level')
|
||
filters.maxFireLevel = readQueryNumber(query, 'max_fire_level')
|
||
filters.minDailyLoss = readQueryNumber(query, 'min_daily_loss')
|
||
filters.maxDailyLoss = readQueryNumber(query, 'max_daily_loss')
|
||
filters.minAwmAmmo = readQueryNumber(query, 'resource_awmAmmo_min')
|
||
filters.maxAwmAmmo = readQueryNumber(query, 'resource_awmAmmo_max')
|
||
// 无 sort 参数即综合推荐(默认)
|
||
sortBy.value = readQueryString(query, 'sort') || 'recommended'
|
||
// zone 不从 query/storage 恢复,由用户实时点击决定,不参与筛选持久化
|
||
}
|
||
|
||
function desktopHomeStateSignature() {
|
||
return JSON.stringify(buildDesktopHomeQueryValues())
|
||
}
|
||
|
||
function syncHomeQuery() {
|
||
const query = mergeHomeQuery(
|
||
route.query,
|
||
desktopHomeFilterQueryKeys,
|
||
buildDesktopHomeQueryValues()
|
||
)
|
||
// 综合推荐不写 sort;若当前无任何筛选条件,必须清空 storage,
|
||
// 否则会回退读到旧的 sort=coinDesc 等,导致无法选回「综合推荐」。
|
||
if (hasHomeQueryValues(query, desktopHomeFilterQueryKeys)) {
|
||
storeHomeQuery(desktopHomeFilterStorageKey, query)
|
||
} else {
|
||
clearStoredHomeQuery(desktopHomeFilterStorageKey)
|
||
}
|
||
if (isSameQuery(route.query, query)) return
|
||
router.replace({ path: '/', query })
|
||
}
|
||
|
||
function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
|
||
return {
|
||
keyword: filters.keyword.trim() || undefined,
|
||
sort: sortBy.value === 'recommended' ? undefined : sortBy.value,
|
||
server: filters.server || undefined,
|
||
region: joinQueryCSV(filters.region),
|
||
login_method: joinQueryCSV(filters.loginMethod),
|
||
rank: joinQueryCSV(filters.rank),
|
||
insurance: joinQueryCSV(filters.insurance),
|
||
stamina: joinQueryCSV(filters.stamina),
|
||
load: joinQueryCSV(filters.load),
|
||
skin_name: joinQueryCSV(filters.skinNames),
|
||
min_coin: filters.minCoin,
|
||
max_coin: filters.maxCoin,
|
||
min_ratio: filters.minRatio,
|
||
max_ratio: filters.maxRatio,
|
||
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,
|
||
min_daily_loss: filters.minDailyLoss,
|
||
max_daily_loss: filters.maxDailyLoss,
|
||
resource_awmAmmo_min: filters.minAwmAmmo,
|
||
resource_awmAmmo_max: filters.maxAwmAmmo,
|
||
}
|
||
}
|
||
|
||
// ========== PC 首页状态与滚动持久化 ==========
|
||
// 详情页返回/浏览器后退时,利用 Pinia 缓存同步恢复全部已加载列表与高度,
|
||
// 使首帧渲染即可完成完整布局,结合 instant 滚动定位彻底解决「从顶部闪跳」问题。
|
||
const pcHomeScrollStorageKey = 'hfb.home.scroll.desktop'
|
||
const pcHomeCache = usePcHomeCacheStore()
|
||
|
||
function restoreFromCache(): boolean {
|
||
const signature = desktopHomeStateSignature()
|
||
if (!pcHomeCache.matchList(signature)) return false
|
||
|
||
restoreSnapshot({
|
||
listings: pcHomeCache.listings,
|
||
totalListings: pcHomeCache.totalListings,
|
||
zoneCounts: pcHomeCache.zoneCounts,
|
||
currentPage: pcHomeCache.currentPage,
|
||
hasMoreListings: pcHomeCache.hasMoreListings,
|
||
})
|
||
if (pcHomeCache.homeConfigLoaded) {
|
||
announcements.value = pcHomeCache.announcements.slice()
|
||
banners.value = pcHomeCache.banners.map(slide => normalizeHomeBanner(slide))
|
||
awRecycle.value = { ...pcHomeCache.awRecycle }
|
||
cooperationFeedback.value = { ...pcHomeCache.cooperationFeedback }
|
||
publishOptions.value = pcHomeCache.publishOptions
|
||
}
|
||
return true
|
||
}
|
||
|
||
function saveToCache() {
|
||
const snapshot = getSnapshot()
|
||
if (snapshot.listings.length === 0) return
|
||
pcHomeCache.saveList({
|
||
signature: desktopHomeStateSignature(),
|
||
listings: snapshot.listings,
|
||
totalListings: snapshot.totalListings,
|
||
zoneCounts: snapshot.zoneCounts,
|
||
currentPage: snapshot.currentPage,
|
||
hasMoreListings: snapshot.hasMoreListings,
|
||
})
|
||
pcHomeCache.saveHomeConfig({
|
||
announcements: announcements.value,
|
||
banners: banners.value,
|
||
awRecycle: awRecycle.value,
|
||
cooperationFeedback: cooperationFeedback.value,
|
||
publishOptions: publishOptions.value,
|
||
})
|
||
pcHomeCache.saveScrollTop(window.scrollY)
|
||
}
|
||
|
||
function savePcHomeScroll() {
|
||
saveToCache()
|
||
try {
|
||
window.sessionStorage.setItem(
|
||
pcHomeScrollStorageKey,
|
||
JSON.stringify({ signature: desktopHomeStateSignature(), top: window.scrollY })
|
||
)
|
||
} catch {
|
||
// sessionStorage 不可用时忽略
|
||
}
|
||
}
|
||
|
||
function restorePcHomeScroll() {
|
||
let savedTop = pcHomeCache.scrollTop
|
||
let savedSignature = pcHomeCache.listSignature
|
||
if (savedTop <= 0) {
|
||
try {
|
||
const raw = window.sessionStorage.getItem(pcHomeScrollStorageKey)
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw) as { signature?: string; top?: number }
|
||
if (typeof parsed.signature === 'string' && typeof parsed.top === 'number') {
|
||
savedSignature = parsed.signature
|
||
savedTop = parsed.top
|
||
}
|
||
}
|
||
} catch {
|
||
return
|
||
}
|
||
}
|
||
if (savedTop <= 0 || savedSignature !== desktopHomeStateSignature()) return
|
||
|
||
const apply = () => {
|
||
window.scrollTo({ top: savedTop, behavior: 'instant' })
|
||
}
|
||
apply()
|
||
requestAnimationFrame(apply)
|
||
}
|
||
|
||
function handleScroll() {
|
||
if (window.scrollY > 0) {
|
||
pcHomeCache.saveScrollTop(window.scrollY)
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||
if (restored) {
|
||
restorePcHomeScroll()
|
||
}
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('scroll', handleScroll)
|
||
})
|
||
|
||
onBeforeRouteLeave(() => {
|
||
savePcHomeScroll()
|
||
})
|
||
|
||
syncHomeQuery()
|
||
const restored = restoreFromCache()
|
||
if (!restored) {
|
||
loadHome()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="pc-home-redesign">
|
||
<HomeAnnouncement :announcements="displayAnnouncements" />
|
||
|
||
<!-- 弹窗公告:进入首页弹出,必须手动关闭,同一天同一条只弹一次 -->
|
||
<AnnouncementPopup />
|
||
|
||
<main class="home-content">
|
||
<!-- 商城专属标识 Banner -->
|
||
<div v-if="siteStore.isDistribution" class="dist-portal-banner">
|
||
<div class="dist-portal-content">
|
||
<div class="dist-portal-tag">
|
||
<span class="dist-pulse"></span>
|
||
{{ siteStore.storefrontText.hero_badge }}
|
||
</div>
|
||
<h2 class="dist-portal-title">{{ siteStore.coBrandName }}</h2>
|
||
<p class="dist-portal-desc">{{ siteStore.storefrontText.hero_desc }}</p>
|
||
</div>
|
||
<div class="dist-portal-badges">
|
||
<div class="dist-badge-item">
|
||
<span class="badge-icon">🛡️</span>
|
||
<div class="badge-text">
|
||
<strong>官方质检</strong>
|
||
<small>账号状态实时同步</small>
|
||
</div>
|
||
</div>
|
||
<div class="dist-badge-item">
|
||
<span class="badge-icon">⚡</span>
|
||
<div class="badge-text">
|
||
<strong>极速交付</strong>
|
||
<small>自动交接 快速上号</small>
|
||
</div>
|
||
</div>
|
||
<div class="dist-badge-item">
|
||
<span class="badge-icon">📑</span>
|
||
<div class="badge-text">
|
||
<strong>独立账单</strong>
|
||
<small>专属订单 隐私隔离</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hero-section">
|
||
<HomeBanner :banners="displayBanners" :stats="statCards" />
|
||
|
||
<!-- 业务入口:贴在轮播右侧竖排;原「租号大厅」只是跳回首页,已移除 -->
|
||
<div class="biz-entry-rail" aria-label="业务入口">
|
||
<RouterLink v-if="!siteStore.isDistribution" class="biz-entry crash" to="/crash">
|
||
<div class="biz-title-row">
|
||
<strong>撞车 <em>NEW</em></strong>
|
||
<small>选购商品 · 进群交接</small>
|
||
</div>
|
||
<van-icon name="arrow" :size="14" class="biz-arrow" />
|
||
</RouterLink>
|
||
<button
|
||
v-if="awRecycle.enabled"
|
||
type="button"
|
||
class="biz-entry aw-recycle"
|
||
@click="openAWRecycle"
|
||
>
|
||
<div class="biz-title-row">
|
||
<strong>{{ awRecycle.title }}</strong>
|
||
<small>{{ awRecycle.subtitle }}</small>
|
||
</div>
|
||
<van-icon name="arrow" :size="14" class="biz-arrow" />
|
||
</button>
|
||
<button
|
||
v-if="cooperationFeedback.enabled"
|
||
type="button"
|
||
class="biz-entry cooperation-feedback"
|
||
@click="openCooperationFeedback"
|
||
>
|
||
<div class="biz-title-row">
|
||
<strong>{{ cooperationFeedback.title }}</strong>
|
||
<small>{{ cooperationFeedback.subtitle }}</small>
|
||
</div>
|
||
<van-icon name="arrow" :size="14" class="biz-arrow" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
v-model="awRecycleDialogVisible"
|
||
:title="awRecycle.title"
|
||
width="720px"
|
||
class="aw-recycle-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<AuthImage
|
||
v-if="awRecycle.image_url"
|
||
:source="awRecycle.image_url"
|
||
:alt="awRecycle.title"
|
||
fit="contain"
|
||
image-class="aw-recycle-image"
|
||
:preview-src-list="[awRecycle.image_url]"
|
||
/>
|
||
<el-empty v-else description="暂无展示图片" />
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="cooperationFeedbackDialogVisible"
|
||
:title="cooperationFeedback.title"
|
||
width="720px"
|
||
class="aw-recycle-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<AuthImage
|
||
v-if="cooperationFeedback.image_url"
|
||
:source="cooperationFeedback.image_url"
|
||
:alt="cooperationFeedback.title"
|
||
fit="contain"
|
||
image-class="aw-recycle-image"
|
||
:preview-src-list="[cooperationFeedback.image_url]"
|
||
/>
|
||
<el-empty v-else description="暂无展示图片" />
|
||
</el-dialog>
|
||
|
||
<HomeFilters
|
||
:filters="filters"
|
||
:total-listings="totalListings"
|
||
:publish-options="publishOptions"
|
||
:region-options="regionOptions"
|
||
:login-method-options="loginMethodOptions"
|
||
:skin-filter-groups="skinFilterGroups"
|
||
:advanced-skin-filter-groups="advancedSkinFilterGroups"
|
||
:advanced-open="advancedFiltersOpen"
|
||
:advanced-active-count="advancedActiveCount"
|
||
@update:filters="updateFilters"
|
||
@update:advanced-open="advancedFiltersOpen = $event"
|
||
@reset="handleResetFilters"
|
||
/>
|
||
|
||
<!-- 专区与排序合并为同一行极简操作条 -->
|
||
<div class="list-control-bar">
|
||
<HomeZones
|
||
:zones="zoneOptions"
|
||
:active-zone="activeZone"
|
||
@update:active-zone="activeZone = $event"
|
||
/>
|
||
<div class="list-actions sort-bar" role="group" aria-label="排序方式">
|
||
<button
|
||
v-for="item in sortButtons"
|
||
:key="item.key"
|
||
type="button"
|
||
class="sort-chip"
|
||
:class="{ active: isSortActive(item) }"
|
||
@click="handleSortClick(item)"
|
||
>
|
||
<span>{{ item.label }}</span>
|
||
<span v-if="item.hasDirection" class="sort-arrows" aria-hidden="true">
|
||
<i class="arrow-up" :class="{ active: getSortDirection(item) === 'asc' }" />
|
||
<i class="arrow-down" :class="{ active: getSortDirection(item) === 'desc' }" />
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!loading && listings.length === 0" class="home-empty-state">
|
||
<strong>没有符合条件的账号</strong>
|
||
<span>可以放宽筛选条件或切回全部专区。</span>
|
||
<button type="button" @click="handleResetFilters">重置条件</button>
|
||
</div>
|
||
<div v-else v-loading="loading" class="enhanced-desktop-list">
|
||
<ListingCard v-for="item in listings" :key="item.id" :listing="item" />
|
||
</div>
|
||
|
||
<div v-if="!loading && listings.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: 12px;
|
||
width: 100%;
|
||
max-width: 1720px;
|
||
min-width: 0;
|
||
margin: 0 auto;
|
||
overflow-x: clip;
|
||
}
|
||
|
||
.home-content {
|
||
display: grid;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.hero-section {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 320px;
|
||
gap: 14px;
|
||
align-items: stretch;
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.biz-entry-rail {
|
||
display: grid;
|
||
grid-auto-rows: minmax(0, 1fr);
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.biz-entry {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
min-height: 84px;
|
||
padding: 16px 46px 16px 20px;
|
||
border-radius: 14px;
|
||
border: 1px solid #e2e8f0;
|
||
background: #ffffff;
|
||
text-decoration: none;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.03);
|
||
transition:
|
||
border-color 0.2s ease,
|
||
box-shadow 0.2s ease,
|
||
transform 0.2s ease;
|
||
}
|
||
|
||
.biz-entry::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 3px;
|
||
background: linear-gradient(180deg, #ff8a3d 0%, #ff6a00 100%);
|
||
opacity: 0;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.biz-entry:hover {
|
||
border-color: #ffbe98;
|
||
box-shadow: 0 8px 20px rgba(255, 106, 0, 0.1);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.biz-entry:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.biz-title-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 5px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.biz-entry strong {
|
||
color: #0f172a;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.biz-entry small {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.biz-arrow {
|
||
position: absolute;
|
||
top: 50%;
|
||
right: 18px;
|
||
color: #94a3b8;
|
||
transform: translateY(-50%);
|
||
transition:
|
||
transform 0.2s ease,
|
||
color 0.2s ease;
|
||
}
|
||
|
||
.biz-entry:hover .biz-arrow {
|
||
color: #ff6a00;
|
||
transform: translateY(-50%) translateX(2px);
|
||
}
|
||
|
||
.biz-entry.crash strong em {
|
||
margin-left: 6px;
|
||
padding: 2px 6px;
|
||
border-radius: 999px;
|
||
background: #ff6a00;
|
||
color: #fff;
|
||
font-size: 10px;
|
||
font-style: normal;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.biz-entry.aw-recycle {
|
||
border-color: #e0e7ff;
|
||
background: linear-gradient(135deg, #ffffff 0%, #f6f8fe 100%);
|
||
}
|
||
|
||
.biz-entry.cooperation-feedback {
|
||
border-color: #ccfbf1;
|
||
background: linear-gradient(135deg, #ffffff 0%, #f0fdf9 100%);
|
||
}
|
||
|
||
:deep(.aw-recycle-image) {
|
||
display: block;
|
||
width: 100%;
|
||
max-height: 70vh;
|
||
object-fit: contain;
|
||
border-radius: 10px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
@media (max-width: 1440px) {
|
||
.hero-section {
|
||
grid-template-columns: minmax(0, 1fr) 280px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.biz-entry-rail {
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.hero-section {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
}
|
||
|
||
.biz-entry-rail {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
grid-auto-rows: auto;
|
||
gap: 10px;
|
||
}
|
||
|
||
.biz-entry {
|
||
min-height: 76px;
|
||
padding: 14px 40px 14px 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 680px) {
|
||
.biz-entry-rail {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.infinite-load-state {
|
||
padding: 12px 0 24px;
|
||
color: #94a3b8;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 专区与排序单行交互条 */
|
||
.list-control-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
padding: 2px 0;
|
||
}
|
||
|
||
.sort-bar {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
align-items: center;
|
||
padding: 3px;
|
||
border-radius: 10px;
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.sort-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 28px;
|
||
padding: 0 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition:
|
||
background 0.15s ease,
|
||
color 0.15s ease,
|
||
box-shadow 0.15s ease;
|
||
}
|
||
|
||
.sort-chip:hover {
|
||
color: #0f172a;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.sort-chip.active {
|
||
background: #ffffff;
|
||
color: #ff6a00;
|
||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.sort-arrows {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.sort-arrows .arrow-up,
|
||
.sort-arrows .arrow-down {
|
||
display: block;
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 3px solid transparent;
|
||
border-right: 3px solid transparent;
|
||
transition: border-color 0.15s ease;
|
||
}
|
||
|
||
.sort-arrows .arrow-up {
|
||
border-bottom: 3.5px solid #cbd5e1;
|
||
}
|
||
|
||
.sort-arrows .arrow-up.active {
|
||
border-bottom-color: #ff6a00;
|
||
}
|
||
|
||
.sort-arrows .arrow-down {
|
||
border-top: 3.5px solid #cbd5e1;
|
||
}
|
||
|
||
.sort-arrows .arrow-down.active {
|
||
border-top-color: #ff6a00;
|
||
}
|
||
|
||
.sort-chip:hover .sort-arrows .arrow-up:not(.active),
|
||
.sort-chip:hover .sort-arrows .arrow-down:not(.active) {
|
||
border-color: #94a3b8 transparent;
|
||
}
|
||
|
||
.enhanced-desktop-list {
|
||
display: grid;
|
||
gap: 16px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.home-empty-state {
|
||
display: grid;
|
||
place-items: center;
|
||
gap: 8px;
|
||
min-height: 148px;
|
||
padding: 28px;
|
||
border: 1px dashed #d8e1ee;
|
||
border-radius: 16px;
|
||
background: #ffffff;
|
||
color: #7b8798;
|
||
text-align: center;
|
||
}
|
||
|
||
.home-empty-state strong {
|
||
color: #17233d;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.home-empty-state span {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.home-empty-state button {
|
||
height: 34px;
|
||
margin-top: 4px;
|
||
padding: 0 16px;
|
||
border: 1px solid #ff6a00;
|
||
border-radius: 8px;
|
||
background: #fff7ed;
|
||
color: #ff6a00;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.dist-portal-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24px;
|
||
padding: 24px 32px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.dist-portal-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.dist-portal-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
width: fit-content;
|
||
padding: 3px 10px;
|
||
border-radius: 999px;
|
||
background: rgba(20, 119, 255, 0.2);
|
||
border: 1px solid rgba(20, 119, 255, 0.4);
|
||
color: #60a5fa;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.dist-pulse {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #38bdf8;
|
||
box-shadow: 0 0 8px #38bdf8;
|
||
}
|
||
|
||
.dist-portal-title {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
color: #f8fafc;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
.dist-portal-desc {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.dist-portal-badges {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.dist-badge-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 18px;
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.badge-icon {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.badge-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.badge-text strong {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #f1f5f9;
|
||
}
|
||
|
||
.badge-text small {
|
||
font-size: 11px;
|
||
color: #64748b;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.dist-portal-banner {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
.dist-portal-badges {
|
||
width: 100%;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
</style>
|