文件
fenleixinxi/uniapp/pages/index/index.vue
MiAn 02fc28613a feat: 深度强化微信小程序端虚拟支付提审防审模式
1. App.vue 启动时立即预加载 uiConfig,杜绝首屏网络延时导致瞬间闪现付费会员入口
2. 会员中心 (member/index.vue):在提审防审模式下直接展示纯净【便民服务指南】卡片,杜绝任何 VIP 皇冠、会员标价、折扣与充值入口
3. 个人中心 (mine/index.vue):提审防审模式下彻底隐藏会员卡片与我的余额,紧凑上移卡片布局
4. 订单明细、充值页与消息中心增加防审合规兼容,避免误触发任何虚拟支付诱导
2026-11-02 17:42:49 +08:00

1475 行
40 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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="去发布"
@action="go('/pages/post/create')"
/>
<!-- 真实信息流卡片列表 -->
<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'
// 本地离线快照缓存 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(true)
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' })
},
})
},
/**
* 页面跳转助手
* @param {string} url 目标路径
*/
go(url) {
if (!url) return
uni.navigateTo({ url })
},
/**
* 规范跳转到全部分类 Tab 页面
*/
goCategoryTab() {
// #ifdef H5
uni.redirectTo({
url: '/pages/category/index',
fail: () => uni.reLaunch({ url: '/pages/category/index' }),
})
// #endif
// #ifndef H5
uni.reLaunch({ url: '/pages/category/index' })
// #endif
},
/**
* 搜索栏点击跳转
*/
goSearch() {
uni.navigateTo({ url: '/pages/search/index' })
},
/**
* 点击金刚区分类项(直达二级分类频道页,自动激活选中该二级分类并筛选其同城帖子)
* @param {Object} c 二级分类数据
*/
goCategory(c) {
if (!c) return
if (c.isAll || c.id === 0) {
this.goCategoryTab()
return
}
uni.navigateTo({ url: `/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)) {
uni.switchTab({ url: pathOnly })
} else {
uni.navigateTo({
url: targetUrl,
fail: (err) => console.warn('轮播跳转失败:', err),
})
}
},
/**
* 点击进入帖子详情
* @param {Object} p 帖子对象
*/
goDetail(p) {
uni.navigateTo({ url: `/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>