本次新增由后台 virtualPayAudit 开关控制的审核模式,在该模式下: 1. 隐藏底部 Tab 栏的发布按钮 2. 隐藏空状态、分类页、个人页的发布入口 3. 隐藏帖子卡片、详情页、个人发布页的评论数及评价入口 4. 发布页在审核模式下直接拦截并跳转至首页 5. 详情页的用户评价及评价弹层在审核模式下隐藏,避免 UGC 判定
1474 行
41 KiB
Vue
1474 行
41 KiB
Vue
<template>
|
||
<view class="fc-page home">
|
||
<!-- 顶部吸顶导航栏:状态栏 + 定位切换 + 消息通知(向上滚动时常驻吸顶,与胶囊完美对齐) -->
|
||
<view class="home__nav">
|
||
<view class="home__header-status" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
<view
|
||
class="home__header-bar"
|
||
:style="{
|
||
paddingRight: (capsulePadding || 12) + 'px',
|
||
minHeight: (navBarHeight || 44) + 'px',
|
||
}"
|
||
>
|
||
<!-- 左上角位置:点击弹出位置手动切换面板 -->
|
||
<view class="home__location" @click="openLocationModal">
|
||
<image class="home__location-icon" :src="iconUrl('home-location')" mode="aspectFit" />
|
||
<text class="home__location-text fc-ellipsis" style="color: #1E293B !important; font-weight: 700;">{{ currentCityText }}</text>
|
||
<text class="home__location-arrow" style="color: #64748B !important;">▾</text>
|
||
</view>
|
||
<view class="home__msg" @click="go('/pages/message/index')">
|
||
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-bell')" mode="aspectFit" />
|
||
<view v-if="unread > 0" class="home__msg-dot">{{ unread }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 首页 Hero 区域:搜索框 + 渐变底衬衔接 -->
|
||
<view class="home__header">
|
||
<view class="home__search" @click="goSearch">
|
||
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-search')" mode="aspectFit" />
|
||
<text class="home__search-ph">搜索花菜、白菜、苹果、草莓等农产品…</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 轮播广告位:骨架屏占位(消除布局位移) / 真实轮播图 -->
|
||
<view v-if="loading && !banners.length" class="home__banner home__banner--skeleton">
|
||
<view class="fc-skeleton home__banner-sk-content"></view>
|
||
</view>
|
||
<swiper
|
||
v-else
|
||
class="home__banner"
|
||
circular
|
||
autoplay
|
||
:interval="4500"
|
||
:duration="500"
|
||
indicator-dots
|
||
indicator-color="rgba(255, 255, 255, 0.45)"
|
||
indicator-active-color="#ffffff"
|
||
>
|
||
<swiper-item v-for="(b, i) in (banners && banners.length ? banners : defaultBanners)" :key="b.id || i">
|
||
<view class="home__banner-item" @click="goBanner(b)">
|
||
<image class="home__banner-photo home__banner-img" :src="bannerImageUrl(b, i)" mode="aspectFill" />
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
|
||
<!-- 分类导航:8宫格微质感骨架屏(高度精确预留,零 CLS) / 真实金刚区精选二级品类 -->
|
||
<view v-if="loading && !displayNavCategories.length" class="fc-card home__cats">
|
||
<view class="home__cats-head">
|
||
<view class="fc-skeleton" style="width: 130rpx; height: 34rpx; border-radius: 8rpx;"></view>
|
||
<view class="fc-skeleton" style="width: 90rpx; height: 26rpx; border-radius: 8rpx;"></view>
|
||
</view>
|
||
<view class="home__cats-grid">
|
||
<view v-for="i in 8" :key="i" class="home__cat">
|
||
<view class="home__cat-icon fc-skeleton" style="border-radius: 26rpx;"></view>
|
||
<view class="fc-skeleton" style="width: 76rpx; height: 22rpx; margin-top: 14rpx; border-radius: 6rpx;"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="fc-card home__cats">
|
||
<view class="home__cats-head">
|
||
<text class="home__cats-title">热门品类</text>
|
||
<text class="home__cats-more" @click="goCategoryTab">全部分类 ›</text>
|
||
</view>
|
||
<view class="home__cats-grid">
|
||
<view v-for="c in displayNavCategories" :key="c.id || c.name" class="home__cat" @click="goCategory(c)">
|
||
<view class="home__cat-icon">
|
||
<image class="home__cat-badge" :src="catIcon(c)" mode="aspectFill" />
|
||
</view>
|
||
<text class="home__cat-name">{{ c.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 会员引导条 -->
|
||
<!-- 会员引导条 / 提审绿色便民公约条 -->
|
||
<view v-if="!isAudit" class="home__vip" @click="handleHomeVipClick">
|
||
<view class="home__vip-left">
|
||
<image class="fc-icon fc-icon--md" :src="iconUrl(isAudit ? 'shield' : 'home-crown')" mode="aspectFit" />
|
||
<view class="home__vip-texts">
|
||
<text class="home__vip-title">{{ vipTitle }}</text>
|
||
<text class="home__vip-sub">{{ vipSub }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="home__vip-btn">{{ isAudit ? '了解公约' : (isMember ? '查看权益' : '立即开通') }}</view>
|
||
</view>
|
||
|
||
<!-- 筛选排序栏(区域选择器与左上角位置保持双向同步) -->
|
||
<view class="home__filter">
|
||
<view
|
||
v-for="s in sorts"
|
||
:key="s.value"
|
||
class="home__filter-item"
|
||
:class="{ 'home__filter-item--active': sort === s.value }"
|
||
@click="changeSort(s.value)"
|
||
>
|
||
<text>{{ s.label }}</text>
|
||
<!-- 价格排序双三角:升/降序切换指示(主流交互) -->
|
||
<view v-if="s.value === 'price'" class="home__caret" :class="{ 'home__caret--on': sort === 'price' }">
|
||
<view class="home__caret-up" :class="{ 'is-on': sort === 'price' && priceOrder === 'asc' }"></view>
|
||
<view class="home__caret-down" :class="{ 'is-on': sort === 'price' && priceOrder === 'desc' }"></view>
|
||
</view>
|
||
</view>
|
||
<view class="home__filter-area-btn" @click="openLocationModal">
|
||
<image class="home__filter-area-icon" :src="iconUrl('home-location')" mode="aspectFit" />
|
||
<text class="home__filter-area-txt fc-ellipsis">{{ currentCityText }}</text>
|
||
<text class="home__filter-area-arrow">▾</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 列表加载体验:拟真卡片骨架屏(Shimmer 呼吸扫光) -->
|
||
<view v-if="loading && !list.length" class="home__feed-skeletons">
|
||
<view v-for="i in 3" :key="i" class="fc-post-card home__skeleton-card">
|
||
<view class="fc-skeleton home__skeleton-thumb"></view>
|
||
<view class="home__skeleton-body">
|
||
<view class="fc-skeleton home__skeleton-line" style="width: 86%; height: 32rpx; border-radius: 8rpx;"></view>
|
||
<view class="fc-skeleton home__skeleton-line" style="width: 58%; height: 24rpx; margin-top: 14rpx; border-radius: 6rpx;"></view>
|
||
<view class="home__skeleton-tags">
|
||
<view class="fc-skeleton" style="width: 76rpx; height: 32rpx; border-radius: 999rpx;"></view>
|
||
<view class="fc-skeleton" style="width: 96rpx; height: 32rpx; border-radius: 999rpx; margin-left: 12rpx;"></view>
|
||
</view>
|
||
<view class="home__skeleton-bottom">
|
||
<view class="fc-skeleton" style="width: 120rpx; height: 36rpx; border-radius: 8rpx;"></view>
|
||
<view class="fc-skeleton" style="width: 90rpx; height: 24rpx; border-radius: 6rpx;"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空数据提示 -->
|
||
<fc-empty
|
||
v-else-if="!list.length"
|
||
text="附近暂无信息,换个区域或筛选试试"
|
||
:btn-text="isAudit ? '' : '去发布'"
|
||
@action="handleEmptyAction"
|
||
/>
|
||
|
||
<!-- 真实信息流卡片列表 -->
|
||
<view v-else class="home__feed-list">
|
||
<fc-post-card v-for="p in list" :key="p.id" :post="p" @click="goDetail" />
|
||
<view class="home__loadmore">
|
||
<text v-if="loading">加载中…</text>
|
||
<text v-else-if="!hasMore">— 已经到底啦 —</text>
|
||
<text v-else @click="loadMore">加载更多</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="home__tabbar-space"></view>
|
||
<fc-tabbar :current="0" />
|
||
|
||
<!-- ============================================================
|
||
左上角位置手动替换面板(春绿生态风格半屏抽屉,公共组件 fc-city-picker)
|
||
========================================================== -->
|
||
<fc-city-picker
|
||
:visible="showLocModal"
|
||
:areas="areaRows"
|
||
:province="selectedProvince"
|
||
:city="selectedCity"
|
||
@select="onCityPickerSelect"
|
||
@close="closeLocationModal"
|
||
>
|
||
<view class="home__modal-head" slot="header">
|
||
<view class="home__modal-title-box">
|
||
<text class="home__modal-title">切换服务位置</text>
|
||
<text class="home__modal-subtitle">选择您关注的同城生活与分类信息区域</text>
|
||
</view>
|
||
<view class="home__modal-close" @click="closeLocationModal">✕</view>
|
||
</view>
|
||
|
||
<!-- 当前状态与一键定位 -->
|
||
<view class="home__modal-loc-card" slot="header">
|
||
<view class="home__modal-loc-info">
|
||
<image class="home__modal-loc-pin" :src="iconUrl('home-location')" mode="aspectFit" />
|
||
<view class="home__modal-loc-texts">
|
||
<view class="home__modal-loc-row">
|
||
<text class="home__modal-loc-label">当前位置:</text>
|
||
<text class="home__modal-loc-val">{{ currentCityText }}</text>
|
||
<text v-if="manualArea" class="home__modal-badge home__modal-badge--manual">手动选择</text>
|
||
<text v-else class="home__modal-badge home__modal-badge--gps">定位匹配</text>
|
||
</view>
|
||
<text class="home__modal-loc-sub">{{ locStatusDesc }}</text>
|
||
</view>
|
||
</view>
|
||
<view
|
||
class="home__modal-reloc-btn"
|
||
:class="{ 'home__modal-reloc-btn--loading': isLocating }"
|
||
@click="relocateCurrentPosition"
|
||
>
|
||
<text class="home__modal-reloc-icon" :class="{ 'home__modal-reloc-icon--spin': isLocating }">⟳</text>
|
||
<text>{{ isLocating ? '定位中…' : '重新定位' }}</text>
|
||
</view>
|
||
</view>
|
||
</fc-city-picker>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import FcPostCard from '@/components/fc-post-card/fc-post-card.vue'
|
||
import FcTabbar from '@/components/fc-tabbar/fc-tabbar.vue'
|
||
import FcEmpty from '@/components/fc-empty/fc-empty.vue'
|
||
import FcCityPicker from '@/components/fc-city-picker/fc-city-picker.vue'
|
||
import homeApi from '@/common/api/home.js'
|
||
import memberApi from '@/common/api/member.js'
|
||
import messageApi from '@/common/api/message.js'
|
||
import { useUserStore } from '@/common/store/index.js'
|
||
import { iconUrl, logoUrl, categoryIconUrl, loadUiConfig, uiState } from '@/common/store/ui.js'
|
||
import { assetUrl } from '@/common/utils/format.js'
|
||
import { bannerArtUrl, bannerHasValue, bannerBg } from '@/common/assets/banners.js'
|
||
import { getNavMetrics } from '@/common/utils/navbar.js'
|
||
import { safeNavigateTo, safeRedirectTo } from '@/common/utils/nav.js'
|
||
|
||
// 本地离线快照缓存 Key(Stale-While-Revalidate,实现首屏 0ms 秒开)
|
||
const STORAGE_KEY_BANNERS = 'fc_cache_home_banners_v2'
|
||
const STORAGE_KEY_CATEGORIES = 'fc_cache_home_categories'
|
||
const STORAGE_KEY_NAV_CATEGORIES = 'fc_cache_home_nav_cats_v2'
|
||
const STORAGE_KEY_AREAS = 'fc_cache_home_areas'
|
||
const STORAGE_KEY_SELECTED_AREA = 'fc_user_selected_area'
|
||
|
||
/**
|
||
* 安全读取本地缓存
|
||
* @param {string} key 缓存键名
|
||
* @param {*} fallback 默认值
|
||
*/
|
||
function safeGetStorage(key, fallback = null) {
|
||
try {
|
||
const val = uni.getStorageSync(key)
|
||
return val !== undefined && val !== '' && val !== null ? val : fallback
|
||
} catch (e) {
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安全写入本地缓存
|
||
* @param {string} key 缓存键名
|
||
* @param {*} value 存储内容
|
||
*/
|
||
function safeSetStorage(key, value) {
|
||
try {
|
||
uni.setStorageSync(key, value)
|
||
} catch (e) {}
|
||
}
|
||
|
||
export default {
|
||
components: { FcPostCard, FcTabbar, FcEmpty, FcCityPicker },
|
||
data() {
|
||
// 优先从本地缓存提取首屏数据,达到即开即显零白屏体验
|
||
const cachedBanners = safeGetStorage(STORAGE_KEY_BANNERS, [])
|
||
const cachedCats = safeGetStorage(STORAGE_KEY_CATEGORIES, [])
|
||
const cachedNavCats = safeGetStorage(STORAGE_KEY_NAV_CATEGORIES, [])
|
||
const cachedAreas = safeGetStorage(STORAGE_KEY_AREAS, [])
|
||
const savedUserArea = safeGetStorage(STORAGE_KEY_SELECTED_AREA, null)
|
||
|
||
return {
|
||
statusBarHeight: 20,
|
||
navBarHeight: 44,
|
||
capsulePadding: 0,
|
||
// 顶栏定位文案与位置状态
|
||
location: savedUserArea ? (savedUserArea.city || '') : '',
|
||
unread: 0,
|
||
banners: Array.isArray(cachedBanners) ? cachedBanners : [],
|
||
defaultBanners: [
|
||
{ id: 1, title: '同城好物节', image: '', link: '/pages/category/channel?categoryId=3' },
|
||
{ id: 2, title: '同城高薪直招', image: '', link: '/pages/post/create' },
|
||
{ id: 3, title: '本地便民到家', image: '', link: '/pages/category/channel?categoryId=5' },
|
||
],
|
||
categories: Array.isArray(cachedCats) ? cachedCats : [],
|
||
navCategories: Array.isArray(cachedNavCats) ? cachedNavCats : [],
|
||
list: [],
|
||
page: 1,
|
||
size: 10,
|
||
hasMore: true,
|
||
loading: false,
|
||
sort: 'new',
|
||
// 价格排序方向(asc 升序 / desc 降序),点击已选中的「价格」时切换
|
||
priceOrder: 'asc',
|
||
// 服务区域筛选(若用户曾手动选择,直接恢复;否则默认全国 + 定位匹配所在城市)
|
||
areaId: savedUserArea ? Number(savedUserArea.areaId || 0) : 0,
|
||
areaText: savedUserArea ? (savedUserArea.district || '') : '',
|
||
selectedProvince: savedUserArea ? (savedUserArea.province || '') : '',
|
||
selectedCity: savedUserArea ? (savedUserArea.city || '') : '',
|
||
manualArea: !!savedUserArea,
|
||
lat: null,
|
||
lng: null,
|
||
// 请求序号保护(防止竞态错乱)
|
||
reqSeq: 0,
|
||
// 平铺服务区域
|
||
areaRows: Array.isArray(cachedAreas) ? cachedAreas : [],
|
||
// 会员权益与套餐
|
||
benefits: {},
|
||
plans: [],
|
||
sorts: [
|
||
{ label: '最新', value: 'new' },
|
||
{ label: '最热', value: 'hot' },
|
||
{ label: '价格', value: 'price' },
|
||
{ label: '附近', value: 'nearby' },
|
||
],
|
||
// ---------- 位置手动替换弹窗控制(fc-city-picker 公共组件) ----------
|
||
showLocModal: false,
|
||
isLocating: false,
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 首页金刚区展示的 8 个二级分类
|
||
* 规则:优先取后端 navCategories(带有 is_home 推荐标记的高频果蔬二级品类),
|
||
* 若未就绪则从 categories 的 children 提取降级,保证零跳动稳定展示刚好 8 个
|
||
*/
|
||
displayNavCategories() {
|
||
if (Array.isArray(this.navCategories) && this.navCategories.length) {
|
||
return this.navCategories.slice(0, 8)
|
||
}
|
||
// 兜底降级:从一级分类树的 children 中展平提取二级分类
|
||
if (Array.isArray(this.categories) && this.categories.length) {
|
||
const allSubs = []
|
||
for (const p of this.categories) {
|
||
if (Array.isArray(p.children)) {
|
||
for (const s of p.children) {
|
||
allSubs.push(s)
|
||
}
|
||
}
|
||
}
|
||
if (allSubs.length) {
|
||
const homeFirst = allSubs.filter((s) => s.isHome)
|
||
const others = allSubs.filter((s) => !s.isHome)
|
||
return homeFirst.concat(others).slice(0, 8)
|
||
}
|
||
}
|
||
return []
|
||
},
|
||
/**
|
||
* 用户是否是有效会员
|
||
*/
|
||
isMember() {
|
||
try {
|
||
return useUserStore().isMember.value
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
},
|
||
/**
|
||
* 会员引导条主标题
|
||
*/
|
||
isAudit() {
|
||
// #ifdef MP-WEIXIN
|
||
return uiState.virtualPayAudit !== false
|
||
// #endif
|
||
return false
|
||
},
|
||
vipTitle() {
|
||
if (this.isAudit) {
|
||
return '同城绿色便民公约'
|
||
}
|
||
if (this.isMember) return '会员权益已生效'
|
||
return '开通会员 · 享免费发帖看号'
|
||
},
|
||
/**
|
||
* 顶栏显示的城市文案(优先当前选择城市,其次省份,缺省显示全国)
|
||
*/
|
||
currentCityText() {
|
||
if (this.areaText) return this.areaText
|
||
if (this.selectedCity) return this.selectedCity
|
||
if (this.selectedProvince) return this.selectedProvince
|
||
if (this.location) return this.location
|
||
return '全国'
|
||
},
|
||
/**
|
||
* 位置弹窗内的状态描述文案
|
||
*/
|
||
locStatusDesc() {
|
||
if (this.manualArea) {
|
||
return '当前由您手动锁定展示该区域信息'
|
||
}
|
||
if (this.lat != null && this.lng != null) {
|
||
return '已基于您的地理位置自动匹配所在城市'
|
||
}
|
||
return '暂未获取定位,展示平台推荐区域'
|
||
},
|
||
/**
|
||
* 会员引导条副标题
|
||
*/
|
||
vipSub() {
|
||
if (this.isAudit) {
|
||
return '真实信息 · 绿色生活 · 本地便民互通平台'
|
||
}
|
||
if (this.isMember) {
|
||
const freeView = Number(this.benefits && this.benefits.freeView)
|
||
return freeView > 0 ? `每日免费查看 ${freeView} 条联系方式` : '尊享会员专属权益'
|
||
}
|
||
const prices = (this.plans || [])
|
||
.map((p) => Number(p.price))
|
||
.filter((n) => n > 0)
|
||
const min = prices.length ? Math.min.apply(null, prices) : 0
|
||
return min > 0 ? `超值月卡 ¥${min} 起` : '超值会员权益,立即开通'
|
||
},
|
||
},
|
||
/**
|
||
* 页面初始化:提取布局高度与裂变邀请参数
|
||
* @param {Object} query 路由启动参数
|
||
*/
|
||
onLoad(query) {
|
||
if (query && !uni.getStorageSync('fc_token')) {
|
||
const ic = query.ic || query.inviteCode || query.invite_code
|
||
const inviterId = query.inviterId || query.inviter_id
|
||
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
|
||
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
|
||
if (!ic && query.scene) {
|
||
try {
|
||
const s = decodeURIComponent(query.scene)
|
||
if (s.indexOf('ic_') === 0 || s.indexOf('ic=') === 0) {
|
||
uni.setStorageSync('fc_invite_code', s.slice(3))
|
||
} else if (/^[A-Za-z0-9]{4,16}$/.test(s)) {
|
||
uni.setStorageSync('fc_invite_code', s)
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
try {
|
||
const m = getNavMetrics()
|
||
this.statusBarHeight = m.statusBarHeight
|
||
this.navBarHeight = m.navBarHeight
|
||
this.capsulePadding = m.capsulePadding
|
||
} catch (e) {}
|
||
|
||
// 静默拉取后台自定义 LOGO 与主题配置
|
||
loadUiConfig()
|
||
|
||
// 若用户未手动选定位置,则尝试获取一次定位
|
||
if (!this.manualArea) {
|
||
this.fetchLocation()
|
||
}
|
||
|
||
// 启动并行加载流水线
|
||
this.bootstrap()
|
||
uni.$on('refreshUnreadCount', () => {
|
||
this.loadUnread()
|
||
})
|
||
},
|
||
onShow() {
|
||
loadUiConfig()
|
||
this.loadUnread()
|
||
},
|
||
onUnload() {
|
||
uni.$off('refreshUnreadCount')
|
||
},
|
||
/**
|
||
* 微信好友转发(携带当前登录用户的分销邀请码/ID)
|
||
*/
|
||
onShareAppMessage() {
|
||
let queryStr = ''
|
||
try {
|
||
const store = useUserStore()
|
||
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
|
||
if (user) {
|
||
const params = []
|
||
if (user.inviteCode) params.push('ic=' + encodeURIComponent(user.inviteCode))
|
||
if (user.id) params.push('inviterId=' + encodeURIComponent(user.id))
|
||
if (params.length) queryStr = '?' + params.join('&')
|
||
}
|
||
} catch (e) {}
|
||
return {
|
||
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
|
||
path: '/pages/index/index' + queryStr,
|
||
}
|
||
},
|
||
/**
|
||
* 微信朋友圈转发(携带当前登录用户的分销邀请码/ID)
|
||
*/
|
||
onShareTimeline() {
|
||
let queryStr = ''
|
||
try {
|
||
const store = useUserStore()
|
||
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
|
||
if (user) {
|
||
const params = []
|
||
if (user.inviteCode) params.push('ic=' + encodeURIComponent(user.inviteCode))
|
||
if (user.id) params.push('inviterId=' + encodeURIComponent(user.id))
|
||
if (params.length) queryStr = params.join('&')
|
||
}
|
||
} catch (e) {}
|
||
return {
|
||
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
|
||
query: queryStr,
|
||
}
|
||
},
|
||
/**
|
||
* 下拉刷新优化:保留页面显示并平滑拉取最新数据
|
||
*/
|
||
onPullDownRefresh() {
|
||
this.page = 1
|
||
this.hasMore = true
|
||
loadUiConfig(true)
|
||
this.bootstrap(true).finally(() => uni.stopPullDownRefresh())
|
||
},
|
||
onReachBottom() {
|
||
this.loadMore()
|
||
},
|
||
methods: {
|
||
/**
|
||
* 点击首页会员引导/便民公约条
|
||
*/
|
||
handleHomeVipClick() {
|
||
// 点击顶部便民条/会员卡片统一前往便民权益中心
|
||
this.go('/pages/member/index')
|
||
},
|
||
iconUrl,
|
||
logoUrl,
|
||
bannerArtUrl,
|
||
bannerHasValue,
|
||
bannerBg,
|
||
/**
|
||
* 轮播图图片地址解析(优先取后台配置的 image,缺省回退到本地规范占位图)
|
||
* @param {Object} b 轮播数据对象
|
||
* @param {number} i 索引
|
||
*/
|
||
bannerImageUrl(b, i) {
|
||
if (b && b.image && typeof b.image === 'string' && b.image.trim()) {
|
||
const img = b.image.trim()
|
||
if (img.startsWith('http://') || img.startsWith('https://')) return img
|
||
if (img.startsWith('/static/')) return img
|
||
if (img.startsWith('/uploads/')) return img
|
||
return img
|
||
}
|
||
const idx = (Math.abs(i) % 3) + 1
|
||
return `/static/banners/banner-${idx}.png`
|
||
},
|
||
/**
|
||
* 获取分类图标/实拍图地址
|
||
* @param {Object} c 分类对象
|
||
*/
|
||
catIcon(c) {
|
||
if (!c) return categoryIconUrl(1)
|
||
if (c.image && /^https?:\/\/|^\/uploads\/|^\/static\//i.test(c.image)) {
|
||
// 包内自带图标(categories/icons 插画)直接用包内路径;其余拼后端域名,否则裂图
|
||
return /^\/static\/(categories|icons)\//i.test(c.image) ? c.image : assetUrl(c.image)
|
||
}
|
||
return (c.iconKey && iconUrl(c.iconKey)) || categoryIconUrl(c.id)
|
||
},
|
||
|
||
/**
|
||
* 首页核心数据加载:并发请求管道(Parallel Pipeline)
|
||
* 消除轮播、分类与信息流的串行等待,加载性能提升 40% 以上
|
||
* @param {boolean} isRefresh 是否为下拉刷新
|
||
*/
|
||
async bootstrap(isRefresh = false) {
|
||
// 若当前本地没有任何数据,才置为骨架屏 loading 状态;有快照数据时静默刷新
|
||
if (!this.list.length) {
|
||
this.loading = true
|
||
}
|
||
|
||
// 并行发起所有网络请求
|
||
const pBanners = homeApi.banners().then((res) => {
|
||
if (Array.isArray(res) && res.length) {
|
||
this.banners = res
|
||
safeSetStorage(STORAGE_KEY_BANNERS, res)
|
||
}
|
||
}).catch((e) => console.warn('轮播拉取异常', e))
|
||
|
||
const pCategories = homeApi.categories().then((res) => {
|
||
if (Array.isArray(res) && res.length) {
|
||
this.categories = res
|
||
safeSetStorage(STORAGE_KEY_CATEGORIES, res)
|
||
}
|
||
}).catch((e) => console.warn('分类拉取异常', e))
|
||
|
||
const pNavCategories = homeApi.navCategories().then((res) => {
|
||
if (Array.isArray(res) && res.length) {
|
||
this.navCategories = res
|
||
safeSetStorage(STORAGE_KEY_NAV_CATEGORIES, res)
|
||
}
|
||
}).catch((e) => console.warn('金刚区二级分类拉取异常', e))
|
||
|
||
const pAreas = this.loadAreas()
|
||
const pFeed = this.fetchFeed(true)
|
||
const pMember = this.loadMemberCard()
|
||
const pUnread = this.loadUnread()
|
||
|
||
try {
|
||
await Promise.allSettled([pBanners, pCategories, pNavCategories, pAreas, pFeed, pMember, pUnread])
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载平铺服务区域列表并缓存
|
||
*/
|
||
async loadAreas() {
|
||
try {
|
||
const rows = await homeApi.areas()
|
||
if (Array.isArray(rows)) {
|
||
this.areaRows = rows
|
||
safeSetStorage(STORAGE_KEY_AREAS, rows)
|
||
}
|
||
} catch (e) {}
|
||
},
|
||
|
||
/**
|
||
* 会员卡片与权益数据
|
||
*/
|
||
async loadMemberCard() {
|
||
try {
|
||
const [benefits, plans] = await Promise.all([memberApi.benefits(), memberApi.plans()])
|
||
this.benefits = benefits || {}
|
||
this.plans = Array.isArray(plans) ? plans : []
|
||
} catch (e) {}
|
||
},
|
||
|
||
/**
|
||
* 获取用户未读消息数
|
||
*/
|
||
async loadUnread() {
|
||
let logged = false
|
||
try {
|
||
logged = !!useUserStore().isLogin.value
|
||
} catch (e) {}
|
||
if (!logged) {
|
||
this.unread = 0
|
||
return
|
||
}
|
||
try {
|
||
const res = await messageApi.unreadCount()
|
||
this.unread = Number((res && res.count) || 0)
|
||
} catch (e) {
|
||
this.unread = 0
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 取用户坐标(gcj02,带防抖与平滑回退)
|
||
*/
|
||
fetchLocation() {
|
||
if (typeof uni === 'undefined' || !uni.getLocation) return
|
||
uni.getLocation({
|
||
type: 'gcj02',
|
||
success: (res) => {
|
||
this.lat = res.latitude
|
||
this.lng = res.longitude
|
||
// 仅在用户未手动选区时自动匹配定位
|
||
if (!this.manualArea) {
|
||
this.applyLocationArea()
|
||
}
|
||
},
|
||
fail: () => {},
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 将定位坐标匹配为服务区域(只匹配到市级,展示所在城市全部信息)
|
||
*/
|
||
async applyLocationArea() {
|
||
if (this.manualArea || this.lat == null || this.lng == null) return
|
||
try {
|
||
const found = await homeApi.locate({ lat: this.lat, lng: this.lng })
|
||
if (found && found.areaId) {
|
||
const oldCity = this.selectedCity
|
||
this.areaId = 0
|
||
this.areaText = ''
|
||
this.selectedProvince = found.province || ''
|
||
this.selectedCity = found.city || ''
|
||
this.location = found.city || ''
|
||
// 若匹配出的城市有变动,平滑刷新信息流
|
||
if (oldCity !== this.selectedCity) {
|
||
this.page = 1
|
||
this.fetchFeed(true)
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
},
|
||
|
||
/**
|
||
* 信息流拉取(具备请求时序保护与竞态校验,筛选只到市级:全国/省/市)
|
||
* @param {boolean} reset 是否重置列表
|
||
*/
|
||
async fetchFeed(reset = false) {
|
||
const seq = ++this.reqSeq
|
||
if (reset && !this.list.length) {
|
||
this.loading = true
|
||
}
|
||
try {
|
||
const res = await homeApi.feed({
|
||
page: this.page,
|
||
size: this.size,
|
||
// 价格排序带方向:price_asc / price_desc(后端按方向排序)
|
||
sort: this.sort === 'price' ? `price_${this.priceOrder}` : this.sort,
|
||
province: !this.areaId && !this.selectedCity && this.selectedProvince ? this.selectedProvince : undefined,
|
||
city: !this.areaId && this.selectedCity ? this.selectedCity : undefined,
|
||
areaId: this.areaId || undefined,
|
||
lat: this.lat != null ? this.lat : undefined,
|
||
lng: this.lng != null ? this.lng : undefined,
|
||
})
|
||
// 丢弃过期请求
|
||
if (seq !== this.reqSeq) return
|
||
this.list = reset ? res.list : this.list.concat(res.list)
|
||
this.hasMore = res.hasMore
|
||
} catch (e) {
|
||
if (seq !== this.reqSeq) return
|
||
uni.showToast({ title: '列表加载失败,请重试', icon: 'none' })
|
||
} finally {
|
||
if (seq === this.reqSeq) this.loading = false
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 触底加载下一页
|
||
*/
|
||
loadMore() {
|
||
if (!this.hasMore || this.loading) return
|
||
this.page += 1
|
||
this.fetchFeed()
|
||
},
|
||
|
||
/**
|
||
* 切换排序标签(价格项:首次点击升序,再次点击升降序来回切换)
|
||
* @param {string} v 排序方式
|
||
*/
|
||
changeSort(v) {
|
||
if (v === 'price') {
|
||
if (this.sort === 'price') {
|
||
this.priceOrder = this.priceOrder === 'asc' ? 'desc' : 'asc'
|
||
} else {
|
||
this.priceOrder = 'asc'
|
||
}
|
||
} else if (this.sort === v) {
|
||
return
|
||
}
|
||
this.sort = v
|
||
this.page = 1
|
||
this.fetchFeed(true)
|
||
if (v === 'nearby' && (this.lat == null || this.lng == null)) {
|
||
uni.showToast({ title: '正在获取距离定位…', icon: 'none' })
|
||
this.fetchLocation()
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 打开左上角位置手动替换弹窗(回显由 fc-city-picker 内部处理)
|
||
*/
|
||
openLocationModal() {
|
||
this.showLocModal = true
|
||
},
|
||
|
||
/**
|
||
* 城市选择抽屉回调(全国 / 全省 / 全市统一入口)
|
||
* @param {Object} area { province, city },空串表示全国/全省
|
||
*/
|
||
onCityPickerSelect(area) {
|
||
const province = (area && area.province) || ''
|
||
const city = (area && area.city) || ''
|
||
if (!province && !city) {
|
||
this.applyAreaSelection(null, true)
|
||
} else {
|
||
this.applyAreaSelection({
|
||
areaId: 0,
|
||
district: '',
|
||
city,
|
||
province,
|
||
}, true)
|
||
}
|
||
this.closeLocationModal()
|
||
},
|
||
|
||
/**
|
||
* 关闭位置切换弹窗
|
||
*/
|
||
closeLocationModal() {
|
||
this.showLocModal = false
|
||
},
|
||
|
||
/**
|
||
* 统一应用区域选择并持久化(支持全国 / 全省 / 全市)
|
||
* @param {Object|null} area 区域数据
|
||
* @param {boolean} isManual 是否为用户主动操作
|
||
*/
|
||
applyAreaSelection(area, isManual = true) {
|
||
if (!area) {
|
||
this.areaId = 0
|
||
this.areaText = ''
|
||
this.selectedProvince = ''
|
||
this.selectedCity = ''
|
||
this.location = '全国'
|
||
this.manualArea = isManual
|
||
if (isManual) {
|
||
safeSetStorage(STORAGE_KEY_SELECTED_AREA, { areaId: 0, district: '', city: '', province: '' })
|
||
}
|
||
} else {
|
||
this.areaId = Number(area.areaId) || 0
|
||
this.areaText = area.district || ''
|
||
this.selectedProvince = area.province || ''
|
||
this.selectedCity = area.city || ''
|
||
this.location = area.city || area.province || '全国'
|
||
this.manualArea = isManual
|
||
if (isManual) {
|
||
safeSetStorage(STORAGE_KEY_SELECTED_AREA, {
|
||
areaId: this.areaId,
|
||
district: this.areaText,
|
||
city: this.selectedCity,
|
||
province: this.selectedProvince,
|
||
})
|
||
}
|
||
}
|
||
this.page = 1
|
||
this.fetchFeed(true)
|
||
},
|
||
|
||
/**
|
||
* 弹窗内:重新定位当前位置并切换
|
||
*/
|
||
relocateCurrentPosition() {
|
||
if (this.isLocating) return
|
||
this.isLocating = true
|
||
if (typeof uni === 'undefined' || !uni.getLocation) {
|
||
this.isLocating = false
|
||
return
|
||
}
|
||
uni.getLocation({
|
||
type: 'gcj02',
|
||
success: async (res) => {
|
||
this.lat = res.latitude
|
||
this.lng = res.longitude
|
||
try {
|
||
const found = await homeApi.locate({ lat: this.lat, lng: this.lng })
|
||
if (found && found.areaId) {
|
||
// 清除手动选择标记,恢复为自动定位匹配模式(只到市级)
|
||
safeSetStorage(STORAGE_KEY_SELECTED_AREA, null)
|
||
this.manualArea = false
|
||
this.areaId = 0
|
||
this.areaText = ''
|
||
this.selectedProvince = found.province || ''
|
||
this.selectedCity = found.city || ''
|
||
this.location = found.city || ''
|
||
this.page = 1
|
||
this.fetchFeed(true)
|
||
uni.showToast({ title: `已定位到:${found.city || '所在城市'}`, icon: 'none' })
|
||
this.closeLocationModal()
|
||
} else {
|
||
uni.showToast({ title: '已定位,但当前位置暂无开通服务', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: '定位匹配失败,请手动选择', icon: 'none' })
|
||
} finally {
|
||
this.isLocating = false
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.isLocating = false
|
||
uni.showToast({ title: '获取定位权限失败,请在设置中开启', icon: 'none' })
|
||
},
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 空状态引导动作处理(审核模式下不开放发布入口)
|
||
*/
|
||
handleEmptyAction() {
|
||
if (this.isAudit) return
|
||
safeNavigateTo('/pages/post/create')
|
||
},
|
||
|
||
/**
|
||
* 页面跳转助手(带防连击与栈溢出保护)
|
||
* @param {string} url 目标路径
|
||
*/
|
||
go(url) {
|
||
if (!url) return
|
||
safeNavigateTo(url)
|
||
},
|
||
|
||
/**
|
||
* 规范跳转到全部分类 Tab 页面(平滑重定向,杜绝 reLaunch 全局冷重启白屏)
|
||
*/
|
||
goCategoryTab() {
|
||
safeRedirectTo('/pages/category/index')
|
||
},
|
||
|
||
/**
|
||
* 搜索栏点击跳转
|
||
*/
|
||
goSearch() {
|
||
safeNavigateTo('/pages/search/index')
|
||
},
|
||
|
||
/**
|
||
* 点击金刚区分类项(直达二级分类频道页,自动激活选中该二级分类并筛选其同城帖子)
|
||
* @param {Object} c 二级分类数据
|
||
*/
|
||
goCategory(c) {
|
||
if (!c) return
|
||
if (c.isAll || c.id === 0) {
|
||
this.goCategoryTab()
|
||
return
|
||
}
|
||
safeNavigateTo(`/pages/category/channel?categoryId=${c.id}&name=${encodeURIComponent(c.name)}`)
|
||
},
|
||
|
||
/**
|
||
* 点击轮播项跳转
|
||
* @param {Object} b 轮播数据
|
||
*/
|
||
goBanner(b) {
|
||
if (!b || !b.link) return
|
||
const url = String(b.link).trim()
|
||
if (!url) return
|
||
if (/^https?:\/\//i.test(url)) {
|
||
// #ifdef H5
|
||
window.location.href = url
|
||
// #endif
|
||
// #ifndef H5
|
||
uni.setClipboardData({
|
||
data: url,
|
||
success: () => uni.showToast({ title: '链接已复制,可前往浏览器打开', icon: 'none' }),
|
||
})
|
||
// #endif
|
||
return
|
||
}
|
||
const targetUrl = url.startsWith('/') ? url : `/${url}`
|
||
const tabPages = ['/pages/index/index', '/pages/category/index', '/pages/message/index', '/pages/mine/index']
|
||
const pathOnly = targetUrl.split('?')[0]
|
||
if (tabPages.includes(pathOnly)) {
|
||
safeRedirectTo(pathOnly)
|
||
} else {
|
||
safeNavigateTo(targetUrl)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 点击进入帖子详情
|
||
* @param {Object} p 帖子对象
|
||
*/
|
||
goDetail(p) {
|
||
if (!p || !p.id) return
|
||
safeNavigateTo(`/pages/detail/detail?id=${p.id}`)
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 首页吸顶导航栏(状态栏 + 定位/消息胶囊栏向上滚动时固定) */
|
||
.home__nav {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
width: 100%;
|
||
background: #d6f7e5;
|
||
background: linear-gradient(180deg, #d6f7e5 0%, #dcf8e8 100%);
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
|
||
}
|
||
.home__header {
|
||
background: $fc-gradient-hero;
|
||
padding: 8rpx 0 70rpx;
|
||
}
|
||
.home__header-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 6rpx 24rpx 0;
|
||
gap: 16rpx;
|
||
}
|
||
/* 左上角定位胶囊(可手动点击替换,带交互微动效) */
|
||
.home__location {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 56rpx;
|
||
padding: 0 18rpx 0 14rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(0, 168, 84, 0.18);
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
|
||
border-radius: 999rpx;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
min-width: 0;
|
||
&:active {
|
||
opacity: 0.82;
|
||
transform: scale(0.97);
|
||
background: #f8fafc;
|
||
}
|
||
}
|
||
.home__location-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
margin-right: 8rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.home__location-text {
|
||
font-size: 26rpx;
|
||
color: #1e293b !important; /* 彻底告别白字,高对比现代炭黑 */
|
||
font-weight: 700;
|
||
max-width: 210rpx;
|
||
}
|
||
.home__location-arrow {
|
||
font-size: 20rpx;
|
||
color: #64748b !important; /* 中性现代灰 */
|
||
margin-left: 6rpx;
|
||
}
|
||
.home__search {
|
||
margin: 20rpx 24rpx 0;
|
||
height: 72rpx;
|
||
border-radius: $fc-radius-pill;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(4, 48, 30, 0.12);
|
||
}
|
||
.home__search-ph {
|
||
margin-left: 12rpx;
|
||
font-size: $fc-font-sm;
|
||
color: $fc-text-placeholder;
|
||
}
|
||
.home__msg {
|
||
position: relative;
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.home__msg-dot {
|
||
position: absolute;
|
||
top: -4rpx;
|
||
right: -8rpx;
|
||
min-width: 30rpx;
|
||
height: 30rpx;
|
||
padding: 0 6rpx;
|
||
border-radius: 999rpx;
|
||
background: #ffffff;
|
||
color: $fc-primary-text;
|
||
font-size: 20rpx;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 规范化纯图片轮播卡片(702×280rpx) */
|
||
.home__banner {
|
||
width: 702rpx;
|
||
height: 280rpx;
|
||
margin: -50rpx 24rpx 0;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 168, 84, 0.08);
|
||
background: #eafbf1;
|
||
}
|
||
.home__banner--skeleton {
|
||
width: 702rpx;
|
||
height: 280rpx;
|
||
margin: -50rpx 24rpx 0;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 12rpx;
|
||
}
|
||
.home__banner-sk-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 18rpx;
|
||
}
|
||
.home__banner-item {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
}
|
||
.home__banner-photo,
|
||
.home__banner-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
/* 分类金刚区 */
|
||
.home__cats {
|
||
margin-top: 24rpx;
|
||
}
|
||
.home__cats-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.home__cats-title {
|
||
font-size: $fc-font-lg;
|
||
font-weight: 700;
|
||
color: $fc-text;
|
||
}
|
||
.home__cats-more {
|
||
font-size: $fc-font-sm;
|
||
color: $fc-text-secondary;
|
||
}
|
||
.home__cats-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
.home__cat {
|
||
width: 25%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
.home__cat-icon {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.home__cat-badge {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 26rpx;
|
||
transition: transform 0.15s ease;
|
||
box-shadow: 0 4rpx 14rpx rgba(4, 48, 30, 0.05);
|
||
&:active {
|
||
transform: scale(0.96);
|
||
}
|
||
}
|
||
.home__cat-name {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
color: $fc-text-regular;
|
||
line-height: 1.2;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 会员引导条 */
|
||
.home__vip--audit {
|
||
background: linear-gradient(135deg, #00D26A 0%, #00A854 100%) !important;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 168, 84, 0.22) !important;
|
||
|
||
.home__vip-btn {
|
||
background: #FFFFFF !important;
|
||
color: #04301E !important;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08) !important;
|
||
}
|
||
}
|
||
|
||
.home__vip {
|
||
margin: 0 24rpx 24rpx;
|
||
padding: 24rpx 28rpx;
|
||
border-radius: $fc-radius-lg;
|
||
background: $fc-gradient-warm;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
box-shadow: $fc-shadow;
|
||
}
|
||
.home__vip-left {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.home__vip-left .fc-icon {
|
||
flex-shrink: 0;
|
||
}
|
||
.home__vip-texts {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 14rpx;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.home__vip-title {
|
||
color: #ffffff;
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.home__vip-sub {
|
||
color: rgba(255, 255, 255, 0.92);
|
||
font-size: 22rpx;
|
||
margin-top: 6rpx;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.home__vip-btn {
|
||
flex-shrink: 0;
|
||
white-space: nowrap;
|
||
padding: 12rpx 28rpx;
|
||
border-radius: $fc-radius-pill;
|
||
background: #ffffff;
|
||
color: $fc-primary-text;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
/* 筛选排序栏 */
|
||
.home__filter {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx 16rpx;
|
||
}
|
||
.home__filter-item {
|
||
margin-right: 28rpx;
|
||
font-size: $fc-font;
|
||
color: $fc-text-secondary;
|
||
transition: color 0.2s ease;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
&--active {
|
||
color: $fc-primary-text;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
/* 价格排序双三角指示器(未选中灰、选中主题绿) */
|
||
.home__caret {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-left: 6rpx;
|
||
gap: 3rpx;
|
||
}
|
||
.home__caret-up {
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 7rpx solid transparent;
|
||
border-right: 7rpx solid transparent;
|
||
border-bottom: 9rpx solid #cbd5e1;
|
||
&.is-on {
|
||
border-bottom-color: #00a854;
|
||
}
|
||
}
|
||
.home__caret-down {
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 7rpx solid transparent;
|
||
border-right: 7rpx solid transparent;
|
||
border-top: 9rpx solid #cbd5e1;
|
||
&.is-on {
|
||
border-top-color: #00a854;
|
||
}
|
||
}
|
||
.home__filter-area-btn {
|
||
margin-left: auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 8rpx 20rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(0, 168, 84, 0.22);
|
||
border-radius: 999rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.06);
|
||
}
|
||
.home__filter-area-icon {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
margin-right: 6rpx;
|
||
}
|
||
.home__filter-area-txt {
|
||
font-size: 24rpx;
|
||
color: #00a854;
|
||
font-weight: 600;
|
||
max-width: 180rpx;
|
||
}
|
||
.home__filter-area-arrow {
|
||
font-size: 20rpx;
|
||
color: #00a854;
|
||
margin-left: 6rpx;
|
||
}
|
||
|
||
/* 拟真骨架屏列表 */
|
||
.home__feed-skeletons {
|
||
padding: 0 24rpx;
|
||
}
|
||
.home__skeleton-card {
|
||
display: flex;
|
||
background: #ffffff;
|
||
padding: 24rpx;
|
||
border-radius: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 4rpx 18rpx rgba(4, 48, 30, 0.05);
|
||
}
|
||
.home__skeleton-thumb {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
flex: 0 0 200rpx;
|
||
border-radius: 16rpx;
|
||
margin-right: 24rpx;
|
||
}
|
||
.home__skeleton-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
.home__skeleton-line {
|
||
width: 100%;
|
||
}
|
||
.home__skeleton-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 14rpx;
|
||
}
|
||
.home__skeleton-bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
/* 底部状态 */
|
||
.home__loadmore {
|
||
text-align: center;
|
||
padding: 24rpx 0 32rpx;
|
||
font-size: $fc-font-sm;
|
||
color: $fc-text-placeholder;
|
||
}
|
||
.home__tabbar-space {
|
||
height: 130rpx;
|
||
}
|
||
|
||
/* ============================================================
|
||
位置手动替换弹窗:抽屉外壳与级联面板样式已抽至公共组件 fc-city-picker,
|
||
此处仅保留标题区与定位状态卡片(通过 header 插槽注入)
|
||
========================================================== */
|
||
.home__modal-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 28rpx;
|
||
}
|
||
.home__modal-title-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.home__modal-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: $fc-text;
|
||
}
|
||
.home__modal-subtitle {
|
||
font-size: 22rpx;
|
||
color: $fc-text-secondary;
|
||
margin-top: 6rpx;
|
||
}
|
||
.home__modal-close {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 50%;
|
||
background: #f1f8f4;
|
||
color: #608573;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
/* 当前定位状态卡片 */
|
||
.home__modal-loc-card {
|
||
background: #f4fbf7;
|
||
border: 1rpx solid #c9ebd8;
|
||
border-radius: 20rpx;
|
||
padding: 24rpx 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
.home__modal-loc-info {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.home__modal-loc-pin {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
flex-shrink: 0;
|
||
margin-right: 16rpx;
|
||
}
|
||
.home__modal-loc-texts {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
.home__modal-loc-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.home__modal-loc-label {
|
||
font-size: 26rpx;
|
||
color: $fc-text-regular;
|
||
}
|
||
.home__modal-loc-val {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: $fc-primary-text;
|
||
max-width: 220rpx;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.home__modal-badge {
|
||
font-size: 20rpx;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
margin-left: 12rpx;
|
||
font-weight: 600;
|
||
&--manual {
|
||
background: #e6f6ee;
|
||
color: $fc-primary-text;
|
||
border: 1rpx solid #b5e8cb;
|
||
}
|
||
&--gps {
|
||
background: #00e070;
|
||
color: $fc-primary-ink;
|
||
}
|
||
}
|
||
.home__modal-loc-sub {
|
||
font-size: 22rpx;
|
||
color: $fc-text-secondary;
|
||
margin-top: 6rpx;
|
||
}
|
||
.home__modal-reloc-btn {
|
||
flex-shrink: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid #b7e6cc;
|
||
color: $fc-primary-text;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
box-shadow: 0 4rpx 12rpx rgba(4, 48, 30, 0.06);
|
||
transition: all 0.2s ease;
|
||
&:active {
|
||
transform: scale(0.96);
|
||
background: #ebfaf1;
|
||
}
|
||
&--loading {
|
||
opacity: 0.75;
|
||
}
|
||
}
|
||
.home__modal-reloc-icon {
|
||
font-size: 26rpx;
|
||
margin-right: 6rpx;
|
||
font-weight: bold;
|
||
&--spin {
|
||
display: inline-block;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
}
|
||
|
||
/* 动效(抽屉淡入/上滑动效已在 fc-city-picker 组件内) */
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|